Merge pull request #487 from lanedirt/342-add-option-to-block-existing-user-in-admin

Add option to block existing user in admin
This commit is contained in:
Leendert de Borst
2024-12-20 19:57:39 +01:00
committed by GitHub
12 changed files with 1091 additions and 7 deletions

View File

@@ -32,6 +32,11 @@ public class UserViewModel
/// </summary>
public bool TwoFactorEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user is blocked.
/// </summary>
public bool Blocked { get; set; }
/// <summary>
/// Gets or sets the vault count.
/// </summary>

View File

@@ -49,7 +49,7 @@ else
<SortableTableColumn>@log.Timestamp.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@log.Username</SortableTableColumn>
<SortableTableColumn>@log.EventType</SortableTableColumn>
<SortableTableColumn><StatusPill Enabled="log.IsSuccess" TextTrue="Success" TextFalse="Failed" /></SortableTableColumn>
<SortableTableColumn><StatusPill Enabled="log.IsSuccess" TextTrue="Success" TextFalse="@log.FailureReason.ToString()" /></SortableTableColumn>
<SortableTableColumn>@log.IpAddress</SortableTableColumn>
</SortableTableRow>
}

View File

@@ -30,14 +30,19 @@ else
@foreach (var user in UserList)
{
<SortableTableRow>
<SortableTableColumn IsPrimary="true">@user.Id</SortableTableColumn>
<SortableTableColumn>@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn IsPrimary="true">@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@user.UserName</SortableTableColumn>
<SortableTableColumn>@user.VaultCount</SortableTableColumn>
<SortableTableColumn>@user.EmailClaimCount</SortableTableColumn>
<SortableTableColumn>@Math.Round((double)user.VaultStorageInKb / 1024, 1) MB</SortableTableColumn>
<SortableTableColumn><StatusPill Enabled="user.TwoFactorEnabled" /></SortableTableColumn>
<SortableTableColumn>@user.LastVaultUpdate.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>
@if (user.Blocked)
{
<StatusPill Enabled="false" TextFalse="Blocked" />
}
</SortableTableColumn>
<SortableTableColumn>
<LinkButton Color="primary" Href="@($"users/{user.Id}")" Text="View" />
</SortableTableColumn>
@@ -49,7 +54,6 @@ else
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Registered", PropertyName = "CreatedAt" },
new TableColumn { Title = "Username", PropertyName = "UserName" },
new TableColumn { Title = "# Vaults", PropertyName = "VaultCount" },
@@ -57,6 +61,7 @@ else
new TableColumn { Title = "Storage", PropertyName = "VaultStorageInKb" },
new TableColumn { Title = "2FA", PropertyName = "TwoFactorEnabled" },
new TableColumn { Title = "LastVaultUpdate", PropertyName = "LastVaultUpdate" },
new TableColumn { Title = "Status", Sortable = false },
new TableColumn { Title = "Actions", Sortable = false},
];
@@ -130,6 +135,7 @@ else
u.UserName,
u.CreatedAt,
u.TwoFactorEnabled,
u.Blocked,
Vaults = u.Vaults.Select(v => new
{
v.FileSize,
@@ -147,6 +153,7 @@ else
Id = user.Id,
UserName = user.UserName?.ToLower() ?? "N/A",
TwoFactorEnabled = user.TwoFactorEnabled,
Blocked = user.Blocked,
CreatedAt = user.CreatedAt,
VaultCount = user.Vaults.Count(),
EmailClaimCount = user.EmailClaims.Count(),

View File

@@ -48,6 +48,17 @@ else
}
}
</div>
<div class="flex items-center space-x-2 mt-4">
<span class="text-sm font-medium text-gray-900 dark:text-white">Account Status:</span>
<StatusPill Enabled="@(!User.Blocked)" TextTrue="Active" TextFalse="Blocked" />
<Button Color="@(User.Blocked ? "success" : "danger")" OnClick="ToggleBlockStatus">
@(User.Blocked ? "Unblock User" : "Block User")
</Button>
<span class="text-sm text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1"></i>
Blocking a user prevents them from logging in or accessing AliasVault
</span>
</div>
</div>
</div>
</div>
@@ -291,4 +302,19 @@ Do you want to proceed with the restoration?")) {
await RefreshData();
}
}
/// <summary>
/// Toggles the blocked status of the user.
/// </summary>
private async Task ToggleBlockStatus()
{
User = await DbContext.AliasVaultUsers.FindAsync(Id);
if (User != null)
{
User.Blocked = !User.Blocked;
await DbContext.SaveChangesAsync();
await RefreshData();
}
}
}

