From 4795aec15c7da9687071e1adcbaa649d7a6bde3d Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Mon, 8 Apr 2024 22:59:13 -0400 Subject: [PATCH 1/4] Added base CLI project with configuration and login. --- src/dotnet/.gitignore | 4 +- .../HQ.CLI/Commands/ConfigureCommand.cs | 34 ++++++ src/dotnet/HQ.CLI/Commands/LoginCommand.cs | 109 ++++++++++++++++++ src/dotnet/HQ.CLI/HQ.CLI.csproj | 32 +++++ src/dotnet/HQ.CLI/HQCommandInterceptor.cs | 18 +++ src/dotnet/HQ.CLI/HQCommandSettings.cs | 13 +++ src/dotnet/HQ.CLI/HQConfig.cs | 18 +++ src/dotnet/HQ.CLI/Program.cs | 67 +++++++++++ src/dotnet/HQ.CLI/TypeRegistrar.cs | 45 ++++++++ src/dotnet/HQ.CLI/TypeResolver.cs | 38 ++++++ src/dotnet/HQ.sln | 6 + 11 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 src/dotnet/HQ.CLI/Commands/ConfigureCommand.cs create mode 100644 src/dotnet/HQ.CLI/Commands/LoginCommand.cs create mode 100644 src/dotnet/HQ.CLI/HQ.CLI.csproj create mode 100644 src/dotnet/HQ.CLI/HQCommandInterceptor.cs create mode 100644 src/dotnet/HQ.CLI/HQCommandSettings.cs create mode 100644 src/dotnet/HQ.CLI/HQConfig.cs create mode 100644 src/dotnet/HQ.CLI/Program.cs create mode 100644 src/dotnet/HQ.CLI/TypeRegistrar.cs create mode 100644 src/dotnet/HQ.CLI/TypeResolver.cs diff --git a/src/dotnet/.gitignore b/src/dotnet/.gitignore index 0c341a13..a4f61b5d 100644 --- a/src/dotnet/.gitignore +++ b/src/dotnet/.gitignore @@ -485,4 +485,6 @@ $RECYCLE.BIN/ appsettings.*.json sharedsettings.*.json -app.db \ No newline at end of file +app.db + +HQ.CLI/data/ \ No newline at end of file diff --git a/src/dotnet/HQ.CLI/Commands/ConfigureCommand.cs b/src/dotnet/HQ.CLI/Commands/ConfigureCommand.cs new file mode 100644 index 00000000..8ba2c172 --- /dev/null +++ b/src/dotnet/HQ.CLI/Commands/ConfigureCommand.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Spectre.Console.Cli; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HQ.CLI.Commands; + +internal class ConfigureCommand : AsyncCommand +{ + private readonly HQConfig _config; + + public ConfigureCommand(HQConfig config) + { + _config = config; + } + + public override async Task ExecuteAsync(CommandContext context, HQCommandSettings settings) + { + _config.ApiUrl = AnsiConsole.Prompt( + new TextPrompt("Enter API URL:") + .ValidationErrorMessage("[red]That's not a valid API URL[/]") + .Validate(uri => uri.IsAbsoluteUri)); + + _config.AuthUrl = AnsiConsole.Prompt(new TextPrompt("Enter Auth URL:") + .ValidationErrorMessage("[red]That's not a valid Auth URL[/]") + .Validate(uri => uri.IsAbsoluteUri)); + + return 0; + } +} diff --git a/src/dotnet/HQ.CLI/Commands/LoginCommand.cs b/src/dotnet/HQ.CLI/Commands/LoginCommand.cs new file mode 100644 index 00000000..4fc2f176 --- /dev/null +++ b/src/dotnet/HQ.CLI/Commands/LoginCommand.cs @@ -0,0 +1,109 @@ +using IdentityModel.Client; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Spectre.Console.Cli; +using System.Diagnostics; +using static IdentityModel.OidcConstants; + +namespace HQ.CLI.Commands; + +internal class LoginCommand : AsyncCommand +{ + private readonly ILogger _logger; + private readonly HQConfig _config; + private readonly IDataProtectionProvider _dataProtectionProvider; + + public LoginCommand(ILogger logger, HQConfig config, IDataProtectionProvider dataProtectionProvider) + { + _logger = logger; + _config = config; + _dataProtectionProvider = dataProtectionProvider; + } + + public override async Task ExecuteAsync(CommandContext context, HQCommandSettings settings) + { + if(String.IsNullOrEmpty(_config.AuthUrl?.AbsoluteUri)) + { + AnsiConsole.MarkupLine("[red]Invalid Auth URL[/]"); + return 1; + } + + var client = new HttpClient(); + + var disco = await client.GetDiscoveryDocumentAsync(_config.AuthUrl.AbsoluteUri); + if (disco.IsError) throw new Exception(disco.Error); + + var authorizeResponse = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest + { + Address = disco.DeviceAuthorizationEndpoint, + ClientId = "hq", + Scope = "openid profile email offline_access" + }); + if (authorizeResponse.IsError) throw new Exception(authorizeResponse.Error); + + AnsiConsole.MarkupLineInterpolated($@"Attempting to automatically open the login page in your default browser. +If the browser does not open or you wish to use a different device to authorize this request, open the following URL: + +[link]{authorizeResponse.VerificationUri}[/] + +Then enter the code: + +{authorizeResponse.UserCode}"); + + try + { + Process launchBrowserProcess = new Process(); + launchBrowserProcess.StartInfo.UseShellExecute = true; + launchBrowserProcess.StartInfo.FileName = authorizeResponse.VerificationUriComplete; + launchBrowserProcess.Start(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Unable to launch browser."); + } + + var interval = TimeSpan.FromSeconds(authorizeResponse.Interval); + var expiresIn = authorizeResponse.ExpiresIn; + DateTime? expiresAt = null; + if (expiresIn.HasValue) + { + expiresAt = DateTime.UtcNow.AddSeconds(expiresIn.Value); + } + + IdentityModel.Client.TokenResponse? response = null; + do + { + response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest + { + Address = disco.TokenEndpoint, + + ClientId = "hq", + DeviceCode = authorizeResponse.DeviceCode + }); + + // Console.WriteLine(response.Raw); + + if (response.Error == "slow_down") + { + // Console.WriteLine("Got a slow down!"); + interval += TimeSpan.FromSeconds(5); + } + + // Console.WriteLine("Waiting for response..."); + await Task.Delay(interval); + } + while (response.Error == "authorization_pending" || response.Error == "slow_down"); + if (response.IsError) throw new Exception(response.Error); + + var protector = _dataProtectionProvider.CreateProtector("HQ.CLI"); + + _config.RefreshToken = !String.IsNullOrEmpty(response.RefreshToken) ? protector.Protect(response.RefreshToken) : null; + _config.AccessToken = !String.IsNullOrEmpty(response.AccessToken) ? protector.Protect(response.AccessToken) : null; ; + _config.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(response.ExpiresIn); + + AnsiConsole.MarkupLine("[green]Authentication successful![/] "); + + return 0; + } +} diff --git a/src/dotnet/HQ.CLI/HQ.CLI.csproj b/src/dotnet/HQ.CLI/HQ.CLI.csproj new file mode 100644 index 00000000..17fe3d27 --- /dev/null +++ b/src/dotnet/HQ.CLI/HQ.CLI.csproj @@ -0,0 +1,32 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + true + Never + + + + + + + + + diff --git a/src/dotnet/HQ.CLI/HQCommandInterceptor.cs b/src/dotnet/HQ.CLI/HQCommandInterceptor.cs new file mode 100644 index 00000000..9e274c8b --- /dev/null +++ b/src/dotnet/HQ.CLI/HQCommandInterceptor.cs @@ -0,0 +1,18 @@ +using Spectre.Console.Cli; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HQ.CLI; + +internal class HQCommandInterceptor : ICommandInterceptor +{ + public void Intercept(CommandContext context, CommandSettings settings) + { + if (settings is HQCommandSettings hqSettings) + { + } + } +} diff --git a/src/dotnet/HQ.CLI/HQCommandSettings.cs b/src/dotnet/HQ.CLI/HQCommandSettings.cs new file mode 100644 index 00000000..ceb385ee --- /dev/null +++ b/src/dotnet/HQ.CLI/HQCommandSettings.cs @@ -0,0 +1,13 @@ +using Spectre.Console.Cli; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HQ.CLI +{ + public class HQCommandSettings : CommandSettings + { + } +} diff --git a/src/dotnet/HQ.CLI/HQConfig.cs b/src/dotnet/HQ.CLI/HQConfig.cs new file mode 100644 index 00000000..f7aeceb0 --- /dev/null +++ b/src/dotnet/HQ.CLI/HQConfig.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 HQ.CLI +{ + internal class HQConfig + { + public Uri? ApiUrl { get; set; } + public Uri? AuthUrl { get; set; } + public string? RefreshToken { get; set; } + public string? AccessToken { get; set; } + public DateTime? AccessTokenExpiresAt { get; set; } + } +} diff --git a/src/dotnet/HQ.CLI/Program.cs b/src/dotnet/HQ.CLI/Program.cs new file mode 100644 index 00000000..a93cf544 --- /dev/null +++ b/src/dotnet/HQ.CLI/Program.cs @@ -0,0 +1,67 @@ +using HQ.CLI; +using HQ.CLI.Commands; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Spectre.Console.Cli; +using System.Text.Json; + +// Setup data directory +var userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify); +var dataPath = Environment.GetEnvironmentVariable("HQ_DATA_PATH") ?? Path.Join(userProfilePath, ".sancsoft", "hq"); +Directory.CreateDirectory(dataPath); + +// Setup services +var services = new ServiceCollection(); + +// Setup logging +var logLevel = LogLevel.Information; +if(Enum.TryParse(Environment.GetEnvironmentVariable("HQ_LOG_LEVEL"), true, out LogLevel envLogLevel)) +{ + logLevel = envLogLevel; +} + +services.AddLogging(c => c.AddConsole().SetMinimumLevel(logLevel)); + +// Setup configuration +var configJsonPath = Path.Join(dataPath, "config.json"); +var config = File.Exists(configJsonPath) ? JsonSerializer.Deserialize(File.ReadAllText(configJsonPath)) ?? new() : new(); +services.AddSingleton(config); + +services.AddSingleton(); + +var dataProtectionBuilder = services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(Path.Join(dataPath, "keys"))) + .SetApplicationName("HQ.CLI"); + +if (OperatingSystem.IsWindows()) +{ + dataProtectionBuilder.ProtectKeysWithDpapi(); +} + +var registrar = new TypeRegistrar(services); +var app = new CommandApp(registrar); +app.Configure(config => +{ + config.AddCommand("configure") + .WithDescription("Configure HQ CLI"); + + config.AddCommand("login") + .WithDescription("Login to HQ CLI"); +}); + +var rc = await app.RunAsync(args); + +if(rc == 0) +{ + var options = new JsonSerializerOptions() + { + WriteIndented = true + }; + + await File.WriteAllTextAsync(configJsonPath, JsonSerializer.Serialize(config, options)); +} + +return rc; \ No newline at end of file diff --git a/src/dotnet/HQ.CLI/TypeRegistrar.cs b/src/dotnet/HQ.CLI/TypeRegistrar.cs new file mode 100644 index 00000000..7c59b2b6 --- /dev/null +++ b/src/dotnet/HQ.CLI/TypeRegistrar.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HQ.CLI +{ + public sealed class TypeRegistrar : ITypeRegistrar + { + private readonly IServiceCollection _builder; + + public TypeRegistrar(IServiceCollection builder) + { + _builder = builder; + } + + public ITypeResolver Build() + { + return new TypeResolver(_builder.BuildServiceProvider()); + } + + public void Register(Type service, Type implementation) + { + _builder.AddSingleton(service, implementation); + } + + public void RegisterInstance(Type service, object implementation) + { + _builder.AddSingleton(service, implementation); + } + + public void RegisterLazy(Type service, Func func) + { + if (func is null) + { + throw new ArgumentNullException(nameof(func)); + } + + _builder.AddSingleton(service, (provider) => func()); + } + } +} diff --git a/src/dotnet/HQ.CLI/TypeResolver.cs b/src/dotnet/HQ.CLI/TypeResolver.cs new file mode 100644 index 00000000..cae83e19 --- /dev/null +++ b/src/dotnet/HQ.CLI/TypeResolver.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HQ.CLI +{ + public sealed class TypeResolver : ITypeResolver, IDisposable + { + private readonly IServiceProvider _provider; + + public TypeResolver(IServiceProvider provider) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + public object? Resolve(Type? type) + { + if (type == null) + { + return null; + } + + return _provider.GetService(type); + } + + public void Dispose() + { + if (_provider is IDisposable disposable) + { + disposable.Dispose(); + } + } + } +} diff --git a/src/dotnet/HQ.sln b/src/dotnet/HQ.sln index d1e8feeb..ee2a2f4b 100644 --- a/src/dotnet/HQ.sln +++ b/src/dotnet/HQ.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HQ.Server", "HQ.Server\HQ.Server.csproj", "{BAFE81CE-8BE4-405E-AF91-827AB2A7F968}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HQ.CLI", "HQ.CLI\HQ.CLI.csproj", "{F3807435-B048-494E-8EF5-B8E71060FBFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {BAFE81CE-8BE4-405E-AF91-827AB2A7F968}.Debug|Any CPU.Build.0 = Debug|Any CPU {BAFE81CE-8BE4-405E-AF91-827AB2A7F968}.Release|Any CPU.ActiveCfg = Release|Any CPU {BAFE81CE-8BE4-405E-AF91-827AB2A7F968}.Release|Any CPU.Build.0 = Release|Any CPU + {F3807435-B048-494E-8EF5-B8E71060FBFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3807435-B048-494E-8EF5-B8E71060FBFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3807435-B048-494E-8EF5-B8E71060FBFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3807435-B048-494E-8EF5-B8E71060FBFB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 94eee8d5cac6e334f1736043c5b05a7e72164d02 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Mon, 8 Apr 2024 23:59:28 -0400 Subject: [PATCH 2/4] Added SDK project, typed HTTP client, message handler for handling token refresh. --- src/dotnet/HQ.CLI/Commands/LoginCommand.cs | 12 ++-- src/dotnet/HQ.CLI/Commands/TestApiCommand.cs | 28 ++++++++ src/dotnet/HQ.CLI/HQ.CLI.csproj | 6 +- src/dotnet/HQ.CLI/HQApiMessageHandler.cs | 76 ++++++++++++++++++++ src/dotnet/HQ.CLI/Program.cs | 11 +++ src/dotnet/HQ.SDK/HQ.SDK.csproj | 9 +++ src/dotnet/HQ.SDK/TestApiService.cs | 23 ++++++ src/dotnet/HQ.sln | 6 ++ 8 files changed, 164 insertions(+), 7 deletions(-) create mode 100644 src/dotnet/HQ.CLI/Commands/TestApiCommand.cs create mode 100644 src/dotnet/HQ.CLI/HQApiMessageHandler.cs create mode 100644 src/dotnet/HQ.SDK/HQ.SDK.csproj create mode 100644 src/dotnet/HQ.SDK/TestApiService.cs diff --git a/src/dotnet/HQ.CLI/Commands/LoginCommand.cs b/src/dotnet/HQ.CLI/Commands/LoginCommand.cs index 4fc2f176..f02e91f0 100644 --- a/src/dotnet/HQ.CLI/Commands/LoginCommand.cs +++ b/src/dotnet/HQ.CLI/Commands/LoginCommand.cs @@ -13,12 +13,14 @@ internal class LoginCommand : AsyncCommand private readonly ILogger _logger; private readonly HQConfig _config; private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly HttpClient _httpClient; - public LoginCommand(ILogger logger, HQConfig config, IDataProtectionProvider dataProtectionProvider) + public LoginCommand(ILogger logger, HQConfig config, IDataProtectionProvider dataProtectionProvider, HttpClient httpClient) { _logger = logger; _config = config; _dataProtectionProvider = dataProtectionProvider; + _httpClient = httpClient; } public override async Task ExecuteAsync(CommandContext context, HQCommandSettings settings) @@ -29,12 +31,10 @@ public override async Task ExecuteAsync(CommandContext context, HQCommandSe return 1; } - var client = new HttpClient(); - - var disco = await client.GetDiscoveryDocumentAsync(_config.AuthUrl.AbsoluteUri); + var disco = await _httpClient.GetDiscoveryDocumentAsync(_config.AuthUrl.AbsoluteUri); if (disco.IsError) throw new Exception(disco.Error); - var authorizeResponse = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest + var authorizeResponse = await _httpClient.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest { Address = disco.DeviceAuthorizationEndpoint, ClientId = "hq", @@ -74,7 +74,7 @@ public override async Task ExecuteAsync(CommandContext context, HQCommandSe IdentityModel.Client.TokenResponse? response = null; do { - response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest + response = await _httpClient.RequestDeviceTokenAsync(new DeviceTokenRequest { Address = disco.TokenEndpoint, diff --git a/src/dotnet/HQ.CLI/Commands/TestApiCommand.cs b/src/dotnet/HQ.CLI/Commands/TestApiCommand.cs new file mode 100644 index 00000000..da62d49e --- /dev/null +++ b/src/dotnet/HQ.CLI/Commands/TestApiCommand.cs @@ -0,0 +1,28 @@ +using HQ.SDK; +using Spectre.Console.Cli; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HQ.CLI.Commands +{ + internal class TestApiCommand : AsyncCommand + { + private readonly TestApiService _testApiService; + + public TestApiCommand(TestApiService testApiService) + { + _testApiService = testApiService; + } + + public override async Task ExecuteAsync(CommandContext context, HQCommandSettings settings) + { + var response = await _testApiService.GetWeatherForecastAsync(); + Console.WriteLine(response); + + return 0; + } + } +} diff --git a/src/dotnet/HQ.CLI/HQ.CLI.csproj b/src/dotnet/HQ.CLI/HQ.CLI.csproj index 17fe3d27..184f723e 100644 --- a/src/dotnet/HQ.CLI/HQ.CLI.csproj +++ b/src/dotnet/HQ.CLI/HQ.CLI.csproj @@ -10,8 +10,8 @@ - + @@ -29,4 +29,8 @@ + + + + diff --git a/src/dotnet/HQ.CLI/HQApiMessageHandler.cs b/src/dotnet/HQ.CLI/HQApiMessageHandler.cs new file mode 100644 index 00000000..6b957490 --- /dev/null +++ b/src/dotnet/HQ.CLI/HQApiMessageHandler.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Net.Http; +using Microsoft.AspNetCore.DataProtection; +using IdentityModel.Client; +using static IdentityModel.OidcConstants; + +namespace HQ.CLI +{ + internal class HQApiMessageHandler : DelegatingHandler + { + private readonly HQConfig _config; + private readonly HttpClient _httpClient; + private readonly IDataProtector _dataProtector; + + public HQApiMessageHandler(HQConfig config, HttpClient httpClient, IDataProtectionProvider dataProtectionProvider) + { + _config = config; + _httpClient = httpClient; + _dataProtector = dataProtectionProvider.CreateProtector("HQ.CLI"); + } + + private async Task GetTokenAsync(CancellationToken cancellationToken, bool forceRefresh = false) + { + var accessToken = _dataProtector.Unprotect(_config.AccessToken); + var refreshToken = _dataProtector.Unprotect(_config.RefreshToken); + + if ((_config.AccessTokenExpiresAt.HasValue && _config.AccessTokenExpiresAt.Value <= DateTime.UtcNow) || forceRefresh) + { + var disco = await _httpClient.GetDiscoveryDocumentAsync(_config.AuthUrl.AbsoluteUri); + if (disco.IsError) throw new Exception(disco.Error); + + var response = await _httpClient.RequestRefreshTokenAsync(new RefreshTokenRequest + { + Address = disco.TokenEndpoint, + + ClientId = "hq", + RefreshToken = refreshToken + }); + + if (response.IsError) throw new Exception(response.Error); + + accessToken = response.AccessToken; + + _config.RefreshToken = !String.IsNullOrEmpty(response.RefreshToken) ? _dataProtector.Protect(response.RefreshToken) : null; + _config.AccessToken = !String.IsNullOrEmpty(response.AccessToken) ? _dataProtector.Protect(response.AccessToken) : null; ; + _config.AccessTokenExpiresAt = DateTime.UtcNow.AddSeconds(response.ExpiresIn); + } + + return accessToken; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var token = await GetTokenAsync(cancellationToken); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + + var response = await base.SendAsync(request, cancellationToken); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + token = await GetTokenAsync(cancellationToken, true); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + response.Dispose(); + + response = await base.SendAsync(request, cancellationToken); + } + + return response; + } + } +} diff --git a/src/dotnet/HQ.CLI/Program.cs b/src/dotnet/HQ.CLI/Program.cs index a93cf544..220eaaf8 100644 --- a/src/dotnet/HQ.CLI/Program.cs +++ b/src/dotnet/HQ.CLI/Program.cs @@ -1,5 +1,6 @@ using HQ.CLI; using HQ.CLI.Commands; +using HQ.SDK; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -41,6 +42,13 @@ dataProtectionBuilder.ProtectKeysWithDpapi(); } +services.AddSingleton(); + +services.AddHttpClient(client => +{ + client.BaseAddress = config.ApiUrl; +}).AddHttpMessageHandler(); + var registrar = new TypeRegistrar(services); var app = new CommandApp(registrar); app.Configure(config => @@ -50,6 +58,9 @@ config.AddCommand("login") .WithDescription("Login to HQ CLI"); + + config.AddCommand("test-api") + .WithDescription("Test API"); }); var rc = await app.RunAsync(args); diff --git a/src/dotnet/HQ.SDK/HQ.SDK.csproj b/src/dotnet/HQ.SDK/HQ.SDK.csproj new file mode 100644 index 00000000..fa71b7ae --- /dev/null +++ b/src/dotnet/HQ.SDK/HQ.SDK.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/dotnet/HQ.SDK/TestApiService.cs b/src/dotnet/HQ.SDK/TestApiService.cs new file mode 100644 index 00000000..d8749b0d --- /dev/null +++ b/src/dotnet/HQ.SDK/TestApiService.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace HQ.SDK +{ + public class TestApiService + { + private readonly HttpClient _httpClient; + + public TestApiService(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task GetWeatherForecastAsync() + { + return await _httpClient.GetStringAsync("/v1/weather-forecast"); + } + } +} diff --git a/src/dotnet/HQ.sln b/src/dotnet/HQ.sln index ee2a2f4b..c44e4278 100644 --- a/src/dotnet/HQ.sln +++ b/src/dotnet/HQ.sln @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HQ.Server", "HQ.Server\HQ.S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HQ.CLI", "HQ.CLI\HQ.CLI.csproj", "{F3807435-B048-494E-8EF5-B8E71060FBFB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HQ.SDK", "HQ.SDK\HQ.SDK.csproj", "{8BE654C4-9AAC-499D-BDAC-33A99A1CC089}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {F3807435-B048-494E-8EF5-B8E71060FBFB}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3807435-B048-494E-8EF5-B8E71060FBFB}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3807435-B048-494E-8EF5-B8E71060FBFB}.Release|Any CPU.Build.0 = Release|Any CPU + {8BE654C4-9AAC-499D-BDAC-33A99A1CC089}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BE654C4-9AAC-499D-BDAC-33A99A1CC089}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BE654C4-9AAC-499D-BDAC-33A99A1CC089}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BE654C4-9AAC-499D-BDAC-33A99A1CC089}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 6c332f092b70cf78448be8f06c3f8860ab7097be Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Tue, 9 Apr 2024 00:10:06 -0400 Subject: [PATCH 3/4] Added HQ.Abstractions project for common models. --- src/dotnet/HQ.Abstractions/HQ.Abstractions.csproj | 9 +++++++++ .../{HQ.Server => HQ.Abstractions}/WeatherForecast.cs | 2 +- src/dotnet/HQ.SDK/HQ.SDK.csproj | 4 ++++ src/dotnet/HQ.SDK/TestApiService.cs | 8 +++++--- .../HQ.Server/Controllers/WeatherForecastController.cs | 1 + src/dotnet/HQ.Server/HQ.Server.csproj | 4 ++++ src/dotnet/HQ.sln | 6 ++++++ 7 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/dotnet/HQ.Abstractions/HQ.Abstractions.csproj rename src/dotnet/{HQ.Server => HQ.Abstractions}/WeatherForecast.cs (89%) diff --git a/src/dotnet/HQ.Abstractions/HQ.Abstractions.csproj b/src/dotnet/HQ.Abstractions/HQ.Abstractions.csproj new file mode 100644 index 00000000..fa71b7ae --- /dev/null +++ b/src/dotnet/HQ.Abstractions/HQ.Abstractions.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/src/dotnet/HQ.Server/WeatherForecast.cs b/src/dotnet/HQ.Abstractions/WeatherForecast.cs similarity index 89% rename from src/dotnet/HQ.Server/WeatherForecast.cs rename to src/dotnet/HQ.Abstractions/WeatherForecast.cs index 57a011ae..8068d484 100644 --- a/src/dotnet/HQ.Server/WeatherForecast.cs +++ b/src/dotnet/HQ.Abstractions/WeatherForecast.cs @@ -1,4 +1,4 @@ -namespace HQ.Server; +namespace HQ.Abstractions; public class WeatherForecast { diff --git a/src/dotnet/HQ.SDK/HQ.SDK.csproj b/src/dotnet/HQ.SDK/HQ.SDK.csproj index fa71b7ae..cf083355 100644 --- a/src/dotnet/HQ.SDK/HQ.SDK.csproj +++ b/src/dotnet/HQ.SDK/HQ.SDK.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/src/dotnet/HQ.SDK/TestApiService.cs b/src/dotnet/HQ.SDK/TestApiService.cs index d8749b0d..476b864b 100644 --- a/src/dotnet/HQ.SDK/TestApiService.cs +++ b/src/dotnet/HQ.SDK/TestApiService.cs @@ -1,6 +1,8 @@ -using System; +using HQ.Abstractions; +using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http.Json; using System.Text; using System.Threading.Tasks; @@ -15,9 +17,9 @@ public TestApiService(HttpClient httpClient) _httpClient = httpClient; } - public async Task GetWeatherForecastAsync() + public async Task?> GetWeatherForecastAsync() { - return await _httpClient.GetStringAsync("/v1/weather-forecast"); + return await _httpClient.GetFromJsonAsync>("/v1/weather-forecast"); } } } diff --git a/src/dotnet/HQ.Server/Controllers/WeatherForecastController.cs b/src/dotnet/HQ.Server/Controllers/WeatherForecastController.cs index 42998205..b8f6f54c 100644 --- a/src/dotnet/HQ.Server/Controllers/WeatherForecastController.cs +++ b/src/dotnet/HQ.Server/Controllers/WeatherForecastController.cs @@ -1,4 +1,5 @@ using Asp.Versioning; +using HQ.Abstractions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/dotnet/HQ.Server/HQ.Server.csproj b/src/dotnet/HQ.Server/HQ.Server.csproj index a5384446..a6ad1b9c 100644 --- a/src/dotnet/HQ.Server/HQ.Server.csproj +++ b/src/dotnet/HQ.Server/HQ.Server.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/src/dotnet/HQ.sln b/src/dotnet/HQ.sln index c44e4278..89a08b12 100644 --- a/src/dotnet/HQ.sln +++ b/src/dotnet/HQ.sln @@ -16,6 +16,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HQ.CLI", "HQ.CLI\HQ.CLI.csp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HQ.SDK", "HQ.SDK\HQ.SDK.csproj", "{8BE654C4-9AAC-499D-BDAC-33A99A1CC089}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HQ.Abstractions", "HQ.Abstractions\HQ.Abstractions.csproj", "{D0253812-BD5E-4764-8813-D73B083C72DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {8BE654C4-9AAC-499D-BDAC-33A99A1CC089}.Debug|Any CPU.Build.0 = Debug|Any CPU {8BE654C4-9AAC-499D-BDAC-33A99A1CC089}.Release|Any CPU.ActiveCfg = Release|Any CPU {8BE654C4-9AAC-499D-BDAC-33A99A1CC089}.Release|Any CPU.Build.0 = Release|Any CPU + {D0253812-BD5E-4764-8813-D73B083C72DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0253812-BD5E-4764-8813-D73B083C72DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0253812-BD5E-4764-8813-D73B083C72DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0253812-BD5E-4764-8813-D73B083C72DE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From e59ccd993178b4f0c687d8ea5279341d05abb7b4 Mon Sep 17 00:00:00 2001 From: Ryan Maffit Date: Tue, 9 Apr 2024 00:17:47 -0400 Subject: [PATCH 4/4] Configured CLI assembly name, publish as self-contained single file executable. --- src/dotnet/HQ.CLI/HQ.CLI.csproj | 4 +++- src/dotnet/HQ.CLI/Program.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dotnet/HQ.CLI/HQ.CLI.csproj b/src/dotnet/HQ.CLI/HQ.CLI.csproj index 184f723e..bf171da4 100644 --- a/src/dotnet/HQ.CLI/HQ.CLI.csproj +++ b/src/dotnet/HQ.CLI/HQ.CLI.csproj @@ -5,7 +5,9 @@ net8.0 enable enable - + true + true + hq diff --git a/src/dotnet/HQ.CLI/Program.cs b/src/dotnet/HQ.CLI/Program.cs index 220eaaf8..7ed15d5b 100644 --- a/src/dotnet/HQ.CLI/Program.cs +++ b/src/dotnet/HQ.CLI/Program.cs @@ -18,7 +18,7 @@ var services = new ServiceCollection(); // Setup logging -var logLevel = LogLevel.Information; +var logLevel = LogLevel.None; if(Enum.TryParse(Environment.GetEnvironmentVariable("HQ_LOG_LEVEL"), true, out LogLevel envLogLevel)) { logLevel = envLogLevel;