commit 2bbae6f00d8e5bd5f8fd2eb1d61b6a5a791af86f Author: Mark van der Wal Date: Tue Mar 24 09:47:08 2020 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8d0a12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +.vs/ +bin/ +obj/ \ No newline at end of file diff --git a/FarmmapsApi/Extensions.cs b/FarmmapsApi/Extensions.cs new file mode 100644 index 0000000..0f5f774 --- /dev/null +++ b/FarmmapsApi/Extensions.cs @@ -0,0 +1,29 @@ +using System.Net.Http; +using FarmmapsApi.HttpMessageHandlers; +using FarmmapsApi.Models; +using FarmmapsApi.Services; +using IdentityModel.Client; +using Microsoft.Extensions.DependencyInjection; + +namespace FarmmapsApi +{ + public static class Extensions + { + public static IServiceCollection AddFarmmapsServices(this IServiceCollection serviceCollection, Configuration configuration) + { + return serviceCollection + .AddSingleton(configuration) + .AddSingleton(sp => + { + var httpFactory = sp.GetRequiredService(); + return new DiscoveryCache(configuration.DiscoveryEndpointUrl, + () => httpFactory.CreateClient()); + }) + .AddTransient() + .AddTransient() + .AddHttpClient() + .AddHttpMessageHandler() + .Services;; + } + } +} \ No newline at end of file diff --git a/FarmmapsApi/FarmmapsApi.csproj b/FarmmapsApi/FarmmapsApi.csproj new file mode 100644 index 0000000..b75ff51 --- /dev/null +++ b/FarmmapsApi/FarmmapsApi.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.1 + + + + + + + + + + + + diff --git a/FarmmapsApi/HttpMessageHandlers/FarmmapsAuthenticationHandler.cs b/FarmmapsApi/HttpMessageHandlers/FarmmapsAuthenticationHandler.cs new file mode 100644 index 0000000..b727903 --- /dev/null +++ b/FarmmapsApi/HttpMessageHandlers/FarmmapsAuthenticationHandler.cs @@ -0,0 +1,24 @@ +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using IdentityModel; + +namespace FarmmapsApi.HttpMessageHandlers +{ + public class FarmmapsAuthenticationHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (!request.Headers.Authorization.Scheme.Equals(OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer) || + string.IsNullOrEmpty(request.Headers.Authorization.Parameter)) + { + return new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Content = new StringContent("You must authenticate before using the API") + }; + } + return await base.SendAsync(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Models/Configuration.cs b/FarmmapsApi/Models/Configuration.cs new file mode 100644 index 0000000..72ec057 --- /dev/null +++ b/FarmmapsApi/Models/Configuration.cs @@ -0,0 +1,13 @@ +namespace FarmmapsApi.Models +{ + public class Configuration + { + public string Authority { get; set; } + public string Endpoint { get; set; } + public string DiscoveryEndpointUrl { get; set; } + public string RedirectUri { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string[] Scopes { get; set; } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Models/Item.cs b/FarmmapsApi/Models/Item.cs new file mode 100644 index 0000000..f1660b7 --- /dev/null +++ b/FarmmapsApi/Models/Item.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace FarmmapsApi.Models +{ + public class Item + { + public Item() + { + Tags = new List(); + } + + public string Code { get; set; } + + public string Name { get; set; } + + public DateTime? Created { get; set; } + + public DateTime? Updated { get; set; } + + public DateTime? DataDate { get; set; } + + public string ItemType { get; set; } + + public string SourceTask { get; set; } + + public long Size { get; set; } + + public int State { get; set; } + + public string ParentCode { get; set; } + + public JObject Geometry { get; set; } + + public JObject Data { get; set; } + + public IList Tags { get; set; } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Models/ItemRequest.cs b/FarmmapsApi/Models/ItemRequest.cs new file mode 100644 index 0000000..41bb7c7 --- /dev/null +++ b/FarmmapsApi/Models/ItemRequest.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace FarmmapsApi.Models +{ + public class ItemRequest + { + public ItemRequest() + { + Tags = new List(); + } + + public string ParentCode { get; set; } + public string ItemType { get; set; } + public string Name { get; set; } + public DateTime? DataDate { get; set; } + public JObject Geometry { get; set; } + public JObject Data { get; set; } + public IList Tags { get; set; } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Models/ItemTaskStatus.cs b/FarmmapsApi/Models/ItemTaskStatus.cs new file mode 100644 index 0000000..60069b7 --- /dev/null +++ b/FarmmapsApi/Models/ItemTaskStatus.cs @@ -0,0 +1,22 @@ +using System; + +namespace FarmmapsApi.Models +{ + public enum ItemTaskState + { + Error, + Ok, + Scheduled, + Processing, + } + + public class ItemTaskStatus + { + public string TaskType { get; set; } + public string Code { get; set; } + public string Message { get; set; } + public ItemTaskState State { get; set; } + public DateTime? Started { get; set; } + public DateTime? Finished { get; set; } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Models/TaskRequest.cs b/FarmmapsApi/Models/TaskRequest.cs new file mode 100644 index 0000000..d690e4d --- /dev/null +++ b/FarmmapsApi/Models/TaskRequest.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace FarmmapsApi.Models +{ + public class TaskRequest + { + public string TaskType { get; set; } + public string Delay { get; set; } + + public Dictionary attributes { get; set; } + + public TaskRequest() + { + this.attributes = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Models/UserRoot.cs b/FarmmapsApi/Models/UserRoot.cs new file mode 100644 index 0000000..0184b74 --- /dev/null +++ b/FarmmapsApi/Models/UserRoot.cs @@ -0,0 +1,8 @@ +namespace FarmmapsApi.Models +{ + public class UserRoot + { + public string Name { get; set; } + public string Code { get; set; } + } +} \ No newline at end of file diff --git a/FarmmapsApi/ResourceEndpoints.cs b/FarmmapsApi/ResourceEndpoints.cs new file mode 100644 index 0000000..8c29321 --- /dev/null +++ b/FarmmapsApi/ResourceEndpoints.cs @@ -0,0 +1,18 @@ +namespace FarmmapsApi +{ + public static class ResourceEndpoints + { + public const string CURRENTUSER_RESOURCE = "currentuser"; + public const string MYROOTS_RESOURCE = "folders/my_roots"; + + public const string ITEMS_RESOURCE = "items/{0}"; + public const string ITEMS_CREATE_RESOURCE = "items"; + public const string ITEMS_DOWNLOAD_RESOURCE = "items/{0}/download"; + public const string ITEMS_CHILDREN_RESOURCE = "items/{0}/children"; + public const string ITEMS_DELETE_RESOURCE = "items/delete"; + + public const string ITEMTASK_REQUEST_RESOURCE = "items/{0}/tasks"; + public const string ITEMTASKS_RESOURCE = "items/{0}/tasks"; + public const string ITEMTASK_RESOURCE = "items/{0}/tasks/{1}"; + } +} \ No newline at end of file diff --git a/FarmmapsApi/Services/FarmmapsApiService.cs b/FarmmapsApi/Services/FarmmapsApiService.cs new file mode 100644 index 0000000..392985c --- /dev/null +++ b/FarmmapsApi/Services/FarmmapsApiService.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Security.Authentication; +using System.Threading.Tasks; +using FarmmapsApi.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using static IdentityModel.OidcConstants; + +namespace FarmmapsApi.Services +{ + public class FarmmapsApiService + { + private readonly HttpClient _httpClient; + private readonly OpenIdConnectService _openIdConnectService; + private readonly Configuration _configuration; + + public FarmmapsApiService(HttpClient httpClient, + OpenIdConnectService openIdConnectService, Configuration configuration) + { + _httpClient = httpClient; + _openIdConnectService = openIdConnectService; + _configuration = configuration; + + _httpClient.BaseAddress = new Uri(configuration.Endpoint); + _httpClient.DefaultRequestHeaders.Add("Accept", MediaTypeNames.Application.Json); + } + + public async Task AuthenticateAsync() + { + if (_httpClient.DefaultRequestHeaders.Authorization != null && + _httpClient.DefaultRequestHeaders.Authorization.Scheme != + AuthenticationSchemes.AuthorizationHeaderBearer) + throw new AuthenticationException("Already seems to be authenticated"); + + var disco = await _openIdConnectService.GetDiscoveryDocumentAsync(); + var token = await _openIdConnectService.GetTokenClientCredentialsAsync(disco.TokenEndpoint, + _configuration.ClientId, _configuration.ClientSecret); + + if (token.IsError) + throw new AuthenticationException(token.Error); + + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue(AuthenticationSchemes.AuthorizationHeaderBearer, + token.AccessToken); + } + + public async Task GetCurrentUserCodeAsync() + { + var response = await _httpClient.GetAsync(ResourceEndpoints.CURRENTUSER_RESOURCE); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonString = await response.Content.ReadAsStringAsync(); + var json = JsonConvert.DeserializeObject(jsonString); + return json["code"].Value(); + } + + public async Task> GetCurrentUserRootsAsync() + { + var response = await _httpClient.GetAsync(ResourceEndpoints.MYROOTS_RESOURCE); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonString = await response.Content.ReadAsStringAsync(); + + return JsonConvert.DeserializeObject>(jsonString); + } + + public async Task GetItemAsync(string itemCode) + { + var resourceUri = string.Format(ResourceEndpoints.ITEMS_RESOURCE, itemCode); + var response = await _httpClient.GetAsync(resourceUri); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(jsonString); + } + + public async Task> GetItemChildrenAsync(string itemCode) + { + var resourceUri = string.Format(ResourceEndpoints.ITEMS_CHILDREN_RESOURCE, itemCode); + var response = await _httpClient.GetAsync(resourceUri); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject>(jsonString); + } + + public async Task CreateItemAsync(ItemRequest itemRequest) + { + var jsonString = JsonConvert.SerializeObject(itemRequest); + var response = await _httpClient.PostAsync(ResourceEndpoints.ITEMS_CREATE_RESOURCE, + new StringContent(jsonString)); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonStringResponse = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(jsonStringResponse); + } + + public async Task DeleteItemAsync(string itemCode) + { + var resourceUri = string.Format(ResourceEndpoints.ITEMS_RESOURCE, itemCode); + var response = await _httpClient.DeleteAsync(resourceUri); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + } + + public async Task DeleteItemsAsync(IList itemCodes) + { + var jsonString = JsonConvert.SerializeObject(itemCodes); + var content = new StringContent(jsonString); + var response = await _httpClient.PostAsync(ResourceEndpoints.ITEMS_DELETE_RESOURCE, content); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + } + + public async Task DownloadItemAsync(string itemCode, string filePath) + { + var resourceUri = string.Format(ResourceEndpoints.ITEMS_DOWNLOAD_RESOURCE, itemCode); + var response = await _httpClient.GetAsync(resourceUri, HttpCompletionOption.ResponseHeadersRead); + + if(response.IsSuccessStatusCode) + { + await using Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); + await using Stream streamToWriteTo = File.Open(filePath, FileMode.Create); + await streamToReadFrom.CopyToAsync(streamToWriteTo); + } + else + { + throw new FileNotFoundException(response.ReasonPhrase); + } + } + + public async Task QueueTaskAsync(string itemCode, TaskRequest taskRequest) + { + var resourceUri = string.Format(ResourceEndpoints.ITEMTASK_REQUEST_RESOURCE, itemCode); + + var jsonString = JsonConvert.SerializeObject(taskRequest); + var response = await _httpClient.PostAsync(resourceUri, new StringContent(jsonString)); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonStringResponse = await response.Content.ReadAsStringAsync(); + var json = JsonConvert.DeserializeObject(jsonStringResponse); + + return json["code"].Value(); + } + + public async Task> GetTasksStatusAsync(string itemCode) + { + var resourceUri = string.Format(ResourceEndpoints.ITEMTASKS_RESOURCE, itemCode); + var response = await _httpClient.GetAsync(resourceUri); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject>(jsonString); + } + + public async Task GetTaskStatusAsync(string itemCode, string itemTaskCode) + { + var resourceUri = string.Format(ResourceEndpoints.ITEMTASK_RESOURCE, itemCode, itemTaskCode); + var response = await _httpClient.GetAsync(resourceUri); + + if (!response.IsSuccessStatusCode) + throw new Exception(response.ReasonPhrase); + + var jsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(jsonString); + } + } +} \ No newline at end of file diff --git a/FarmmapsApi/Services/OpenIdConnectService.cs b/FarmmapsApi/Services/OpenIdConnectService.cs new file mode 100644 index 0000000..d2a0788 --- /dev/null +++ b/FarmmapsApi/Services/OpenIdConnectService.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using FarmmapsApi.Models; +using IdentityModel.Client; + +namespace FarmmapsApi.Services +{ + public class OpenIdConnectService + { + private readonly IDiscoveryCache _discoveryCache; + private readonly Configuration _configuration; + private readonly HttpClient _httpClient; + + public OpenIdConnectService(IDiscoveryCache discoveryCache, + IHttpClientFactory httpFactory, Configuration configuration) + { + _discoveryCache = discoveryCache; + _configuration = configuration; + _httpClient = httpFactory.CreateClient(); + } + + public async Task GetDiscoveryDocumentAsync() + { + var disco = await _discoveryCache.GetAsync(); + if (disco.IsError) + throw new Exception(disco.Error); + + return disco; + } + + public async Task GetTokenClientCredentialsAsync(string tokenEndpointUrl, string clientId, string clientSecret) + { + return await _httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest() + { + Address = tokenEndpointUrl, + ClientId = clientId, + ClientSecret = clientSecret, + Scope = string.Join(" ", _configuration.Scopes) + }); + } + + public async Task RefreshTokensAsync(string tokenEndpointUrl, string refreshToken) + { + return await _httpClient.RequestRefreshTokenAsync(new RefreshTokenRequest() + { + Address = tokenEndpointUrl, + ClientId = _configuration.ClientId, + ClientSecret = _configuration.ClientSecret, + RefreshToken = refreshToken + }); + } + } +} \ No newline at end of file diff --git a/FarmmapsApiSamples.sln b/FarmmapsApiSamples.sln new file mode 100644 index 0000000..60c727a --- /dev/null +++ b/FarmmapsApiSamples.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FarmmapsApiSamples", "FarmmapsApiSamples\FarmmapsApiSamples.csproj", "{E08EF7E9-F09E-42D8-825C-164E458C78F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FarmmapsApi", "FarmmapsApi\FarmmapsApi.csproj", "{1FA9E50B-F45E-4534-953A-37C783D03C74}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E08EF7E9-F09E-42D8-825C-164E458C78F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E08EF7E9-F09E-42D8-825C-164E458C78F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E08EF7E9-F09E-42D8-825C-164E458C78F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E08EF7E9-F09E-42D8-825C-164E458C78F4}.Release|Any CPU.Build.0 = Release|Any CPU + {1FA9E50B-F45E-4534-953A-37C783D03C74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FA9E50B-F45E-4534-953A-37C783D03C74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FA9E50B-F45E-4534-953A-37C783D03C74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FA9E50B-F45E-4534-953A-37C783D03C74}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/FarmmapsApiSamples/DefaultApp.cs b/FarmmapsApiSamples/DefaultApp.cs new file mode 100644 index 0000000..21fa9cb --- /dev/null +++ b/FarmmapsApiSamples/DefaultApp.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using FarmmapsApi.Services; +using Microsoft.Extensions.Logging; + +namespace FarmmapsApiSamples +{ + public class DefaultApp : IApp + { + private readonly ILogger _logger; + private readonly FarmmapsApiService _farmmapsApiService; + + public DefaultApp(ILogger logger, FarmmapsApiService farmmapsApiService) + { + _logger = logger; + _farmmapsApiService = farmmapsApiService; + } + + public async Task RunAsync() + { + try + { + await _farmmapsApiService.AuthenticateAsync(); + + _logger.LogInformation("Authenticated client credentials"); + + var user = await _farmmapsApiService.GetCurrentUserCodeAsync(); + _logger.LogInformation($"Usercode: {user}"); + + var roots = await _farmmapsApiService.GetCurrentUserRootsAsync(); + foreach (var userRoot in roots) + { + _logger.LogInformation($"{userRoot.Name} - {userRoot.Code}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/FarmmapsApiSamples/FarmmapsApiSamples.csproj b/FarmmapsApiSamples/FarmmapsApiSamples.csproj new file mode 100644 index 0000000..4f86bf8 --- /dev/null +++ b/FarmmapsApiSamples/FarmmapsApiSamples.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp3.0 + + + + + Always + + + + + + + + diff --git a/FarmmapsApiSamples/IApp.cs b/FarmmapsApiSamples/IApp.cs new file mode 100644 index 0000000..8390891 --- /dev/null +++ b/FarmmapsApiSamples/IApp.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace FarmmapsApiSamples +{ + public interface IApp + { + Task RunAsync(); + } +} \ No newline at end of file diff --git a/FarmmapsApiSamples/Program.cs b/FarmmapsApiSamples/Program.cs new file mode 100644 index 0000000..23ab66f --- /dev/null +++ b/FarmmapsApiSamples/Program.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using FarmmapsApi; +using FarmmapsApi.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace FarmmapsApiSamples +{ + class Program + { + private static async Task Main(string[] args) + { + IConfiguration config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", false, true) + .Build(); + + var configuration = config.Get(); + + var serviceProvider = new ServiceCollection() + .AddLogging(opts => opts + .AddConsole() + .AddFilter("System.Net.Http", LogLevel.Warning)) + .AddFarmmapsServices(configuration) + .AddSingleton() + .BuildServiceProvider(); + + await serviceProvider.GetService().RunAsync(); + } + } +} \ No newline at end of file diff --git a/FarmmapsApiSamples/appsettings.json b/FarmmapsApiSamples/appsettings.json new file mode 100644 index 0000000..337d0e9 --- /dev/null +++ b/FarmmapsApiSamples/appsettings.json @@ -0,0 +1,9 @@ +{ + "Authority": "https://accounts.farmmaps.awtest.nl/", + "Endpoint": "https://farmmaps.awtest.nl/api/v1/", + "DiscoveryEndpointUrl": "https://accounts.farmmaps.awtest.nl/.well-known/openid-configuration", + "RedirectUri": "http://example.nl/api", + "ClientId": "", + "ClientSecret": "", + "Scopes": ["api"] +} \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..4a98e86 --- /dev/null +++ b/README.MD @@ -0,0 +1 @@ +Put your clientId and clientSecret in the appsettings.json \ No newline at end of file