From cd4dc918cb1a8f402a17ada26471c7157889be66 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 31 Aug 2024 16:24:03 +0200 Subject: [PATCH 1/4] Update index.template.html loading screen delay (#184) --- .../wwwroot/index.template.html | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/AliasVault.Client/wwwroot/index.template.html b/src/AliasVault.Client/wwwroot/index.template.html index 5ab34962f..8eea190e3 100644 --- a/src/AliasVault.Client/wwwroot/index.template.html +++ b/src/AliasVault.Client/wwwroot/index.template.html @@ -77,30 +77,32 @@ const randomIndex = Math.floor(Math.random() * securityQuotes.length); quoteElement.textContent = `"${securityQuotes[randomIndex]}"`; - window.addEventListener('load', function() { + function manageLoadingScreen() { const startTime = new Date().getTime(); const minDisplayTime = 1000; + const checkInterval = 500; - function hideLoadingScreen() { - document.getElementById('loading-screen').style.display = 'none'; - document.getElementById('app').style.removeProperty('visibility'); - } + const appElement = document.getElementById('app'); + const loadingScreen = document.getElementById('loading-screen'); - function checkElapsedTime() { - const currentTime = new Date().getTime(); - const elapsedTime = currentTime - startTime; + appElement.style.visibility = 'hidden'; + loadingScreen.style.display = 'flex'; - if (elapsedTime >= minDisplayTime) { - hideLoadingScreen(); - } else { - setTimeout(hideLoadingScreen, minDisplayTime - elapsedTime); + const checkContentAndTime = () => { + const elapsedTime = new Date().getTime() - startTime; + const hasContent = appElement.innerHTML.trim() !== ''; + + if ((elapsedTime >= minDisplayTime && hasContent)) { + loadingScreen.style.display = 'none'; + appElement.style.removeProperty('visibility'); + clearInterval(intervalId); } - } + }; - document.getElementById('app').style.visibility = 'hidden'; - document.getElementById('loading-screen').style.display = 'flex'; - checkElapsedTime(); - }); + const intervalId = setInterval(checkContentAndTime, checkInterval); + } + + window.addEventListener('load', manageLoadingScreen); From 466c181ad17616ba94dc2d6c7b5fb68de1864fa1 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 31 Aug 2024 17:26:12 +0200 Subject: [PATCH 2/4] Add email delete option to email modal (#184) --- .../Controllers/AuthController.cs | 4 +- .../Controllers/Email/EmailController.cs | 98 +++++++++++++++---- .../Controllers/VaultController.cs | 4 +- .../Main/Components/Email/EmailModal.razor | 97 +++++++++++++++++- .../Main/Components/Email/RecentEmails.razor | 2 +- .../Main/Pages/Emails/Home.razor | 4 +- .../Services/CredentialService.cs | 2 +- .../Services/Database/DbService.cs | 2 +- .../Handlers/DatabaseMessageStore.cs | 2 +- .../AuthLoggingService.cs | 4 +- .../AliasVault.WorkerStatus/StatusWorker.cs | 6 +- 11 files changed, 188 insertions(+), 37 deletions(-) diff --git a/src/AliasVault.Api/Controllers/AuthController.cs b/src/AliasVault.Api/Controllers/AuthController.cs index 9fe97dba0..b8fe5f61d 100644 --- a/src/AliasVault.Api/Controllers/AuthController.cs +++ b/src/AliasVault.Api/Controllers/AuthController.cs @@ -240,7 +240,7 @@ public class AuthController(IDbContextFactory dbContextFac var newRefreshToken = GenerateRefreshToken(); // Add new refresh token. - await context.AliasVaultUserRefreshTokens.AddAsync(new AliasVaultUserRefreshToken + context.AliasVaultUserRefreshTokens.Add(new AliasVaultUserRefreshToken { UserId = user.Id, DeviceIdentifier = deviceIdentifier, @@ -526,7 +526,7 @@ public class AuthController(IDbContextFactory dbContextFac context.AliasVaultUserRefreshTokens.RemoveRange(existingTokens); // Add new refresh token. - await context.AliasVaultUserRefreshTokens.AddAsync(new AliasVaultUserRefreshToken + context.AliasVaultUserRefreshTokens.Add(new AliasVaultUserRefreshToken { UserId = user.Id, DeviceIdentifier = deviceIdentifier, diff --git a/src/AliasVault.Api/Controllers/Email/EmailController.cs b/src/AliasVault.Api/Controllers/Email/EmailController.cs index a4cdee79e..dc9ef851e 100644 --- a/src/AliasVault.Api/Controllers/Email/EmailController.cs +++ b/src/AliasVault.Api/Controllers/Email/EmailController.cs @@ -19,10 +19,11 @@ using Microsoft.EntityFrameworkCore; /// /// Email controller for retrieving emails from the database. /// +/// ILogger instance. /// DbContext instance. /// UserManager instance. [ApiVersion("1")] -public class EmailController(IDbContextFactory dbContextFactory, UserManager userManager) : AuthenticatedRequestController(userManager) +public class EmailController(ILogger logger, IDbContextFactory dbContextFactory, UserManager userManager) : AuthenticatedRequestController(userManager) { /// /// Get the newest version of the vault for the current user. @@ -34,31 +35,15 @@ public class EmailController(IDbContextFactory dbContextFa { await using var context = await dbContextFactory.CreateDbContextAsync(); - var user = await GetCurrentUserAsync(); - if (user is null) + var (email, errorResult) = await AuthenticateAndRetrieveEmailAsync(id, context); + if (errorResult != null) { - return Unauthorized("Not authenticated."); - } - - // Retrieve email from database. - var email = await context.Emails.Include(x => x.EncryptionKey).AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); - if (email is null) - { - return NotFound("Email not found."); - } - - // See if this user has a valid claim to the email address. - var emailClaim = await context.UserEmailClaims - .FirstOrDefaultAsync(x => x.UserId == user.Id && x.Address == email.To); - - if (emailClaim is null) - { - return Unauthorized("User does not have a claim to this email address."); + return errorResult; } var returnEmail = new EmailApiModel { - Id = email.Id, + Id = email!.Id, Subject = email.Subject, FromDisplay = ConversionHelper.ConvertFromToFromDisplay(email.From), FromDomain = email.FromDomain, @@ -94,4 +79,75 @@ public class EmailController(IDbContextFactory dbContextFa return Ok(returnEmail); } + + /// + /// Deletes an email for the current user. + /// + /// The email ID to delete. + /// A response indicating the success or failure of the deletion. + [HttpDelete(template: "{id}", Name = "DeleteEmail")] + public async Task DeleteEmail(int id) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + + var (email, errorResult) = await AuthenticateAndRetrieveEmailAsync(id, context); + if (errorResult != null) + { + return errorResult; + } + + // Delete associated attachments + context.EmailAttachments.RemoveRange(email!.Attachments); + + // Delete the email + context.Emails.Remove(email); + + try + { + await context.SaveChangesAsync(); + return Ok(); + } + catch (Exception ex) + { + // Log the exception + logger.LogError(ex, "An error occurred while deleting email with ID {id}.", id); + return StatusCode(500, $"An error occurred while deleting the email: {ex.Message}"); + } + } + + /// + /// Authenticates the user and retrieves the requested email. + /// + /// The email ID to retrieve. + /// The database context. + /// A tuple containing the authenticated user, the email, and an IActionResult if there's an error. + private async Task<(Email? Email, IActionResult? ErrorResult)> AuthenticateAndRetrieveEmailAsync(int id, AliasServerDbContext context) + { + var user = await GetCurrentUserAsync(); + if (user is null) + { + return (null, Unauthorized("Not authenticated.")); + } + + // Retrieve email from database. + var email = await context.Emails + .Include(x => x.Attachments) + .FirstOrDefaultAsync(x => x.Id == id); + + if (email is null) + { + return (null, NotFound("Email not found.")); + } + + // See if this user has a valid claim to the email address. + var emailClaim = await context.UserEmailClaims + .FirstOrDefaultAsync(x => x.UserId == user.Id && x.Address == email.To); + + if (emailClaim is null) + { + return (null, Unauthorized("User does not have a claim to this email address.")); + } + + return (email, null); + } } diff --git a/src/AliasVault.Api/Controllers/VaultController.cs b/src/AliasVault.Api/Controllers/VaultController.cs index bd769a88a..1ec730f60 100644 --- a/src/AliasVault.Api/Controllers/VaultController.cs +++ b/src/AliasVault.Api/Controllers/VaultController.cs @@ -116,7 +116,7 @@ public class VaultController(ILogger logger, IDbContextFactory< context.Vaults.RemoveRange(vaultsToDelete); // Add the new vault and commit to database. - await context.Vaults.AddAsync(newVault); + context.Vaults.Add(newVault); await context.SaveChangesAsync(); // Update user email claims if email addresses have been supplied. @@ -176,7 +176,7 @@ public class VaultController(ILogger logger, IDbContextFactory< { try { - await context.UserEmailClaims.AddAsync(new UserEmailClaim + context.UserEmailClaims.Add(new UserEmailClaim { UserId = userId, Address = sanitizedEmail, diff --git a/src/AliasVault.Client/Main/Components/Email/EmailModal.razor b/src/AliasVault.Client/Main/Components/Email/EmailModal.razor index a543201b8..6347a3037 100644 --- a/src/AliasVault.Client/Main/Components/Email/EmailModal.razor +++ b/src/AliasVault.Client/Main/Components/Email/EmailModal.razor @@ -1,5 +1,8 @@ @using AliasVault.Shared.Models.Spamok @inject JsInteropService JsInteropService +@inject GlobalNotificationService GlobalNotificationService +@inject IHttpClientFactory HttpClientFactory +@inject HttpClient HttpClient
@@ -32,7 +35,8 @@
-
+
+
@@ -58,6 +62,12 @@ [Parameter] public EventCallback OnClose { get; set; } + /// + /// Callback when an email is deleted. + /// + [Parameter] + public EventCallback OnEmailDeleted { get; set; } + /// /// The message body to display /// @@ -72,6 +82,91 @@ return OnClose.InvokeAsync(false); } + /// + /// Delete the current email. + /// + private async Task DeleteEmail() + { + if (Email == null) + { + return; + } + + if (IsSpamOk) + { + //await JsInteropService.Confirm("Are you sure you want to delete this email?", DeleteEmailAliasVault); + await DeleteEmailSpamOk(); + } + else + { + //await JsInteropService.Confirm("Are you sure you want to delete this email?", DeleteEmailAliasVault); + await DeleteEmailAliasVault(); + } + } + + /// + /// Delete the current email in SpamOk. + /// + private async Task DeleteEmailSpamOk() + { + if (Email == null) + { + return; + } + + try + { + var client = HttpClientFactory.CreateClient("EmailClient"); + var response = await client.DeleteAsync($"https://api.spamok.com/v2/Email/{Email.ToLocal}/{Email.Id}"); + if (response.IsSuccessStatusCode) + { + GlobalNotificationService.AddSuccessMessage("Email deleted successfully", true); + await OnEmailDeleted.InvokeAsync(Email.Id); + await Close(); + } + else + { + var errorMessage = await response.Content.ReadAsStringAsync(); + GlobalNotificationService.AddErrorMessage($"Failed to delete email: {errorMessage}", true); + } + } + catch (Exception ex) + { + GlobalNotificationService.AddErrorMessage($"An error occurred: {ex.Message}", true); + } + } + + /// + /// Delete the current email in AliasVault. + /// + private async Task DeleteEmailAliasVault() + { + if (Email == null) + { + return; + } + + try + { + var response = await HttpClient.DeleteAsync($"api/v1/Email/{Email.Id}"); + if (response.IsSuccessStatusCode) + { + GlobalNotificationService.AddSuccessMessage("Email deleted successfully", true); + await OnEmailDeleted.InvokeAsync(Email.Id); + await Close(); + } + else + { + var errorMessage = await response.Content.ReadAsStringAsync(); + GlobalNotificationService.AddErrorMessage($"Failed to delete email: {errorMessage}", true); + } + } + catch (Exception ex) + { + GlobalNotificationService.AddErrorMessage($"An error occurred: {ex.Message}", true); + } + } + /// protected override async Task OnInitializedAsync() { diff --git a/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor b/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor index 7e227c231..da56d8ee1 100644 --- a/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor +++ b/src/AliasVault.Client/Main/Components/Email/RecentEmails.razor @@ -14,7 +14,7 @@ @if (EmailModalVisible) { - + } @if (ShowComponent) diff --git a/src/AliasVault.Client/Main/Pages/Emails/Home.razor b/src/AliasVault.Client/Main/Pages/Emails/Home.razor index 7e8163b7c..13108fc3f 100644 --- a/src/AliasVault.Client/Main/Pages/Emails/Home.razor +++ b/src/AliasVault.Client/Main/Pages/Emails/Home.razor @@ -13,7 +13,7 @@ @if (EmailModalVisible) { - + }
@@ -44,7 +44,7 @@ else
-
+

Inbox

diff --git a/src/AliasVault.Client/Services/CredentialService.cs b/src/AliasVault.Client/Services/CredentialService.cs index 15326c704..0b2880be7 100644 --- a/src/AliasVault.Client/Services/CredentialService.cs +++ b/src/AliasVault.Client/Services/CredentialService.cs @@ -160,7 +160,7 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService login.Attachments.Add(attachment); } - await context.Credentials.AddAsync(login); + context.Credentials.Add(login); // Add password. login.Passwords.Add(loginObject.Passwords.First()); diff --git a/src/AliasVault.Client/Services/Database/DbService.cs b/src/AliasVault.Client/Services/Database/DbService.cs index 5bdfa4f75..072b2b3d6 100644 --- a/src/AliasVault.Client/Services/Database/DbService.cs +++ b/src/AliasVault.Client/Services/Database/DbService.cs @@ -526,7 +526,7 @@ public sealed class DbService : IDisposable CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now, }; - await _dbContext.EncryptionKeys.AddAsync(encryptionKey); + _dbContext.EncryptionKeys.Add(encryptionKey); return encryptionKey; } } diff --git a/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs b/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs index 3777b2955..b7e780300 100644 --- a/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs +++ b/src/Services/AliasVault.SmtpService/Handlers/DatabaseMessageStore.cs @@ -357,7 +357,7 @@ public class DatabaseMessageStore(ILogger logger, Config c newEmail = EmailEncryption.EncryptEmail(newEmail, userEncryptionKey); // Insert the email into the database. - await dbContext.Emails.AddAsync(newEmail); + dbContext.Emails.Add(newEmail); await dbContext.SaveChangesAsync(); return newEmail.Id; diff --git a/src/Utilities/AliasVault.AuthLogging/AuthLoggingService.cs b/src/Utilities/AliasVault.AuthLogging/AuthLoggingService.cs index c1fe881a1..bf0ba9c61 100644 --- a/src/Utilities/AliasVault.AuthLogging/AuthLoggingService.cs +++ b/src/Utilities/AliasVault.AuthLogging/AuthLoggingService.cs @@ -48,7 +48,7 @@ public class AuthLoggingService(IServiceProvider serviceProvider, IHttpContextAc IsSuspiciousActivity = false, }; - await dbContext.AuthLogs.AddAsync(authAttempt); + dbContext.AuthLogs.Add(authAttempt); await dbContext.SaveChangesAsync(); } @@ -82,7 +82,7 @@ public class AuthLoggingService(IServiceProvider serviceProvider, IHttpContextAc IsSuspiciousActivity = false, }; - await dbContext.AuthLogs.AddAsync(authAttempt); + dbContext.AuthLogs.Add(authAttempt); await dbContext.SaveChangesAsync(); } diff --git a/src/Utilities/AliasVault.WorkerStatus/StatusWorker.cs b/src/Utilities/AliasVault.WorkerStatus/StatusWorker.cs index 2535ebd2e..08255a37d 100644 --- a/src/Utilities/AliasVault.WorkerStatus/StatusWorker.cs +++ b/src/Utilities/AliasVault.WorkerStatus/StatusWorker.cs @@ -84,7 +84,7 @@ public class StatusWorker(ILogger logger, FuncNew current status. private async Task GetServiceStatus() { - var entry = await GetOrCreateInitialStatusRecord(); + var entry = GetOrCreateInitialStatusRecord(); if (!string.IsNullOrEmpty(entry.DesiredStatus) && entry.CurrentStatus != entry.DesiredStatus) { @@ -155,7 +155,7 @@ public class StatusWorker(ILogger logger, Func /// Retrieves status record or creates an initial status record if it does not exist. ///
- private async Task GetOrCreateInitialStatusRecord() + private WorkerServiceStatus GetOrCreateInitialStatusRecord() { var entry = _dbContext.WorkerServiceStatuses.FirstOrDefault(x => x.ServiceName == globalServiceStatus.ServiceName); if (entry != null) @@ -169,7 +169,7 @@ public class StatusWorker(ILogger logger, Func Date: Sat, 31 Aug 2024 17:51:45 +0200 Subject: [PATCH 3/4] Update E2E test for email delete button (#171) --- .../Main/Components/Email/EmailModal.razor | 4 ++-- .../Tests/Client/EmailDecryptionTests.cs | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/AliasVault.Client/Main/Components/Email/EmailModal.razor b/src/AliasVault.Client/Main/Components/Email/EmailModal.razor index 6347a3037..7a3acf778 100644 --- a/src/AliasVault.Client/Main/Components/Email/EmailModal.razor +++ b/src/AliasVault.Client/Main/Components/Email/EmailModal.razor @@ -36,8 +36,8 @@
- - + +
diff --git a/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTests.cs b/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTests.cs index 6909cfddc..1da33a210 100644 --- a/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTests.cs +++ b/src/Tests/AliasVault.E2ETests/Tests/Client/EmailDecryptionTests.cs @@ -46,12 +46,13 @@ public class EmailDecryptionTests : ClientPlaywrightTest } /// - /// Test if received email encrypted by server can be successfully decrypted by client. + /// Test if received email encrypted by server can be successfully decrypted by client + /// and then be deleted by client. /// /// Async task. [Test] [Order(1)] - public async Task EmailEncryptionDecryptionTest() + public async Task EmailEncryptionDecryptionDeleteTest() { // Create credential which should automatically create claim on server during database sync. const string serviceName = "Test Service"; @@ -103,6 +104,20 @@ public class EmailDecryptionTests : ClientPlaywrightTest // Check if the email is visible on the page now. emailContent = await Page.TextContentAsync("body"); Assert.That(emailContent, Does.Contain(textSubject), "Email not (correctly) decrypted and displayed on the emails page. Check email decryption logic."); + + // Attempt to click on the email subject to open the modal. + await Page.Locator("text=" + textSubject).First.ClickAsync(); + await WaitForUrlAsync("emails**", "Delete"); + + // Click the delete button to delete the email. + await Page.Locator("id=delete-email").First.ClickAsync(); + + // Wait for the email delete confirm message to show up. + await WaitForUrlAsync("emails**", "Email deleted successfully"); + + // Assert that the email is no longer visible on the page. + var body = await Page.TextContentAsync("body"); + Assert.That(body, Does.Not.Contain(textSubject), "Email not deleted from page after deletion. Check email deletion logic."); } /// From ca6aa40850b4eed8f3ea39628dccaf9c411c5a00 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 31 Aug 2024 19:09:07 +0200 Subject: [PATCH 4/4] Update EmailModal.razor (#171) --- src/AliasVault.Client/Main/Components/Email/EmailModal.razor | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/AliasVault.Client/Main/Components/Email/EmailModal.razor b/src/AliasVault.Client/Main/Components/Email/EmailModal.razor index 7a3acf778..ab6a89277 100644 --- a/src/AliasVault.Client/Main/Components/Email/EmailModal.razor +++ b/src/AliasVault.Client/Main/Components/Email/EmailModal.razor @@ -94,12 +94,10 @@ if (IsSpamOk) { - //await JsInteropService.Confirm("Are you sure you want to delete this email?", DeleteEmailAliasVault); await DeleteEmailSpamOk(); } else { - //await JsInteropService.Confirm("Are you sure you want to delete this email?", DeleteEmailAliasVault); await DeleteEmailAliasVault(); } }