Merge branch 'main' into 188-add-email-anchor-tags-translation-to-open-in-new-tabs-instead-of-iframe

This commit is contained in:
Leendert de Borst
2024-08-31 19:18:59 +02:00
committed by GitHub
13 changed files with 219 additions and 57 deletions

View File

@@ -240,7 +240,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> 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<AliasServerDbContext> 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,

View File

@@ -19,10 +19,11 @@ using Microsoft.EntityFrameworkCore;
/// <summary>
/// Email controller for retrieving emails from the database.
/// </summary>
/// <param name="logger">ILogger instance.</param>
/// <param name="dbContextFactory">DbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
[ApiVersion("1")]
public class EmailController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
public class EmailController(ILogger<VaultController> logger, IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Get the newest version of the vault for the current user.
@@ -34,31 +35,15 @@ public class EmailController(IDbContextFactory<AliasServerDbContext> 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,
FromDomain = email.FromDomain,
FromLocal = email.FromLocal,
@@ -87,4 +72,75 @@ public class EmailController(IDbContextFactory<AliasServerDbContext> dbContextFa
return Ok(returnEmail);
}
/// <summary>
/// Deletes an email for the current user.
/// </summary>
/// <param name="id">The email ID to delete.</param>
/// <returns>A response indicating the success or failure of the deletion.</returns>
[HttpDelete(template: "{id}", Name = "DeleteEmail")]
public async Task<IActionResult> 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}");
}
}
/// <summary>
/// Authenticates the user and retrieves the requested email.
/// </summary>
/// <param name="id">The email ID to retrieve.</param>
/// <param name="context">The database context.</param>
/// <returns>A tuple containing the authenticated user, the email, and an IActionResult if there's an error.</returns>
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);
}
}

View File

@@ -116,7 +116,7 @@ public class VaultController(ILogger<VaultController> 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<VaultController> logger, IDbContextFactory<
{
try
{
await context.UserEmailClaims.AddAsync(new UserEmailClaim
context.UserEmailClaims.Add(new UserEmailClaim
{
UserId = userId,
Address = sanitizedEmail,

View File

@@ -1,6 +1,9 @@
@using AliasVault.Shared.Models.Spamok
@using AliasVault.Shared.Utilities
@inject JsInteropService JsInteropService
@inject GlobalNotificationService GlobalNotificationService
@inject IHttpClientFactory HttpClientFactory
@inject HttpClient HttpClient
<ClickOutsideHandler OnClose="OnClose" ContentId="emailModal">
<div class="fixed inset-0 z-50 overflow-auto bg-gray-500 bg-opacity-75 flex items-center justify-center">
@@ -33,8 +36,9 @@
</iframe>
</div>
</div>
<div class="mt-6 flex justify-end">
<button @onclick="Close" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Close</button>
<div class="mt-6 flex justify-end space-x-4">
<button id="delete-email" @onclick="DeleteEmail" class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">Delete</button>
<button id="close-email-modal" @onclick="Close" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Close</button>
</div>
</div>
</div>
@@ -59,6 +63,12 @@
[Parameter]
public EventCallback<bool> OnClose { get; set; }
/// <summary>
/// Callback when an email is deleted.
/// </summary>
[Parameter]
public EventCallback<int> OnEmailDeleted { get; set; }
/// <summary>
/// The message body to display
/// </summary>
@@ -73,6 +83,89 @@
return OnClose.InvokeAsync(false);
}
/// <summary>
/// Delete the current email.
/// </summary>
private async Task DeleteEmail()
{
if (Email == null)
{
return;
}
if (IsSpamOk)
{
await DeleteEmailSpamOk();
}
else
{
await DeleteEmailAliasVault();
}
}
/// <summary>
/// Delete the current email in SpamOk.
/// </summary>
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);
}
}
/// <summary>
/// Delete the current email in AliasVault.
/// </summary>
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);
}
}
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{

View File

@@ -14,7 +14,7 @@
@if (EmailModalVisible)
{
<EmailModal Email="@Email" IsSpamOk="@IsSpamOk" OnClose="CloseEmailModal" />
<EmailModal Email="@Email" IsSpamOk="@IsSpamOk" OnClose="CloseEmailModal" OnEmailDeleted="ManualRefresh" />
}
@if (ShowComponent)

View File

@@ -13,7 +13,7 @@
@if (EmailModalVisible)
{
<EmailModal Email="EmailModalEmail" IsSpamOk="false" OnClose="CloseEmailModal" />
<EmailModal Email="EmailModalEmail" IsSpamOk="false" OnClose="CloseEmailModal" OnEmailDeleted="RefreshData" />
}
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
@@ -44,7 +44,7 @@ else
<div class="overflow-x-auto px-4">
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged"/>
<div class="bg-white shadow rounded-lg overflow-hidden mt-6">
<div class="bg-white border rounded-lg overflow-hidden mt-6">
<div class="px-4 py-2 bg-gray-100 border-b">
<h2 class="font-semibold text-gray-800">Inbox</h2>
</div>

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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);
</script>
<script src="lib/qrcode.min.js?v=@CacheBuster"></script>
<script src="js/crypto.js?v=@CacheBuster"></script>

View File

@@ -357,7 +357,7 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> 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;

View File

@@ -46,12 +46,13 @@ public class EmailDecryptionTests : ClientPlaywrightTest
}
/// <summary>
/// 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.
/// </summary>
/// <returns>Async task.</returns>
[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";
@@ -124,6 +125,16 @@ public class EmailDecryptionTests : ClientPlaywrightTest
// Assert that the anchor tag in the email iframe has target="_blank" attribute.
var anchorTag = await Page.Locator("iframe").First.GetAttributeAsync("srcdoc");
Assert.That(anchorTag, Does.Contain("target=\"_blank\""), "Anchor tag in email iframe does not have target=\"_blank\" attribute. Check email decryption logic.");
// 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.");
}
/// <summary>

View File

@@ -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();
}

View File

@@ -84,7 +84,7 @@ public class StatusWorker(ILogger<StatusWorker> logger, Func<IWorkerStatusDbCont
/// <returns>New current status.</returns>
private async Task<WorkerServiceStatus> 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<StatusWorker> logger, Func<IWorkerStatusDbCont
/// <summary>
/// Retrieves status record or creates an initial status record if it does not exist.
/// </summary>
private async Task<WorkerServiceStatus> 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<StatusWorker> logger, Func<IWorkerStatusDbCont
CurrentStatus = Status.Started.ToString(),
DesiredStatus = string.Empty,
};
await _dbContext.WorkerServiceStatuses.AddAsync(entry);
_dbContext.WorkerServiceStatuses.Add(entry);
return entry;
}