diff --git a/apps/server/AliasVault.Client/Main/Components/Alerts/ServerValidationErrors.razor b/apps/server/AliasVault.Client/Main/Components/Alerts/ServerValidationErrors.razor
index 7777532b6..b9919f0d4 100644
--- a/apps/server/AliasVault.Client/Main/Components/Alerts/ServerValidationErrors.razor
+++ b/apps/server/AliasVault.Client/Main/Components/Alerts/ServerValidationErrors.razor
@@ -1,9 +1,11 @@
@if (_errors.Any())
{
- @foreach (var error in _errors)
- {
-
- }
+
+ @foreach (var error in _errors)
+ {
+
+ }
+
}
@code {
diff --git a/apps/server/AliasVault.Client/Services/Database/DbService.cs b/apps/server/AliasVault.Client/Services/Database/DbService.cs
index c9a3433cd..41f174504 100644
--- a/apps/server/AliasVault.Client/Services/Database/DbService.cs
+++ b/apps/server/AliasVault.Client/Services/Database/DbService.cs
@@ -40,6 +40,7 @@ public sealed class DbService : IDisposable
private readonly ILogger _logger;
private readonly GlobalNotificationService _globalNotificationService;
private readonly IStringLocalizer _sharedLocalizer;
+ private readonly CancellationTokenSource _backgroundSyncCts = new();
private SettingsService _settingsService = new();
private SqliteConnection? _sqlConnection;
private AliasClientDbContext _dbContext;
@@ -210,44 +211,79 @@ public sealed class DbService : IDisposable
// Set state to indicate background sync is pending
_state.UpdateState(DbServiceState.DatabaseStatus.BackgroundSyncPending);
+ // Capture cancellation token for this background operation
+ var cancellationToken = _backgroundSyncCts.Token;
+
// Fire and forget the background save operation
- _ = Task.Run(async () =>
- {
- try
+ _ = Task.Run(
+ async () =>
{
- // Prune expired items from trash before saving.
- await PruneExpiredTrashItemsAsync();
-
- // Make sure a public/private RSA encryption key exists before saving the database.
- await GetOrCreateEncryptionKeyAsync();
-
- var encryptedBase64String = await GetEncryptedDatabaseBase64String();
-
- // Update state to show we're actively syncing
- _state.UpdateState(DbServiceState.DatabaseStatus.SavingToServer);
-
- // Save to webapi.
- var success = await SaveToServerAsync(encryptedBase64String);
- if (success)
+ try
{
- _logger.LogInformation("Database successfully saved to server (background sync).");
+ if (cancellationToken.IsCancellationRequested || _disposed)
+ {
+ return;
+ }
+
+ // Prune expired items from trash before saving.
+ await PruneExpiredTrashItemsAsync();
+
+ if (cancellationToken.IsCancellationRequested || _disposed)
+ {
+ return;
+ }
+
+ // Make sure a public/private RSA encryption key exists before saving the database.
+ await GetOrCreateEncryptionKeyAsync();
+
+ if (cancellationToken.IsCancellationRequested || _disposed)
+ {
+ return;
+ }
+
+ var encryptedBase64String = await GetEncryptedDatabaseBase64String();
+
+ if (cancellationToken.IsCancellationRequested || _disposed)
+ {
+ return;
+ }
+
+ // Update state to show we're actively syncing
+ _state.UpdateState(DbServiceState.DatabaseStatus.SavingToServer);
+
+ // Save to webapi.
+ var success = await SaveToServerAsync(encryptedBase64String);
+ if (success)
+ {
+ _logger.LogInformation("Database successfully saved to server (background sync).");
+ _state.UpdateState(DbServiceState.DatabaseStatus.Ready);
+ }
+ else
+ {
+ _logger.LogWarning("Background sync to server failed.");
+ _globalNotificationService.AddErrorMessage(
+ "Failed to sync changes to server. Your changes are saved locally and will be synced on next refresh.");
+ _state.UpdateState(DbServiceState.DatabaseStatus.Ready);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Background sync was cancelled (e.g., during logout), this is expected
+ _logger.LogDebug("Background database sync was cancelled.");
+ }
+ catch (Exception ex) when (_disposed || cancellationToken.IsCancellationRequested)
+ {
+ // Service was disposed during sync, silently ignore
+ _logger.LogDebug(ex, "Background database sync aborted due to disposal.");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during background database sync.");
+ _globalNotificationService.AddErrorMessage(_sharedLocalizer["ErrorUnknown"]);
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
}
- else
- {
- _logger.LogWarning("Background sync to server failed.");
- _globalNotificationService.AddErrorMessage(
- "Failed to sync changes to server. Your changes are saved locally and will be synced on next refresh.");
- _state.UpdateState(DbServiceState.DatabaseStatus.Ready);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error during background database sync.");
- _globalNotificationService.AddErrorMessage(_sharedLocalizer["ErrorUnknown"]);
- _state.UpdateState(DbServiceState.DatabaseStatus.Ready);
- }
- });
+ },
+ cancellationToken);
}
///
@@ -1127,6 +1163,9 @@ public sealed class DbService : IDisposable
if (disposing)
{
+ // Cancel any pending background sync operations first
+ _backgroundSyncCts.Cancel();
+ _backgroundSyncCts.Dispose();
_sqlConnection?.Dispose();
}
diff --git a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/Abstracts/TwoFactorAuthBase.cs b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/Abstracts/TwoFactorAuthBase.cs
index 0c1f63a0a..6e6124d32 100644
--- a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/Abstracts/TwoFactorAuthBase.cs
+++ b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/Abstracts/TwoFactorAuthBase.cs
@@ -42,16 +42,16 @@ public class TwoFactorAuthBase : ClientPlaywrightTest
// Press the confirm disable button as well.
await confirmButton.ClickAsync();
- // Check if the success message is displayed.
+ // Check if the success message is displayed
var expectedMessage = "Two-factor authentication is now successfully disabled.";
- var successMessageLocator = Page.Locator($"div[role='alert']:has-text('{expectedMessage}')");
+ var successMessageLocator = Page.Locator($".messages-container div[role='alert']:has-text('{expectedMessage}')");
await successMessageLocator.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 5000,
});
- var message = await Page.TextContentAsync("div[role='alert']");
+ var message = await successMessageLocator.TextContentAsync();
Assert.That(message, Does.Contain("Two-factor authentication is now successfully disabled."), "No two-factor auth disable success message displayed.");
}
}
diff --git a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/TwoFactorAuthTests.cs b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/TwoFactorAuthTests.cs
index d3ee4db69..352922706 100644
--- a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/TwoFactorAuthTests.cs
+++ b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard3/TwoFactorAuthTests.cs
@@ -27,14 +27,14 @@ public class TwoFactorAuthTests : TwoFactorAuthBase
await DisableTwoFactorIfEnabled();
var (totpCode, _) = await EnableTwoFactor();
- // Check if the success message is displayed.
- var successMessage = Page.Locator("div[role='alert']");
+ // Check if the success message is displayed (target the app notification area, not the error UI in index.html).
+ var successMessage = Page.Locator(".messages-container div[role='alert']");
await successMessage.WaitForAsync(new LocatorWaitForOptions
{
State = WaitForSelectorState.Visible,
Timeout = 5000,
});
- var message = await Page.TextContentAsync("div[role='alert']");
+ var message = await successMessage.TextContentAsync();
Assert.That(message, Does.Contain("Two-factor authentication is now successfully enabled."), "No success message displayed.");
await Logout();
diff --git a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs
index 3e752d67f..3f99259aa 100644
--- a/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs
+++ b/apps/server/Tests/AliasVault.E2ETests/Tests/Client/Shard4/AuthTests.cs
@@ -160,7 +160,8 @@ public class AuthTests : ClientPlaywrightTest
await deleteButton.ClickAsync();
// Check for error message about wrong username
- var warning = await Page.TextContentAsync("div[role='alert']");
+ var warningLocator = Page.Locator(".messages-container div[role='alert']");
+ var warning = await warningLocator.TextContentAsync();
Assert.That(warning, Does.Contain("The username you entered does not match your current username"), "No warning shown when attempting to delete account with wrong username.");
// Try with correct username
@@ -178,7 +179,7 @@ public class AuthTests : ClientPlaywrightTest
await confirmButton.ClickAsync();
// Check for error message about wrong password
- warning = await Page.TextContentAsync("div[role='alert']");
+ warning = await warningLocator.TextContentAsync();
Assert.That(warning, Does.Contain("The provided password does not match"), "No warning shown when attempting to delete account with wrong password.");
// Fill in correct password
@@ -201,7 +202,7 @@ public class AuthTests : ClientPlaywrightTest
var loginButton = await WaitForAndGetElement("button[type='submit']");
await loginButton.ClickAsync();
- warning = await Page.TextContentAsync("div[role='alert']");
+ warning = await warningLocator.TextContentAsync();
Assert.That(warning, Does.Contain("Invalid username or password"), "No error shown when attempting to login with deleted account.");
}