diff --git a/apps/server/AliasVault.Admin/Main/Pages/Users/Delete.razor b/apps/server/AliasVault.Admin/Main/Pages/Users/Delete.razor index a36ef557a..40ce9361d 100644 --- a/apps/server/AliasVault.Admin/Main/Pages/Users/Delete.razor +++ b/apps/server/AliasVault.Admin/Main/Pages/Users/Delete.razor @@ -17,8 +17,8 @@ else {
- -

User

+ +

User

@Id
diff --git a/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor b/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor index e8db06cba..1092eace6 100644 --- a/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor +++ b/apps/server/AliasVault.Admin/Main/Pages/Users/View/Index.razor @@ -2,8 +2,10 @@ @using AliasVault.Admin.Main.Pages.Users.View.Components @using AliasVault.Admin.Main.Models @using AliasVault.Admin.Services +@using Microsoft.EntityFrameworkCore @inherits MainBase @inject StatisticsService StatisticsService +@inject ILogger Logger User @@ -28,7 +30,54 @@ else
-

@User.UserName

+
+
+

@User.UserName

+ @if (!IsEditingUsername) + { + + } +
+
+ + @if (IsEditingUsername) + { +
+
+
+ + + +

Change Username

+
+

+ Changing a username is permanent and may affect the user's ability to log in. + This is typically used for account archival or when a user specifically requests a username change. +

+
+
+
+ + + @if (!string.IsNullOrEmpty(UsernameValidationError)) + { +

@UsernameValidationError

+ } +
+
+ + +
+
+
+ } @if (UserUsageStats != null) @@ -234,6 +283,9 @@ else private bool IsEditingEmailLimits { get; set; } private int EditMaxEmails { get; set; } private int EditMaxEmailAgeDays { get; set; } + private bool IsEditingUsername { get; set; } + private string NewUsername { get; set; } = string.Empty; + private string UsernameValidationError { get; set; } = string.Empty; /// protected override async Task OnInitializedAsync() @@ -447,6 +499,7 @@ Do you want to proceed with the restoration?")) { if (User != null) { + var wasBlocked = User.Blocked; User.Blocked = !User.Blocked; // If user is unblocked by the admin, also reset any lockout status, which can be @@ -457,6 +510,11 @@ Do you want to proceed with the restoration?")) { } await dbContext.SaveChangesAsync(); + + // Add log entry for block/unblock action + var action = User.Blocked ? "Blocked" : "Unblocked"; + Logger.LogWarning("{Action} user {UserName} ({UserId}).", action, User.UserName, User.Id); + await RefreshData(); } } @@ -515,4 +573,93 @@ Do you want to proceed with the restoration?")) { GlobalNotificationService.AddSuccessMessage("Email limits updated successfully.", true); } } + + /// + /// Starts editing username for the user. + /// + private void StartEditingUsername() + { + IsEditingUsername = true; + NewUsername = User!.UserName ?? string.Empty; + UsernameValidationError = string.Empty; + } + + /// + /// Cancels editing username. + /// + private void CancelEditingUsername() + { + IsEditingUsername = false; + NewUsername = string.Empty; + UsernameValidationError = string.Empty; + } + + /// + /// Changes the username for the user with validation. + /// + private async Task ChangeUsername() + { + if (string.IsNullOrWhiteSpace(NewUsername)) + { + UsernameValidationError = "Username cannot be empty."; + return; + } + + if (NewUsername.Length < 3) + { + UsernameValidationError = "Username must be at least 3 characters long."; + return; + } + + if (NewUsername.Length > 256) + { + UsernameValidationError = "Username cannot be longer than 256 characters."; + return; + } + + if (NewUsername == User!.UserName) + { + UsernameValidationError = "New username must be different from current username."; + return; + } + + await using var dbContext = await DbContextFactory.CreateDbContextAsync(); + + // Check if username already exists + var existingUser = await dbContext.AliasVaultUsers.FirstOrDefaultAsync(x => x.UserName == NewUsername); + if (existingUser != null) + { + UsernameValidationError = "This username is already in use by another user."; + return; + } + + // Reload user to ensure we have the latest data + User = await dbContext.AliasVaultUsers.FindAsync(Id); + if (User != null) + { + var oldUsername = User.UserName; + User.UserName = NewUsername; + User.NormalizedUserName = NewUsername.ToUpperInvariant(); + User.Email = NewUsername; + User.NormalizedEmail = NewUsername.ToUpperInvariant(); + User.UpdatedAt = DateTime.UtcNow; + + try + { + await dbContext.SaveChangesAsync(); + + // Add log entry for username change + Logger.LogWarning("Changed username for user {OldUsername} ({UserId}) to {NewUsername}.", oldUsername, User.Id, NewUsername); + + IsEditingUsername = false; + UsernameValidationError = string.Empty; + await RefreshData(); + GlobalNotificationService.AddSuccessMessage($"Username changed from '{oldUsername}' to '{NewUsername}' successfully.", true); + } + catch (Exception ex) + { + UsernameValidationError = $"Error updating username: {ex.Message}"; + } + } + } } diff --git a/apps/server/AliasVault.Admin/wwwroot/css/tailwind.css b/apps/server/AliasVault.Admin/wwwroot/css/tailwind.css index 55bee90d4..849d29f81 100644 --- a/apps/server/AliasVault.Admin/wwwroot/css/tailwind.css +++ b/apps/server/AliasVault.Admin/wwwroot/css/tailwind.css @@ -1744,6 +1744,11 @@ video { color: rgb(22 101 52 / var(--tw-text-opacity)); } +.text-orange-600 { + --tw-text-opacity: 1; + color: rgb(234 88 12 / var(--tw-text-opacity)); +} + .text-primary-600 { --tw-text-opacity: 1; color: rgb(214 131 56 / var(--tw-text-opacity)); @@ -2227,6 +2232,11 @@ video { color: rgb(74 222 128 / var(--tw-text-opacity)); } +.dark\:text-orange-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(251 146 60 / var(--tw-text-opacity)); +} + .dark\:text-primary-200:is(.dark *) { --tw-text-opacity: 1; color: rgb(251 203 116 / var(--tw-text-opacity)); @@ -2581,10 +2591,6 @@ video { grid-template-columns: repeat(4, minmax(0, 1fr)); } - .lg\:grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - .lg\:flex-row { flex-direction: row; }