[backend/startup] Add user management commands (ISH-504)
This commit is contained in:
parent
4fc7ed1ac0
commit
2da465307e
3 changed files with 114 additions and 8 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue