-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added base CLI project with configuration and login.
- Loading branch information
1 parent
9735e2a
commit 4795aec
Showing
11 changed files
with
383 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -485,4 +485,6 @@ $RECYCLE.BIN/ | |
|
||
appsettings.*.json | ||
sharedsettings.*.json | ||
app.db | ||
app.db | ||
|
||
HQ.CLI/data/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HQCommandSettings> | ||
{ | ||
private readonly HQConfig _config; | ||
|
||
public ConfigureCommand(HQConfig config) | ||
{ | ||
_config = config; | ||
} | ||
|
||
public override async Task<int> ExecuteAsync(CommandContext context, HQCommandSettings settings) | ||
{ | ||
_config.ApiUrl = AnsiConsole.Prompt( | ||
new TextPrompt<Uri>("Enter API URL:") | ||
.ValidationErrorMessage("[red]That's not a valid API URL[/]") | ||
.Validate(uri => uri.IsAbsoluteUri)); | ||
|
||
_config.AuthUrl = AnsiConsole.Prompt(new TextPrompt<Uri>("Enter Auth URL:") | ||
.ValidationErrorMessage("[red]That's not a valid Auth URL[/]") | ||
.Validate(uri => uri.IsAbsoluteUri)); | ||
|
||
return 0; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<HQCommandSettings> | ||
{ | ||
private readonly ILogger<LoginCommand> _logger; | ||
private readonly HQConfig _config; | ||
private readonly IDataProtectionProvider _dataProtectionProvider; | ||
|
||
public LoginCommand(ILogger<LoginCommand> logger, HQConfig config, IDataProtectionProvider dataProtectionProvider) | ||
{ | ||
_logger = logger; | ||
_config = config; | ||
_dataProtectionProvider = dataProtectionProvider; | ||
} | ||
|
||
public override async Task<int> 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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
|
||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="IdentityModel" Version="6.2.0" /> | ||
<PackageReference Include="IdentityModel.AspNetCore" Version="4.3.0" /> | ||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="8.0.3" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" /> | ||
<PackageReference Include="Spectre.Console.Cli" Version="0.48.1-preview.0.38" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<None Update="Properties\launchSettings.json"> | ||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile> | ||
<CopyToPublishDirectory>Never</CopyToPublishDirectory> | ||
</None> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Folder Include="data\keys\" /> | ||
<Folder Include="Settings\" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
{ | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
{ | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<LogLevel>(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<HQConfig>(File.ReadAllText(configJsonPath)) ?? new() : new(); | ||
services.AddSingleton(config); | ||
|
||
services.AddSingleton<ICommandInterceptor, HQCommandInterceptor>(); | ||
|
||
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<ConfigureCommand>("configure") | ||
.WithDescription("Configure HQ CLI"); | ||
|
||
config.AddCommand<LoginCommand>("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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<object> func) | ||
{ | ||
if (func is null) | ||
{ | ||
throw new ArgumentNullException(nameof(func)); | ||
} | ||
|
||
_builder.AddSingleton(service, (provider) => func()); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.