diff --git a/.gitignore b/.gitignore index 3585f2b73ecf1c9ddc9c46a21d91f18dbe79b937..384fa9f8d9d163f7bd144a4d87e4897fc5e3b7ac 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Local History for Visual Studio Code .history/ +.DS_Store ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider diff --git a/Documentation/documentation.md b/Documentation/documentation.md new file mode 100644 index 0000000000000000000000000000000000000000..abf547283ee95acd8534a696bcb96fbd5e63fe25 --- /dev/null +++ b/Documentation/documentation.md @@ -0,0 +1,7 @@ +# FitConnect SDK User Guide + +## Introduction + +## Examples + +[Examples](./example.md) diff --git a/example.md b/Documentation/example.md similarity index 73% rename from example.md rename to Documentation/example.md index 49b965424a1c087744c66632f66d0e823339e715..033abed93f0aeb70fbb8a9f82f8decf61c3492d8 100644 --- a/example.md +++ b/Documentation/example.md @@ -24,6 +24,20 @@ async Task AbstractCall() { ## The more verbose way to call the FitConnect API +### The Fluent Sender Api call +```csharp +void FluentSenderCall() { + var client = + new Client(FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development), + logger: _logger); + client.Sender + .Authenticate(clientId, clientSecret) + .CreateSubmission(new Submission()) + .UploadAttachments(new List<Attachment>()) + .SendSubmission(new Metadata(), new Data()); +} +``` + ### Here to call the **Sender** interface ```csharp diff --git a/working_notes.md b/Documentation/working_notes.md similarity index 100% rename from working_notes.md rename to Documentation/working_notes.md diff --git a/DummyClient/DummyClient.csproj b/DummyClient/DummyClient.csproj index e449bbc26417523e8a0a2dc0ac0878d7f2f50811..6c218fbb2f85b3b34dce23b65399f248a2677605 100644 --- a/DummyClient/DummyClient.csproj +++ b/DummyClient/DummyClient.csproj @@ -9,7 +9,7 @@ </PropertyGroup> <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> </ItemGroup> </Project> diff --git a/DummyClient/Program.cs b/DummyClient/Program.cs index 912c74464028655d299c8ebea61394c2654db9bc..c60290a01226fe8d94fc2f2aab570b5a1510fcde 100644 --- a/DummyClient/Program.cs +++ b/DummyClient/Program.cs @@ -1,12 +1,20 @@ -using FitConnect; +using System.Security.Cryptography.X509Certificates; +using FitConnect; using FitConnect.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +var clientId = ""; +var clientSecret = ""; +PublicKey publicKey; +ILogger _logger; +Client client; /* * The easy way to call the FitConnect API */ async Task AbstractCall() { - var client = new Client( + client = new Client( FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development), "clientId", "clientSecret"); @@ -18,18 +26,49 @@ async Task AbstractCall() { } +void FluentSenderCall() { + client.Sender + .Authenticate(clientId!, clientSecret!) + .CreateSubmission(new Submission()) + .UploadAttachments(new List<Attachment>()) + .SendSubmission(new Metadata(), new Data()); +} + +void FluentSubscriberCall() { + client.Subscriber + // Polling available submissions + .Authenticate(clientId!, clientSecret!) + .PollSubmissions("destinationId", out var _) + // + // Getting list of submissions + .Authenticate(clientId!, clientSecret!) + .GetSubmissions("destinationId", out var _) + // + // Reading submission + .Authenticate(clientId!, clientSecret!) + .GetSubmission("destinationId", "submissionId", out var _) + .GetAttachments(out var _) + .DecryptAttachments(out var _) + .DecryptData(out var _) + .DecryptMetadata(out var _) + .ConfirmSubmission(); +} + /* * The more verbose way to call the FitConnect API * Here to call the **Sender** interface */ -void DetailSenderCall() { +async Task DetailSenderCall() { var sender = new Sender( FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development)); - sender.CreateSubmissionDto(new Submission()); - /* - .... - */ + + var submissionDto = sender.AddSubmission(new Submission()); + var encryptedAttachments = sender.Encrypt(publicKey, new List<Attachment>()); + var success = await sender.UploadAttachmentsAsync(encryptedAttachments); + var encryptedMetadata = sender.Encrypt(publicKey, new Metadata()); + var encryptedData = sender.Encrypt(publicKey, new Data()); + success = await sender.SubmitAsync(encryptedData, encryptedMetadata); } /* @@ -50,3 +89,8 @@ async Task DetailSubscriberCall() { Console.WriteLine( "This is a dummy client to demonstrate the usage of the FitConnect SDK for .NET"); + +_logger = new Microsoft.Extensions.Logging.Logger<System.AppDomain>(new NullLoggerFactory()); +client = + new Client(FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development), + logger: _logger); diff --git a/FitConnect/BaseClasses/FunctionalBaseClass.cs b/FitConnect/BaseClasses/FunctionalBaseClass.cs index 99143f74a7a8c35828c2ef7d2065039e79906fa9..0483bf91e4eeec23934e796155c68e7d0290e239 100644 --- a/FitConnect/BaseClasses/FunctionalBaseClass.cs +++ b/FitConnect/BaseClasses/FunctionalBaseClass.cs @@ -11,6 +11,8 @@ public abstract class FunctionalBaseClass { protected readonly FitConnectApiService ApiService; public readonly IEncryption Encryption; protected readonly ILogger? Logger; + internal Client Owner { get; set; } + /// <summary> /// Constructor for the FunctionalBaseClass diff --git a/FitConnect/Client.cs b/FitConnect/Client.cs index c7a95da841f021445eb8a6abd70fdfc13a8cf020..4daf71cf2a7c8f1fb07ee93da01ac613bbebb6c3 100644 --- a/FitConnect/Client.cs +++ b/FitConnect/Client.cs @@ -9,20 +9,26 @@ namespace FitConnect; /// </summary> // ReSharper disable once UnusedType.Global public class Client { - private readonly string _clientId; - private readonly string _clientSecret; - private readonly Sender _sender; - private readonly Subscriber _subscriber; + internal string? ClientId; + internal string? ClientSecret; + private readonly X509Certificate2? _senderCertificate; + private readonly X509Certificate2? _receiverCertificate; + public FluentSender Sender { get; } + public FluentSubscriber Subscriber { get; } public Client(FitConnectEndpoints endpoints, - string clientId, string clientSecret, - X509Certificate2? certificate = null, + string? clientId = null, string? clientSecret = null, + X509Certificate2? senderCertificate = null, + X509Certificate2? receiverCertificate = null, ILogger? logger = null) { - _clientId = clientId; - _clientSecret = clientSecret; - _sender = new Sender(endpoints, certificate, logger); - _subscriber = new Subscriber(endpoints, certificate, logger); + ClientId = clientId; + ClientSecret = clientSecret; + _senderCertificate = senderCertificate; + _receiverCertificate = receiverCertificate; + + Sender = new FluentSender(endpoints, _senderCertificate, logger) { Owner = this }; + Subscriber = new FluentSubscriber(endpoints, _receiverCertificate, logger) { Owner = this }; //_service = new FitConnectApiService(endpoints, logger); } @@ -45,7 +51,7 @@ public class Client { string.IsNullOrEmpty(areaId)) throw new ArgumentException("One of the area specifier has to be set"); - return await _sender.GetDestinationIdAsync(leiaKey, ags, ars, areaId); + return await Sender.GetDestinationIdAsync(leiaKey, ags, ars, areaId); } /// <summary> @@ -57,23 +63,24 @@ public class Client { // ReSharper disable once MemberCanBePrivate.Global public async Task<bool> SendSubmissionAsync(Submission submission) { // Einreichung anlegen und ankündigen - var createSubmissionDto = _sender.CreateSubmissionDto(submission); + var createSubmissionDto = Sender.AddSubmission(submission); // Anlagen verschlüsseln var encryptedAttachments = submission.Attachments - .Select(a => _sender.Encrypt("", a)).ToList(); + .Select(a => Sender.Encrypt(_senderCertificate.PublicKey, a)) + .ToDictionary(p => p.Key, p => p.Value); // Anlagen hochladen - await _sender.UploadAttachmentsAsync(encryptedAttachments); + await Sender.UploadAttachmentsAsync(encryptedAttachments); // Metadaten befüllen und verschlüsseln - var metaData = await _sender.CreateMetadataAsync("", new byte[] { }); + var metaData = await Sender.CreateMetadataAsync("", new byte[] { }); // Fachdaten verschlüsseln - var encryptedData = _sender.Encrypt("", submission.Data); + var encryptedData = Sender.Encrypt(_senderCertificate.PublicKey, submission.Data); // Fachdaten & Metadaten hochladen und Einreichung absenden - await _sender.SubmitSubmissionAsync(submission); + await Sender.SubmitSubmissionAsync(submission); return true; } @@ -98,7 +105,7 @@ public class Client { /// <param name="id">Id of the submission</param> /// <returns></returns> public async Task<Submission> GetSubmissionAsync(string id) { - return await _subscriber.GetSubmissionAsync(id); + return await Subscriber.GetSubmissionAsync(id); } /// <summary> diff --git a/FitConnect/FitConnect.csproj b/FitConnect/FitConnect.csproj index d07b3e6cdb4d51702f466661d13a5dc1406da0a5..30c89efb000c08f73948a96e07ca5647cf6c4103 100644 --- a/FitConnect/FitConnect.csproj +++ b/FitConnect/FitConnect.csproj @@ -11,24 +11,24 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1"/> - <PackageReference Include="Newtonsoft.Json" Version="13.0.1"/> - <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14"/> - <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.19.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.19.0" /> </ItemGroup> <ItemGroup> - <None Remove="metadata.schema.json"/> - <EmbeddedResource Include="metadata.schema.json"/> + <None Remove="metadata.schema.json" /> + <EmbeddedResource Include="metadata.schema.json" /> </ItemGroup> <ItemGroup> - <Folder Include="Interfaces"/> + <Folder Include="Interfaces" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\Models\Models.csproj"/> - <ProjectReference Include="..\Services\Services.csproj"/> + <ProjectReference Include="..\Models\Models.csproj" /> + <ProjectReference Include="..\Services\Services.csproj" /> </ItemGroup> </Project> diff --git a/FitConnect/FluentSender.cs b/FitConnect/FluentSender.cs new file mode 100644 index 0000000000000000000000000000000000000000..2867a5bf671fd65232fec799f01f32809eeec577 --- /dev/null +++ b/FitConnect/FluentSender.cs @@ -0,0 +1,69 @@ +using System.Security.Cryptography.X509Certificates; +using FitConnect.Models; +using FitConnect.Services.Models; +using Microsoft.Extensions.Logging; + +namespace FitConnect; + +public class FluentSender : Sender { + private OAuthAccessToken? token; + public PublicKey PublicKey { get; set; } + public CreateSubmissionDto? NewSubmission { get; set; } + + public static Sender Create() { + throw new NotImplementedException(); + } + + public FluentSender Authenticate(string clientId, string clientSecret, string? scope = null) { + Owner.ClientId = clientId; + Owner.ClientSecret = clientSecret; + + token ??= AuthenticateAsync(clientId, clientSecret, scope).Result; + return this; + } + + /// <summary> + /// Creates a new <see cref="Submission"/> on the FitConnect server. + /// </summary> + /// <param name="submission"></param> + /// <returns></returns> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="ArgumentException"></exception> + public FluentSender CreateSubmission(Submission submission) { + if (token == null) + throw new InvalidOperationException("You must authenticate first."); + + if (!submission.IsSubmissionReadyToAdd(out var errorMessage)) + throw new ArgumentException($"Submission is not ready to add. {errorMessage}"); + + NewSubmission = AddSubmission(submission); + + return this; + } + + public FluentSender SendSubmission() { + throw new NotImplementedException(); + } + + public FluentSender UploadAttachments(List<Attachment> attachments) { + if (NewSubmission == null) + throw new InvalidOperationException("You must create a submission first."); + + if (attachments.Count == 0) + return this; + + var encryptedAttachments = Encrypt(PublicKey, attachments); + UploadAttachmentsAsync(encryptedAttachments).Wait(); + + return this; + } + + public FluentSender SendSubmission(Metadata metadata, Data? data = null) { + throw new NotImplementedException(); + } + + public FluentSender(FitConnectEndpoints endpoints, X509Certificate2? certificate = null, + ILogger? logger = null) : base(endpoints, certificate, logger) { + PublicKey = certificate.PublicKey; + } +} diff --git a/FitConnect/FluentSubscriber.cs b/FitConnect/FluentSubscriber.cs new file mode 100644 index 0000000000000000000000000000000000000000..4e1e19df98bdba2291d75f9770be7d9b74889ef9 --- /dev/null +++ b/FitConnect/FluentSubscriber.cs @@ -0,0 +1,58 @@ +using System.Security.Cryptography.X509Certificates; +using FitConnect.Models; +using FitConnect.Services.Models; +using Microsoft.Extensions.Logging; + +namespace FitConnect; + +public class FluentSubscriber : Subscriber { + private OAuthAccessToken? token; + + public FluentSubscriber(FitConnectEndpoints endpoints, X509Certificate2? certificate = null, + ILogger? logger = null) : base(endpoints, certificate, logger) { + } + + public FluentSubscriber + Authenticate(string clientId, string clientSecret, string? scope = null) { + Owner.ClientId = clientId; + Owner.ClientSecret = clientSecret; + + token ??= AuthenticateAsync(clientId, clientSecret, scope).Result; + + return this; + } + + public FluentSubscriber GetSubmissions(string destinationId, out List<Submission> submissions) { + submissions = new List<Submission>(); + throw new NotImplementedException(); + } + + public FluentSubscriber GetSubmission(string destinationId, string submissionId, + out Submission submission) { + throw new NotImplementedException(); + } + + public FluentSubscriber PollSubmissions(string destinationId, out int availableSubmissions) { + throw new NotImplementedException(); + } + + public FluentSubscriber GetAttachments(out object unknown) { + throw new NotImplementedException(); + } + + public FluentSubscriber DecryptAttachments(out object o) { + throw new NotImplementedException(); + } + + public FluentSubscriber DecryptData(out object o) { + throw new NotImplementedException(); + } + + public FluentSubscriber DecryptMetadata(out object o) { + throw new NotImplementedException(); + } + + public FluentSubscriber ConfirmSubmission() { + throw new NotImplementedException(); + } +} diff --git a/FitConnect/Models/Metadata.cs b/FitConnect/Models/Metadata.cs new file mode 100644 index 0000000000000000000000000000000000000000..8caccfe8ed2d4f0decaf77e6b527e09c2c2a8ec7 --- /dev/null +++ b/FitConnect/Models/Metadata.cs @@ -0,0 +1,5 @@ +namespace FitConnect.Models; + +public record Metadata; + +public record Data; diff --git a/FitConnect/Models/Submission.cs b/FitConnect/Models/Submission.cs index 64ae38a828eb26cc6cd29cf542cb548955fa6f95..e12ed517037f720824acf4940e6ea5a076e848d2 100644 --- a/FitConnect/Models/Submission.cs +++ b/FitConnect/Models/Submission.cs @@ -13,6 +13,15 @@ public record Submission { public Callback? Callback { get; set; } + public bool IsSubmissionReadyToAdd(out string? error) { + error = null; + return true; + } + + public bool IsSubmissionReadyToSend() { + return true; + } + /// <summary> /// Fachdaten diff --git a/FitConnect/Sender.cs b/FitConnect/Sender.cs index 76245eb25a091ebce732bb5ebcc5f48a7d98c15a..c265c97d8305fb05a4c47de4ae8d6a0745cf52be 100644 --- a/FitConnect/Sender.cs +++ b/FitConnect/Sender.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; namespace FitConnect; -public class Sender : FunctionalBaseClass { +public partial class Sender : FunctionalBaseClass { public Sender(FitConnectEndpoints endpoints, X509Certificate2? certificate = null, ILogger? logger = null) : base(logger, endpoints, certificate) { @@ -21,7 +21,7 @@ public class Sender : FunctionalBaseClass { throw new NotImplementedException(); } - public async Task<string> Encrypt(string publicKey, byte[] attachment) { + public async Task<string> Encrypt(PublicKey publicKey, byte[] attachment) { throw new NotImplementedException(); } @@ -50,7 +50,7 @@ public class Sender : FunctionalBaseClass { /// </summary> /// <param name="data"></param> /// <returns></returns> - public string Encrypt(string publicKey, string? data) { + public string Encrypt(PublicKey publicKey, string? data) { if (data == null) return ""; throw new NotImplementedException(); } @@ -61,10 +61,20 @@ public class Sender : FunctionalBaseClass { /// <param name="publicKey"></param> /// <param name="attachment"></param> /// <returns></returns> - public KeyValuePair<string, string> Encrypt(string publicKey, Attachment attachment) { + public KeyValuePair<string, string> Encrypt(PublicKey publicKey, Attachment attachment) { throw new NotImplementedException(); } + /// <summary> + /// Encrypt attachments (Anhänge) + /// </summary> + /// <param name="publicKey">Public key for encryption</param> + /// <param name="attachments">List of attachments to encrypt</param> + /// <returns></returns> + public Dictionary<string, string> + Encrypt(PublicKey publicKey, IEnumerable<Attachment> attachments) => + attachments.Select(a => Encrypt(publicKey, a)).ToDictionary(p => p.Key, p => p.Value); + /// <summary> /// Create Metadata incl. Hash /// </summary> @@ -75,7 +85,7 @@ public class Sender : FunctionalBaseClass { throw new NotImplementedException(); } - public CreateSubmissionDto CreateSubmissionDto(Submission submission) { + public CreateSubmissionDto AddSubmission(Submission submission) { return new() { DestinationId = submission.DestinationId, ServiceType = (ServiceTypeDto)submission.ServiceType, @@ -83,8 +93,13 @@ public class Sender : FunctionalBaseClass { }; } - public async Task UploadAttachmentsAsync( - List<KeyValuePair<string, string>> encryptedAttachments) { + /// <summary> + /// Uploading the encrypted data to the server + /// </summary> + /// <param name="encryptedAttachments">Encrypted attachments with id and content</param> + /// <exception cref="NotImplementedException"></exception> + public async Task<bool> UploadAttachmentsAsync( + Dictionary<string, string> encryptedAttachments) { throw new NotImplementedException(); } @@ -96,4 +111,37 @@ public class Sender : FunctionalBaseClass { string? areaId) { return ApiService.RouteService.GetDestinationIdAsync(leiaKey, ags, ars, areaId); } + + + /// <summary> + /// Encrypt Metadata with public key + /// </summary> + /// <param name="publicKey"></param> + /// <param name="metadata"></param> + /// <returns></returns> + /// <exception cref="NotImplementedException"></exception> + public string Encrypt(PublicKey publicKey, Metadata metadata) { + throw new NotImplementedException(); + } + + /// <summary> + /// Encrypt data with public key (Fachdaten) + /// </summary> + /// <param name="publicKey"></param> + /// <param name="data"></param> + /// <returns></returns> + /// <exception cref="NotImplementedException"></exception> + public string Encrypt(PublicKey publicKey, Data data) { + throw new NotImplementedException(); + } + + /// <summary> + /// Uploading the encrypted data and metadata to the server + /// </summary> + /// <param name="encryptedData"></param> + /// <param name="encryptedMetadata"></param> + /// <exception cref="NotImplementedException"></exception> + public async Task<bool> SubmitAsync(string encryptedData, string encryptedMetadata) { + throw new NotImplementedException(); + } } diff --git a/FitConnect/Subscriber.cs b/FitConnect/Subscriber.cs index 59dc16f21b0b3fffb1e3ba23126a93694f314622..a9a53deba643bc6760240f52e781082a707658d1 100644 --- a/FitConnect/Subscriber.cs +++ b/FitConnect/Subscriber.cs @@ -110,4 +110,4 @@ public class Subscriber : FunctionalBaseClass { int limit = 100) { throw new NotImplementedException(); } -} +} \ No newline at end of file diff --git a/Services/RouteService.cs b/Services/RouteService.cs index 109136f0087d8187a48ee306415cf5f796595c5b..4680fb689fa622c359347fa39f505119e375a814 100644 --- a/Services/RouteService.cs +++ b/Services/RouteService.cs @@ -17,6 +17,8 @@ public class RouteService : RestCallService { /// <exception cref="NotImplementedException"></exception> public Task<string> GetDestinationIdAsync(string leiaKey, string? ags, string? ars, string? areaId) { + // TODO: 1st step to implement + throw new NotImplementedException(); } } diff --git a/readme.md b/readme.md index 40b1463002e42aa080cb85558983666f5b507947..ab8a5455732c3c092b77b09577f99fb339e35953 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,13 @@ # Fit-Connect .NET SDK +## **!! IN DEVELOPMENT NOT FOR PRODUCTION USE !!** + **Fit-Connect .NET SDK** is a .NET library for the Fit-Connect API. -## **!! IN DEVELOPMENT NOT FOR PRODUCTION USE !!** +## For information how to use the SDK and FitConnect visit: + +* [SDK-Documentation](./documentation/documentation.md) +* [FitConnect Documentation](https://fit-connect.com/docs) ## Structure