feat(admincli): add command for creating a user

This commit is contained in:
Stevan Freeborn
2026-02-02 15:41:47 -06:00
parent 5a5efbefd7
commit 35557a8aa7
11 changed files with 114 additions and 14 deletions
+1
View File
@@ -11,5 +11,6 @@
</PackageVersion> </PackageVersion>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
<PackageVersion Include="Spectre.Console" Version="0.54.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+1
View File
@@ -1,3 +1,4 @@
[*.cs] [*.cs]
dotnet_diagnostic.CA1303.severity = none dotnet_diagnostic.CA1303.severity = none
dotnet_diagnostic.CA2007.severity = none
+52
View File
@@ -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<IConfiguration>();
var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var selectionPrompt = new SelectionPrompt<string>()
.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<string>("Enter the [green]username[/]:", cancellationToken);
var password = _console.Prompt(
new TextPrompt<string>("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;
}
}
+9
View File
@@ -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];
}
@@ -4,4 +4,20 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Spectre.Console" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FiscalOS.Infra\FiscalOS.Infra.csproj" />
<ProjectReference Include="..\FiscalOS.Core\FiscalOS.Core.csproj" />
</ItemGroup>
</Project> </Project>
+15 -11
View File
@@ -1,11 +1,15 @@
// TODO: We need a way to add await Host.CreateDefaultBuilder(args)
// a user to the system with .ConfigureAppConfiguration(
// a properly hashed password static c => c.SetBasePath(AppContext.BaseDirectory)
// so need something like this: .AddJsonFile("appsettings.json")
// .AddEnvironmentVariables()
// 1. Ask for username )
// 2. Ask for user password .ConfigureLogging(static c => c.ClearProviders())
// 3. Hash user password .ConfigureServices(static (_, services) =>
// 4. Save user to database {
services.AddSingleton(AnsiConsole.Console);
Console.WriteLine("Hello, World!"); services.AddInfrastructure();
services.AddHostedService<App>();
})
.Build()
.StartAsync();
+12
View File
@@ -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;
@@ -0,0 +1,5 @@
{
"AppDbContextOptions": {
"DatabaseFilePath": "DatabaseFilePath"
}
}
+1 -3
View File
@@ -51,9 +51,7 @@ public sealed class AppDbContext(IOptions<AppDbContextOptions> ctxOptions) : DbC
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
eb.Property(static u => u.Username); eb.Property(static u => u.Username);
eb.Property(static u => u.HashedPassword); eb.Property(static u => u.HashedPassword); });
});
modelBuilder.Entity<RefreshToken>(static eb => modelBuilder.Entity<RefreshToken>(static eb =>
{ {
eb.Property(static t => t.Id); eb.Property(static t => t.Id);
@@ -3,6 +3,7 @@ namespace FiscalOS.Infra.Data;
public sealed record AppDbContextOptions public sealed record AppDbContextOptions
{ {
public string DatabaseFilePath { get; init; } = string.Empty; public string DatabaseFilePath { get; init; } = string.Empty;
public string GetFullyQualifiedDatabasePath() public string GetFullyQualifiedDatabasePath()
{ {
return Path.GetFullPath(DatabaseFilePath, AppContext.BaseDirectory); return Path.GetFullPath(DatabaseFilePath, AppContext.BaseDirectory);
@@ -5,6 +5,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddInfrastructure(this IServiceCollection services) public static IServiceCollection AddInfrastructure(this IServiceCollection services)
{ {
services.AddSingleton(TimeProvider.System); services.AddSingleton(TimeProvider.System);
services.ConfigureOptions<JwtOptionsSetup>();
services.AddSingleton<ITokenGenerator, TokenGenerator>(TokenGenerator.From); services.AddSingleton<ITokenGenerator, TokenGenerator>(TokenGenerator.From);
services.AddSingleton<IPasswordHasher, PasswordHasher>(PasswordHasher.From); services.AddSingleton<IPasswordHasher, PasswordHasher>(PasswordHasher.From);
services.ConfigureOptions<AppDbContextOptionsSetup>(); services.ConfigureOptions<AppDbContextOptionsSetup>();