diff --git a/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor b/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor
index b8cba55b4..7cf3599bb 100644
--- a/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor
+++ b/src/AliasVault.Admin/Main/Pages/Logging/Auth.razor
@@ -48,6 +48,7 @@ else
@log.Id
@log.Timestamp.ToString("yyyy-MM-dd HH:mm")
@log.Username
+ @log.Client
@log.EventType
@log.IpAddress
@@ -62,6 +63,7 @@ else
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
new TableColumn { Title = "Username", PropertyName = "Username" },
+ new TableColumn { Title = "Client", PropertyName = "Client" },
new TableColumn { Title = "Event", PropertyName = "EventType" },
new TableColumn { Title = "Success", PropertyName = "IsSuccess" },
new TableColumn { Title = "IP", PropertyName = "IpAddress" },
@@ -175,6 +177,11 @@ else
? query.OrderBy(x => x.Username)
: query.OrderByDescending(x => x.Username);
break;
+ case "Client":
+ query = SortDirection == SortDirection.Ascending
+ ? query.OrderBy(x => x.Client)
+ : query.OrderByDescending(x => x.Client);
+ break;
case "EventType":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.EventType)
diff --git a/src/AliasVault.Api/Controllers/Security/SecurityController.cs b/src/AliasVault.Api/Controllers/Security/SecurityController.cs
index 7bcc9194a..7221f77b5 100644
--- a/src/AliasVault.Api/Controllers/Security/SecurityController.cs
+++ b/src/AliasVault.Api/Controllers/Security/SecurityController.cs
@@ -113,6 +113,7 @@ public class SecurityController(IAliasServerDbContextFactory dbContextFactory, U
Username = x.Username,
IpAddress = x.IpAddress ?? string.Empty,
UserAgent = x.UserAgent ?? string.Empty,
+ Client = x.Client ?? string.Empty,
IsSuccess = x.IsSuccess,
})
.ToListAsync();
diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/Components/RecentAuthLogsSection.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/RecentAuthLogsSection.razor
index 9c540dde6..611467614 100644
--- a/src/AliasVault.Client/Main/Pages/Settings/Security/Components/RecentAuthLogsSection.razor
+++ b/src/AliasVault.Client/Main/Pages/Settings/Security/Components/RecentAuthLogsSection.razor
@@ -24,7 +24,7 @@
| Timestamp |
Event Type |
- Username |
+ Client |
IP Address |
Device |
Success |
@@ -36,7 +36,7 @@
| @authLog.Timestamp.ToLocalTime().ToString("g") |
@authLog.EventType |
- @authLog.Username |
+ @authLog.Client |
@authLog.IpAddress |
@authLog.UserAgent |
|
diff --git a/src/AliasVault.Client/Program.cs b/src/AliasVault.Client/Program.cs
index 62583f881..9caa41ac9 100644
--- a/src/AliasVault.Client/Program.cs
+++ b/src/AliasVault.Client/Program.cs
@@ -8,6 +8,7 @@
using AliasVault.Client;
using AliasVault.Client.Providers;
using AliasVault.RazorComponents.Services;
+using AliasVault.Shared.Core;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Web;
@@ -56,6 +57,10 @@ builder.Services.AddScoped(sp =>
// If API URL override is set (used e.g. in dev), then ensure the API URL ends with a forward slash.
var baseUrl = string.IsNullOrEmpty(apiConfig.ApiUrl) ? builder.HostEnvironment.BaseAddress + "api/" : apiConfig.ApiUrl.TrimEnd('/') + "/";
httpClient.BaseAddress = new Uri(baseUrl);
+
+ // Add client header.
+ httpClient.DefaultRequestHeaders.Add("X-AliasVault-Client", "client-" + AppInfo.GetFullVersion());
+
return httpClient;
});
builder.Services.AddTransient();
diff --git a/src/Databases/AliasServerDb/AuthLog.cs b/src/Databases/AliasServerDb/AuthLog.cs
index 07510007a..493381555 100644
--- a/src/Databases/AliasServerDb/AuthLog.cs
+++ b/src/Databases/AliasServerDb/AuthLog.cs
@@ -157,4 +157,10 @@ public class AuthLog
/// Gets or sets a value indicating whether the authentication event is flagged as suspicious activity.
///
public bool IsSuspiciousActivity { get; set; }
+
+ ///
+ /// Gets or sets the client application name and version.
+ ///
+ [MaxLength(100)]
+ public string? Client { get; set; }
}
diff --git a/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/20250210101233_AddAuthLogClientHeader.Designer.cs b/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/20250210101233_AddAuthLogClientHeader.Designer.cs
new file mode 100644
index 000000000..51419bf46
--- /dev/null
+++ b/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/20250210101233_AddAuthLogClientHeader.Designer.cs
@@ -0,0 +1,913 @@
+//
+using System;
+using AliasServerDb;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AliasServerDb.Migrations.PostgresqlMigrations
+{
+ [DbContext(typeof(AliasServerDbContextPostgresql))]
+ [Migration("20250210101233_AddAuthLogClientHeader")]
+ partial class AddAuthLogClientHeader
+ {
+ ///
+ 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)
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("AliasServerDb.AdminRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("NormalizedName")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("AdminRoles");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AdminUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasColumnType("text");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("LastPasswordChanged")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("text");
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("text");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("AdminUsers");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("NormalizedName")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("AliasVaultRoles");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("Blocked")
+ .HasColumnType("boolean");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Email")
+ .HasColumnType("text");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("text");
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("text");
+
+ b.Property("PasswordChangedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserName")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("AliasVaultUsers");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeviceIdentifier")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("ExpireDate")
+ .HasMaxLength(255)
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IpAddress")
+ .HasMaxLength(45)
+ .HasColumnType("character varying(45)");
+
+ b.Property("PreviousTokenValue")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AliasVaultUserRefreshTokens");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AuthLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdditionalInfo")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Browser")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Client")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Country")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("DeviceType")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("EventType")
+ .HasColumnType("integer");
+
+ b.Property("FailureReason")
+ .HasColumnType("integer");
+
+ b.Property("IpAddress")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("IsSuccess")
+ .HasColumnType("boolean");
+
+ b.Property("IsSuspiciousActivity")
+ .HasColumnType("boolean");
+
+ b.Property("OperatingSystem")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("RequestPath")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Timestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserAgent")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ 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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DateSystem")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EncryptedSymmetricKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("From")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FromDomain")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FromLocal")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("MessageHtml")
+ .HasColumnType("text");
+
+ b.Property("MessagePlain")
+ .HasColumnType("text");
+
+ b.Property("MessagePreview")
+ .HasColumnType("text");
+
+ b.Property("MessageSource")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("PushNotificationSent")
+ .HasColumnType("boolean");
+
+ b.Property("Subject")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("To")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ToDomain")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ToLocal")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UserEncryptionKeyId")
+ .HasMaxLength(255)
+ .HasColumnType("uuid");
+
+ b.Property("Visible")
+ .HasColumnType("boolean");
+
+ 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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Bytes")
+ .IsRequired()
+ .HasColumnType("bytea");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EmailId")
+ .HasColumnType("integer");
+
+ b.Property("Filename")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Filesize")
+ .HasColumnType("integer");
+
+ b.Property("MimeType")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EmailId");
+
+ b.ToTable("EmailAttachments");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Log", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Application")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Exception")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Level")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("LogEvent")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("LogEvent");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("MessageTemplate")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Properties")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SourceContext")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("TimeStamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Application");
+
+ b.HasIndex("TimeStamp");
+
+ b.ToTable("Logs", (string)null);
+ });
+
+ modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("Key");
+
+ b.ToTable("ServerSettings");
+ });
+
+ modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("EndTime")
+ .HasColumnType("time without time zone");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("text");
+
+ b.Property("IsOnDemand")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("RunDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("StartTime")
+ .HasColumnType("time without time zone");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("TaskRunnerJobs");
+ });
+
+ modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("AddressDomain")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("AddressLocal")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Address")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserEmailClaims");
+ });
+
+ modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsPrimary")
+ .HasColumnType("boolean");
+
+ b.Property("PublicKey")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("UserEncryptionKeys");
+ });
+
+ modelBuilder.Entity("AliasServerDb.Vault", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CredentialsCount")
+ .HasColumnType("integer");
+
+ b.Property("EmailClaimsCount")
+ .HasColumnType("integer");
+
+ b.Property("EncryptionSettings")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("EncryptionType")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FileSize")
+ .HasColumnType("integer");
+
+ b.Property("RevisionNumber")
+ .HasColumnType("bigint");
+
+ b.Property("Salt")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("VaultBlob")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Verifier")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("character varying(1000)");
+
+ b.Property("Version")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("Vaults");
+ });
+
+ modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CurrentStatus")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("DesiredStatus")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)");
+
+ b.Property("Heartbeat")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ServiceName")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.HasKey("Id");
+
+ b.ToTable("WorkerServiceStatuses");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FriendlyName")
+ .HasColumnType("text");
+
+ b.Property("Xml")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataProtectionKeys");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("RoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("UserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.ToTable("UserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.ToTable("UserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("text");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("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")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ 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
+ }
+ }
+}
diff --git a/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/20250210101233_AddAuthLogClientHeader.cs b/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/20250210101233_AddAuthLogClientHeader.cs
new file mode 100644
index 000000000..755956e9c
--- /dev/null
+++ b/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/20250210101233_AddAuthLogClientHeader.cs
@@ -0,0 +1,30 @@
+//
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace AliasServerDb.Migrations.PostgresqlMigrations
+{
+ ///
+ public partial class AddAuthLogClientHeader : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Client",
+ table: "AuthLogs",
+ type: "character varying(100)",
+ maxLength: 100,
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Client",
+ table: "AuthLogs");
+ }
+ }
+}
diff --git a/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/AliasServerDbContextPostgresqlModelSnapshot.cs b/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/AliasServerDbContextPostgresqlModelSnapshot.cs
index aec0c01e7..ee90e2093 100644
--- a/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/AliasServerDbContextPostgresqlModelSnapshot.cs
+++ b/src/Databases/AliasServerDb/Migrations/PostgresqlMigrations/AliasServerDbContextPostgresqlModelSnapshot.cs
@@ -241,6 +241,10 @@ namespace AliasServerDb.Migrations.PostgresqlMigrations
.HasMaxLength(100)
.HasColumnType("character varying(100)");
+ b.Property("Client")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
b.Property("Country")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
diff --git a/src/Databases/AliasServerDb/Migrations/SqliteMigrations/20250210101257_AddAuthLogClientHeader.Designer.cs b/src/Databases/AliasServerDb/Migrations/SqliteMigrations/20250210101257_AddAuthLogClientHeader.Designer.cs
new file mode 100644
index 000000000..0c7f8f1a9
--- /dev/null
+++ b/src/Databases/AliasServerDb/Migrations/SqliteMigrations/20250210101257_AddAuthLogClientHeader.Designer.cs
@@ -0,0 +1,891 @@
+//
+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.SqliteMigrations
+{
+ [DbContext(typeof(AliasServerDbContextSqlite))]
+ [Migration("20250210101257_AddAuthLogClientHeader")]
+ partial class AddAuthLogClientHeader
+ {
+ ///
+ 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("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AdminRoles");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AdminUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastPasswordChanged")
+ .HasColumnType("TEXT");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AdminUsers");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AliasVaultRoles");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("TEXT");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("Blocked")
+ .HasColumnType("INTEGER");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .HasColumnType("TEXT");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordChangedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordHash")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("TEXT");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("INTEGER");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("INTEGER");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("UserName")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("AliasVaultUsers");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceIdentifier")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("ExpireDate")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("IpAddress")
+ .HasMaxLength(45)
+ .HasColumnType("TEXT");
+
+ b.Property("PreviousTokenValue")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AliasVaultUserRefreshTokens");
+ });
+
+ modelBuilder.Entity("AliasServerDb.AuthLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AdditionalInfo")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Browser")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Client")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Country")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceType")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("EventType")
+ .HasColumnType("INTEGER");
+
+ b.Property("FailureReason")
+ .HasColumnType("INTEGER");
+
+ b.Property("IpAddress")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("IsSuccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("IsSuspiciousActivity")
+ .HasColumnType("INTEGER");
+
+ b.Property("OperatingSystem")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("RequestPath")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT");
+
+ b.Property("Timestamp")
+ .HasColumnType("TEXT");
+
+ b.Property("UserAgent")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("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