From 35557a8aa7c6b91ddf0c4f9cbd1c37bce442ddeb Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Mon, 2 Feb 2026 15:41:47 -0600
Subject: [PATCH] feat(admincli): add command for creating a user
---
src/Directory.Packages.props | 1 +
src/FiscalOS.AdminCLI/.editorconfig | 1 +
src/FiscalOS.AdminCLI/App.cs | 52 +++++++++++++++++++
src/FiscalOS.AdminCLI/Commands.cs | 9 ++++
.../FiscalOS.AdminCLI.csproj | 16 ++++++
src/FiscalOS.AdminCLI/Program.cs | 26 ++++++----
src/FiscalOS.AdminCLI/Usings.cs | 12 +++++
.../appsettings.Example.json | 5 ++
src/FiscalOS.Infra/Data/AppDbContext.cs | 4 +-
.../Data/AppDbContextOptions.cs | 1 +
.../ServiceCollectionExtensions.cs | 1 +
11 files changed, 114 insertions(+), 14 deletions(-)
create mode 100644 src/FiscalOS.AdminCLI/App.cs
create mode 100644 src/FiscalOS.AdminCLI/Commands.cs
create mode 100644 src/FiscalOS.AdminCLI/Usings.cs
create mode 100644 src/FiscalOS.AdminCLI/appsettings.Example.json
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 92a593a..6a86a48 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -11,5 +11,6 @@
+
\ No newline at end of file
diff --git a/src/FiscalOS.AdminCLI/.editorconfig b/src/FiscalOS.AdminCLI/.editorconfig
index e04d110..e0edca3 100644
--- a/src/FiscalOS.AdminCLI/.editorconfig
+++ b/src/FiscalOS.AdminCLI/.editorconfig
@@ -1,3 +1,4 @@
[*.cs]
dotnet_diagnostic.CA1303.severity = none
+dotnet_diagnostic.CA2007.severity = none
diff --git a/src/FiscalOS.AdminCLI/App.cs b/src/FiscalOS.AdminCLI/App.cs
new file mode 100644
index 0000000..4ebd969
--- /dev/null
+++ b/src/FiscalOS.AdminCLI/App.cs
@@ -0,0 +1,52 @@
+namespace FiscalOS.AdminCLI;
+
+internal sealed class App(
+ IAnsiConsole console,
+ IPasswordHasher passwordHasher,
+ IServiceScopeFactory serviceScopeFactory
+) : IHostedService
+{
+ private readonly IAnsiConsole _console = console;
+ private readonly IServiceScopeFactory _serviceScopeFactory = serviceScopeFactory;
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ using var scope = _serviceScopeFactory.CreateScope();
+ var config = scope.ServiceProvider.GetRequiredService();
+ var appDbContext = scope.ServiceProvider.GetRequiredService();
+
+ var selectionPrompt = new SelectionPrompt()
+ .Title("What do you want to do?")
+ .AddChoices(Commands.All);
+
+ var command = await _console.PromptAsync(selectionPrompt, cancellationToken);
+
+ switch (command)
+ {
+ case Commands.CreateUser:
+ var username = await _console.AskAsync("Enter the [green]username[/]:", cancellationToken);
+ var password = _console.Prompt(
+ new TextPrompt("Enter the [green]password[/]:")
+ .PromptStyle("red")
+ .Secret()
+ );
+
+ var hashedPassword = passwordHasher.Hash(password);
+ var user = User.From(username, hashedPassword);
+
+ await appDbContext.Users.AddAsync(user, cancellationToken);
+ await appDbContext.SaveChangesAsync(cancellationToken);
+
+ _console.MarkupLine($"User [green]{username}[/] created successfully.");
+ break;
+ case Commands.Exit:
+ default:
+ break;
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/FiscalOS.AdminCLI/Commands.cs b/src/FiscalOS.AdminCLI/Commands.cs
new file mode 100644
index 0000000..031eb96
--- /dev/null
+++ b/src/FiscalOS.AdminCLI/Commands.cs
@@ -0,0 +1,9 @@
+namespace FiscalOS.AdminCLI;
+
+internal static class Commands
+{
+ public const string CreateUser = "Create User";
+ public const string Exit = "Exit";
+
+ public static readonly string[] All = [CreateUser, Exit];
+}
\ No newline at end of file
diff --git a/src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj b/src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj
index 6a70a1a..bdc14e5 100644
--- a/src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj
+++ b/src/FiscalOS.AdminCLI/FiscalOS.AdminCLI.csproj
@@ -4,4 +4,20 @@
Exe
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/src/FiscalOS.AdminCLI/Program.cs b/src/FiscalOS.AdminCLI/Program.cs
index b5ed5d6..4f575c1 100644
--- a/src/FiscalOS.AdminCLI/Program.cs
+++ b/src/FiscalOS.AdminCLI/Program.cs
@@ -1,11 +1,15 @@
-// TODO: We need a way to add
-// a user to the system with
-// a properly hashed password
-// so need something like this:
-//
-// 1. Ask for username
-// 2. Ask for user password
-// 3. Hash user password
-// 4. Save user to database
-
-Console.WriteLine("Hello, World!");
\ No newline at end of file
+await Host.CreateDefaultBuilder(args)
+ .ConfigureAppConfiguration(
+ static c => c.SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json")
+ .AddEnvironmentVariables()
+ )
+ .ConfigureLogging(static c => c.ClearProviders())
+ .ConfigureServices(static (_, services) =>
+ {
+ services.AddSingleton(AnsiConsole.Console);
+ services.AddInfrastructure();
+ services.AddHostedService();
+ })
+ .Build()
+ .StartAsync();
diff --git a/src/FiscalOS.AdminCLI/Usings.cs b/src/FiscalOS.AdminCLI/Usings.cs
new file mode 100644
index 0000000..4b1a40b
--- /dev/null
+++ b/src/FiscalOS.AdminCLI/Usings.cs
@@ -0,0 +1,12 @@
+global using FiscalOS.AdminCLI;
+global using FiscalOS.Core.Authentication;
+global using FiscalOS.Core.Identity;
+global using FiscalOS.Infra.Data;
+global using FiscalOS.Infra.DependencyInjection;
+
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
+
+global using Spectre.Console;
diff --git a/src/FiscalOS.AdminCLI/appsettings.Example.json b/src/FiscalOS.AdminCLI/appsettings.Example.json
new file mode 100644
index 0000000..a2cd868
--- /dev/null
+++ b/src/FiscalOS.AdminCLI/appsettings.Example.json
@@ -0,0 +1,5 @@
+{
+ "AppDbContextOptions": {
+ "DatabaseFilePath": "DatabaseFilePath"
+ }
+}
diff --git a/src/FiscalOS.Infra/Data/AppDbContext.cs b/src/FiscalOS.Infra/Data/AppDbContext.cs
index 1858447..db90d6f 100644
--- a/src/FiscalOS.Infra/Data/AppDbContext.cs
+++ b/src/FiscalOS.Infra/Data/AppDbContext.cs
@@ -51,9 +51,7 @@ public sealed class AppDbContext(IOptions ctxOptions) : DbC
.OnDelete(DeleteBehavior.Cascade);
eb.Property(static u => u.Username);
- eb.Property(static u => u.HashedPassword);
- });
-
+ eb.Property(static u => u.HashedPassword); });
modelBuilder.Entity(static eb =>
{
eb.Property(static t => t.Id);
diff --git a/src/FiscalOS.Infra/Data/AppDbContextOptions.cs b/src/FiscalOS.Infra/Data/AppDbContextOptions.cs
index b0d48e4..231cf41 100644
--- a/src/FiscalOS.Infra/Data/AppDbContextOptions.cs
+++ b/src/FiscalOS.Infra/Data/AppDbContextOptions.cs
@@ -3,6 +3,7 @@ namespace FiscalOS.Infra.Data;
public sealed record AppDbContextOptions
{
public string DatabaseFilePath { get; init; } = string.Empty;
+
public string GetFullyQualifiedDatabasePath()
{
return Path.GetFullPath(DatabaseFilePath, AppContext.BaseDirectory);
diff --git a/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs b/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs
index 158d635..e5754c2 100644
--- a/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/FiscalOS.Infra/DependencyInjection/ServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{
services.AddSingleton(TimeProvider.System);
+ services.ConfigureOptions();
services.AddSingleton(TokenGenerator.From);
services.AddSingleton(PasswordHasher.From);
services.ConfigureOptions();