View File

@@ -61,9 +61,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
private static readonly string[] InvalidRecoveryCode = ["Invalid recovery code."];
/// <summary>
/// Error message for invalid 2-factor authentication recovery code.
/// Error message for too many failed login attempts.
/// </summary>
private static readonly string[] AccountLocked = ["You have entered an incorrect password too many times and your account has now been locked out. You can try again in 30 minutes.."];
private static readonly string[] AccountLocked = ["You have entered an incorrect password too many times and your account has now been locked out. You can try again in 30 minutes."];
/// <summary>
/// Error message for if user is (manually) blocked by admin.
/// </summary>
private static readonly string[] AccountBlocked = ["Your account has been disabled. If you believe this is a mistake, please contact support."];
/// <summary>
/// Semaphore to prevent concurrent access to the database when generating new tokens for a user.
@@ -103,6 +108,13 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
return BadRequest(ServerValidationErrorResponse.Create(AccountLocked, 400));
}
// Check if the account is blocked.
if (user.Blocked)
{
await authLoggingService.LogAuthEventFailAsync(model.Username, AuthEventType.Login, AuthFailureReason.AccountBlocked);
return BadRequest(ServerValidationErrorResponse.Create(AccountBlocked, 400));
}
// Retrieve latest vault of user which contains the current salt and verifier.
var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user);
@@ -263,6 +275,13 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
return Unauthorized("User not found (name-2)");
}
// Check if the account is blocked.
if (user.Blocked)
{
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TokenRefresh, AuthFailureReason.AccountBlocked);
return Unauthorized("Account blocked");
}
// Generate new tokens for the user.
var token = await GenerateNewTokensForUser(user, tokenModel.RefreshToken);
if (token == null)
@@ -610,6 +629,13 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
return (null, null, BadRequest(ServerValidationErrorResponse.Create(AccountLocked, 400)));
}
// Check if the account is blocked.
if (user.Blocked)
{
await authLoggingService.LogAuthEventFailAsync(model.Username, AuthEventType.Login, AuthFailureReason.AccountBlocked);
return (null, null, BadRequest(ServerValidationErrorResponse.Create(AccountBlocked, 400)));
}
// Validate the SRP session (actual password check).
var serverSession = AuthHelper.ValidateSrpSession(cache, user, model.ClientPublicEphemeral, model.ClientSessionProof);
if (serverSession is null)

View File

