-
@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.
+
+
+
+
+
New Username
+
+ @if (!string.IsNullOrEmpty(UsernameValidationError))
+ {
+
@UsernameValidationError
+ }
+
+
+ Change Username
+ Cancel
+
+
+
+ }
@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;
}