mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-17 21:05:48 -04:00
Merge branch 'main' into 188-add-email-anchor-tags-translation-to-open-in-new-tabs-instead-of-iframe
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
@if (EmailModalVisible)
|
||||
{
|
||||
<EmailModal Email="@Email" IsSpamOk="@IsSpamOk" OnClose="CloseEmailModal" />
|
||||
<EmailModal Email="@Email" IsSpamOk="@IsSpamOk" OnClose="CloseEmailModal" OnEmailDeleted="ManualRefresh" />
|
||||
}
|
||||
|
||||
@if (ShowComponent)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user