@@ -24,6 +24,11 @@ public class AliasVaultUser : IdentityUser
/// </summary>
public DateTime PasswordChangedAt { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user is blocked and should not be able to log in.
/// </summary>
public bool Blocked { get; set; }
/// <summary>
/// Gets or sets updated timestamp.
/// </summary>

View File

@@ -47,6 +47,11 @@ public enum AuthFailureReason
/// </summary>
InvalidRefreshToken = 6,
/// <summary>
/// Indicates that the account is manually blocked by an administrator.
/// </summary>
AccountBlocked = 7,
/// <summary>
/// Indicates that the failure reason was unknown.
/// </summary>

View File

@@ -0,0 +1,884 @@
// <auto-generated />
using System;
using AliasServerDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AliasServerDb.Migrations
{
[DbContext(typeof(AliasServerDbContext))]
[Migration("20241220164855_AddUserBlockedStatus")]
partial class AddUserBlockedStatus
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminRoles");
});
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastPasswordChanged")
.HasColumnType("TEXT");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("NormalizedName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultRoles");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<bool>("Blocked")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT");
b.Property<DateTime>("PasswordChangedAt")
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("AliasVaultUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DeviceIdentifier")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("ExpireDate")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("IpAddress")
.HasMaxLength(45)
.HasColumnType("TEXT");
b.Property<string>("PreviousTokenValue")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AliasVaultUserRefreshTokens");
});
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Country")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DeviceType")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("nvarchar(50)");
b.Property<int?>("FailureReason")
.HasColumnType("INTEGER");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<bool>("IsSuccess")
.HasColumnType("INTEGER");
b.Property<bool>("IsSuspiciousActivity")
.HasColumnType("INTEGER");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("RequestPath")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.Property<string>("UserAgent")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "EventType" }, "IX_EventType");
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
.IsDescending(false, false, true);
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
.IsDescending(false, true);
b.ToTable("AuthLogs");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<DateTime>("DateSystem")
.HasColumnType("TEXT");
b.Property<string>("EncryptedSymmetricKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("From")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("FromLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MessageHtml")
.HasColumnType("TEXT");
b.Property<string>("MessagePlain")
.HasColumnType("TEXT");
b.Property<string>("MessagePreview")
.HasColumnType("TEXT");
b.Property<string>("MessageSource")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("PushNotificationSent")
.HasColumnType("INTEGER");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("To")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToDomain")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ToLocal")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserEncryptionKeyId")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("Visible")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Date");
b.HasIndex("DateSystem");
b.HasIndex("PushNotificationSent");
b.HasIndex("ToLocal");
b.HasIndex("UserEncryptionKeyId");
b.HasIndex("Visible");
b.ToTable("Emails");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<byte[]>("Bytes")
.IsRequired()
.HasColumnType("BLOB");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<int>("EmailId")
.HasColumnType("INTEGER");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Filesize")
.HasColumnType("INTEGER");
b.Property<string>("MimeType")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("EmailId");
b.ToTable("EmailAttachments");
});
modelBuilder.Entity("AliasServerDb.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Application")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Exception")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Level")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT");
b.Property<string>("LogEvent")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("LogEvent");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("MessageTemplate")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Properties")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("SourceContext")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("TimeStamp")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Application");
b.HasIndex("TimeStamp");
b.ToTable("Logs", (string)null);
});
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
{
b.Property<string>("Key")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("ServerSettings");
});
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<TimeOnly?>("EndTime")
.HasColumnType("TEXT");
b.Property<string>("ErrorMessage")
.HasColumnType("TEXT");
b.Property<bool>("IsOnDemand")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("RunDate")
.HasColumnType("TEXT");
b.Property<TimeOnly>("StartTime")
.HasColumnType("TEXT");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("TaskRunnerJobs");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AddressDomain")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AddressLocal")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Address")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("UserEmailClaims");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsPrimary")
.HasColumnType("INTEGER");
b.Property<string>("PublicKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserEncryptionKeys");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("CredentialsCount")
.HasColumnType("INTEGER");
b.Property<int>("EmailClaimsCount")
.HasColumnType("INTEGER");
b.Property<string>("EncryptionSettings")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("EncryptionType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("FileSize")
.HasColumnType("INTEGER");
b.Property<long>("RevisionNumber")
.HasColumnType("INTEGER");
b.Property<string>("Salt")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("VaultBlob")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Verifier")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("TEXT");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Vaults");
});
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CurrentStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("DesiredStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime>("Heartbeat")
.HasColumnType("TEXT");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar");
b.HasKey("Id");
b.ToTable("WorkerServiceStatuses");
});
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("FriendlyName")
.HasColumnType("TEXT");
b.Property<string>("Xml")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("DataProtectionKeys");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
.WithMany("Emails")
.HasForeignKey("UserEncryptionKeyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("EncryptionKey");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.HasOne("AliasServerDb.Email", "Email")
.WithMany("Attachments")
.HasForeignKey("EmailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Email");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EmailClaims")
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EncryptionKeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("Vaults")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Navigation("EmailClaims");
b.Navigation("EncryptionKeys");
b.Navigation("Vaults");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Navigation("Attachments");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Navigation("Emails");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,30 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasServerDb.Migrations
{
/// <inheritdoc />
public partial class AddUserBlockedStatus : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Blocked",
table: "AliasVaultUsers",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Blocked",
table: "AliasVaultUsers");
}
}
}

