[backend/startup] Add user management commands (ISH-504)

This commit is contained in:
Laura Hausmann 2025-03-13 20:14:02 +01:00
parent 4fc7ed1ac0
commit 2da465307e
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 114 additions and 8 deletions

View file

@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Migrations;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Backend.Core.Services.ImageProcessing;
@ -228,13 +229,106 @@ public static class WebApplicationExtensions
Environment.Exit(0);
}
string[] userMgmtCommands =
[
"--create-user", "--create-admin-user", "--reset-password", "--grant-admin", "--revoke-admin"
];
if (args.FirstOrDefault(userMgmtCommands.Contains) is { } cmd)
{
if (args is not [not null, var username])
{
app.Logger.LogError("Invalid syntax. Usage: {cmd} <username>", cmd);
Environment.Exit(1);
return null!;
}
if (cmd is "--create-user" or "--create-admin-user")
{
var password = CryptographyHelpers.GenerateRandomString(16);
app.Logger.LogInformation("Creating user {username}...", username);
var userSvc = provider.GetRequiredService<UserService>();
await userSvc.CreateLocalUserAsync(username, password, null, force: true);
if (args[0] is "--create-admin-user")
{
await db.Users.Where(p => p.Username == username)
.ExecuteUpdateAsync(p => p.SetProperty(i => i.IsAdmin, true));
app.Logger.LogInformation("Successfully created admin user.");
}
else
{
app.Logger.LogInformation("Successfully created user.");
}
app.Logger.LogInformation("Username: {username}", username);
app.Logger.LogInformation("Password: {password}", password);
Environment.Exit(0);
}
if (cmd is "--reset-password")
{
var settings = await db.UserSettings
.FirstOrDefaultAsync(p => p.User.UsernameLower == username.ToLowerInvariant());
if (settings == null)
{
app.Logger.LogError("User {username} not found.", username);
Environment.Exit(1);
}
app.Logger.LogInformation("Resetting password for user {username}...", username);
var password = CryptographyHelpers.GenerateRandomString(16);
settings.Password = AuthHelpers.HashPassword(password);
await db.SaveChangesAsync();
app.Logger.LogInformation("Password for user {username} was reset to: {password}", username, password);
Environment.Exit(0);
}
if (cmd is "--grant-admin")
{
var user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username.ToLowerInvariant());
if (user == null)
{
app.Logger.LogError("User {username} not found.", username);
Environment.Exit(1);
}
else
{
user.IsAdmin = true;
await db.SaveChangesAsync();
app.Logger.LogInformation("Granted admin privileges to user {username}.", username);
Environment.Exit(0);
}
}
if (cmd is "--revoke-admin")
{
var user = await db.Users.FirstOrDefaultAsync(p => p.UsernameLower == username.ToLowerInvariant());
if (user == null)
{
app.Logger.LogError("User {username} not found.", username);
Environment.Exit(1);
}
else
{
user.IsAdmin = false;
await db.SaveChangesAsync();
app.Logger.LogInformation("Revoked admin privileges of user {username}.", username);
Environment.Exit(0);
}
}
}
var storageConfig = app.Configuration.GetSection("Storage").Get<Config.StorageSection>() ??
throw new Exception("Failed to read Storage config section");
if (storageConfig.Provider == Enums.FileStorage.Local)
{
if (string.IsNullOrWhiteSpace(storageConfig.Local?.Path) ||
!Directory.Exists(storageConfig.Local.Path))
if (string.IsNullOrWhiteSpace(storageConfig.Local?.Path) || !Directory.Exists(storageConfig.Local.Path))
{
app.Logger.LogCritical("Local storage path does not exist");
Environment.Exit(1);

View file

@ -10,6 +10,8 @@ public static class StartupHelpers
{
Console.WriteLine($"""
Usage: ./{typeof(Program).Assembly.GetName().Name} [options...]
General options & commands:
-h, -?, --help Prints information on available command line arguments.
--migrate Applies pending migrations.
--migrate-and-start Applies pending migrations, then starts the application.
@ -26,6 +28,15 @@ public static class StartupHelpers
instead of http on the specified port.
--environment <env> Specifies the ASP.NET Core environment. Available options
are 'Development' and 'Production'.
User management commands:
--create-user <username> Creates a new user with the specified username
and a randomly generated password.
--create-admin-user <username> Creates a new admin user with the specified
username and a randomly generated password.
--reset-password <username> Resets the password of the specified user.
--grant-admin <username> Grants admin privileges to the specified user.
--revoke-admin <username> Revokes admin privileges of the specified user.
""");
Environment.Exit(0);
}

View file

@ -429,15 +429,16 @@ public class UserService(
return user;
}
public async Task<User> CreateLocalUserAsync(string username, string password, string? invite)
public async Task<User> CreateLocalUserAsync(string username, string password, string? invite, bool force = false)
{
//TODO: invite system should allow multi-use invites & time limited invites
if (security.Value.Registrations == Enums.Registrations.Closed)
if (security.Value.Registrations == Enums.Registrations.Closed && !force)
throw new GracefulException(HttpStatusCode.Forbidden, "Registrations are disabled on this server");
if (security.Value.Registrations == Enums.Registrations.Invite && invite == null)
if (security.Value.Registrations == Enums.Registrations.Invite && invite == null && !force)
throw new GracefulException(HttpStatusCode.Forbidden, "Request is missing the invite code");
if (security.Value.Registrations == Enums.Registrations.Invite
&& !await db.RegistrationInvites.AnyAsync(p => p.Code == invite))
&& !await db.RegistrationInvites.AnyAsync(p => p.Code == invite)
&& !force)
throw new GracefulException(HttpStatusCode.Forbidden, "The specified invite code is invalid");
if (!Regex.IsMatch(username, @"^\w+$"))
throw new GracefulException(HttpStatusCode.BadRequest, "Username must only contain letters and numbers");
@ -472,7 +473,7 @@ public class UserService(
var usedUsername = new UsedUsername { CreatedAt = DateTime.UtcNow, Username = username.ToLowerInvariant() };
if (security.Value.Registrations == Enums.Registrations.Invite)
if (security.Value.Registrations == Enums.Registrations.Invite && !force)
{
var ticket = await db.RegistrationInvites.FirstOrDefaultAsync(p => p.Code == invite);
if (ticket == null)