View File

@@ -122,6 +122,9 @@ namespace AliasServerDb.Migrations
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<bool>("Blocked")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("TEXT");

View File

@@ -1,5 +1,5 @@
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 shadow rounded border dark:border-gray-700">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<thead class="text-xs text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
@foreach (var column in Columns)
{

View File

@@ -0,0 +1,93 @@
//-----------------------------------------------------------------------
// <copyright file="UserBlockedTests.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.E2ETests.Tests.Client.Shard5;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// End-to-end tests for user block functionality.
/// </summary>
[Parallelizable(ParallelScope.Self)]
[Category("ClientTests")]
[TestFixture]
public class UserBlockedTests : ClientPlaywrightTest
{
/// <summary>
/// Test that a blocked user cannot log in.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task BlockedUserLoginTest()
{
// First logout the current user
await Logout();
// Find the current test user in the database and set their blocked status to true
var user = await ApiDbContext.AliasVaultUsers.FirstAsync(x => x.UserName == TestUserUsername);
user.Blocked = true;
await ApiDbContext.SaveChangesAsync();
// Attempt to log in with the blocked user's credentials
await NavigateToLogin();
// Try to log in with test credentials
var emailField = await WaitForAndGetElement("input[id='email']");
var passwordField = await WaitForAndGetElement("input[id='password']");
await emailField.FillAsync(TestUserUsername);
await passwordField.FillAsync(TestUserPassword);
var loginButton = await WaitForAndGetElement("button[type='submit']");
await loginButton.ClickAsync();
// Check if we get an error message about the account being blocked
await WaitForUrlAsync("user/login**", "Your account has been disabled");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Your account has been disabled"), "No blocked account message shown when attempting to login with blocked account.");
}
/// <summary>
/// Test that a blocked user gets logged out when their access token expires and cannot be refreshed.
/// </summary>
/// <returns>Async task.</returns>
[Test]
public async Task BlockedUserTokenRefreshTest()
{
// Make sure test user is not blocked.
var user = await ApiDbContext.AliasVaultUsers.FirstAsync(x => x.UserName == TestUserUsername);
user.Blocked = false;
await ApiDbContext.SaveChangesAsync();
// Make sure we're logged in.
await Logout();
await Login();
// Wait for the index page to load which should show "Credentials" in the top menu.
await WaitForUrlAsync("**", "Credentials");
// First navigate to a test page to verify we're logged in
await NavigateUsingBlazorRouter("test/1");
await WaitForUrlAsync("test/1", "Test 1 OK");
// Find the current test user in the database and set their blocked status to true
user.Blocked = true;
await ApiDbContext.SaveChangesAsync();
// Increase the time by 1 hour to make the JWT token expire
ApiTimeProvider.AdvanceBy(TimeSpan.FromHours(1));
// Navigate to another test page which will trigger a token refresh attempt
await NavigateUsingBlazorRouter("test/2");
// We should be redirected to the login page with an error message
await WaitForUrlAsync("user/login", "Log in to AliasVault");
var pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Log in to AliasVault"), "No login page shown after token refresh attempt with blocked account.");
}
}