mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-02-27 12:28:56 -05:00
Merge pull request #147 from lanedirt/137-improve-credential-email-generation-ui
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
API_URL=
|
||||
JWT_KEY=
|
||||
SMTP_ALLOWED_DOMAINS=
|
||||
PRIVATE_EMAIL_DOMAINS=
|
||||
SMTP_TLS_ENABLED=false
|
||||
|
||||
27
.github/workflows/dotnet-e2e-tests.yml
vendored
27
.github/workflows/dotnet-e2e-tests.yml
vendored
@@ -25,9 +25,24 @@ jobs:
|
||||
run: dotnet build
|
||||
- name: Ensure browsers are installed
|
||||
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install --with-deps
|
||||
- name: Run AdminTests
|
||||
run: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category=AdminTests"
|
||||
- name: Run ClientTests
|
||||
run: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category=ClientTests"
|
||||
- name: Run remaining tests
|
||||
run: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category!=AdminTests&Category!=ClientTests"
|
||||
|
||||
- name: Run AdminTests with retry
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category=AdminTests"
|
||||
|
||||
- name: Run ClientTests with retry
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category=ClientTests"
|
||||
|
||||
- name: Run remaining tests with retry
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: dotnet test src/Tests/AliasVault.E2ETests --no-build --verbosity normal --filter "Category!=AdminTests&Category!=ClientTests"
|
||||
|
||||
@@ -80,7 +80,7 @@ Here is an example file with the various options explained:
|
||||
```
|
||||
{
|
||||
"ApiUrl": "http://localhost:5092",
|
||||
"SmtpAllowedDomains": ["example.tld"],
|
||||
"PrivateEmailDomains": ["example.tld"],
|
||||
"UseDebugEncryptionKey": "true"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,11 +30,11 @@ This README provides step-by-step instructions for manually setting up AliasVaul
|
||||
```
|
||||
JWT_KEY=your_32_char_string_here
|
||||
|
||||
3. **Set SMTP_ALLOWED_DOMAINS**
|
||||
3. **Set PRIVATE_EMAIL_DOMAINS**
|
||||
|
||||
Update the .env file and set the SMTP_ALLOWED_DOMAINS value the allowed domains that can be used for email addresses. Separate multiple domains with commas.
|
||||
Update the .env file and set the PRIVATE_EMAIL_DOMAINS value the allowed domains that can be used for email addresses. Separate multiple domains with commas.
|
||||
```
|
||||
SMTP_ALLOWED_DOMAINS=yourdomain.com,anotherdomain.com
|
||||
PRIVATE_EMAIL_DOMAINS=yourdomain.com,anotherdomain.com
|
||||
```
|
||||
Replace `yourdomain.com,anotherdomain.com` with your actual allowed domains.
|
||||
|
||||
|
||||
34
install.sh
34
install.sh
@@ -170,33 +170,33 @@ populate_jwt_key() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to ask the user for SMTP_ALLOWED_DOMAINS
|
||||
set_smtp_allowed_domains() {
|
||||
printf "${CYAN}> Setting SMTP_ALLOWED_DOMAINS...${NC}\n"
|
||||
if ! grep -q "^SMTP_ALLOWED_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^SMTP_ALLOWED_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
# Function to ask the user for PRIVATE_EMAIL_DOMAINS
|
||||
set_private_email_domains() {
|
||||
printf "${CYAN}> Setting PRIVATE_EMAIL_DOMAINS...${NC}\n"
|
||||
if ! grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "Please enter the domains that should be allowed to receive email, separated by commas (press Enter to disable email support): "
|
||||
read -r smtp_allowed_domains
|
||||
read -r private_email_domains
|
||||
|
||||
# Set default value if user input is empty
|
||||
smtp_allowed_domains=${smtp_allowed_domains:-"DISABLED.TLD"}
|
||||
private_email_domains=${private_email_domains:-"DISABLED.TLD"}
|
||||
|
||||
if grep -q "^SMTP_ALLOWED_DOMAINS=" "$ENV_FILE"; then
|
||||
awk -v domains="$smtp_allowed_domains" '/^SMTP_ALLOWED_DOMAINS=/ {$0="SMTP_ALLOWED_DOMAINS="domains} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
if grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE"; then
|
||||
awk -v domains="$private_email_domains" '/^PRIVATE_EMAIL_DOMAINS=/ {$0="PRIVATE_EMAIL_DOMAINS="domains} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
else
|
||||
echo "SMTP_ALLOWED_DOMAINS=${smtp_allowed_domains}" >> "$ENV_FILE"
|
||||
echo "PRIVATE_EMAIL_DOMAINS=${private_email_domains}" >> "$ENV_FILE"
|
||||
fi
|
||||
|
||||
if [ "$smtp_allowed_domains" = "DISABLED.TLD" ]; then
|
||||
printf "${GREEN}> SMTP_ALLOWED_DOMAINS has been set to 'DISABLED.TLD' in $ENV_FILE.${NC} ${RED}SMTP is disabled.${NC}\n"
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf "${GREEN}> PRIVATE_EMAIL_DOMAINS has been set to 'DISABLED.TLD' in $ENV_FILE.${NC} ${RED}SMTP is disabled.${NC}\n"
|
||||
else
|
||||
printf "${GREEN}> SMTP_ALLOWED_DOMAINS has been set to '${smtp_allowed_domains}' in $ENV_FILE.${NC}\n"
|
||||
printf "${GREEN}> PRIVATE_EMAIL_DOMAINS has been set to '${private_email_domains}' in $ENV_FILE.${NC}\n"
|
||||
fi
|
||||
else
|
||||
smtp_allowed_domains=$(grep "^SMTP_ALLOWED_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
if [ "$smtp_allowed_domains" = "DISABLED.TLD" ]; then
|
||||
printf "${GREEN}> SMTP_ALLOWED_DOMAINS already exists in $ENV_FILE.${NC} ${RED}SMTP is disabled.${NC}\n"
|
||||
private_email_domains=$(grep "^private_email_domains=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf "${GREEN}> PRIVATE_EMAIL_DOMAINS already exists in $ENV_FILE.${NC} ${RED}SMTP is disabled.${NC}\n"
|
||||
else
|
||||
printf "${GREEN}> SMTP_ALLOWED_DOMAINS already exists in $ENV_FILE with value: ${smtp_allowed_domains}${NC}\n"
|
||||
printf "${GREEN}> PRIVATE_EMAIL_DOMAINS already exists in $ENV_FILE with value: ${private_email_domains}${NC}\n"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -316,7 +316,7 @@ main() {
|
||||
create_env_file || exit $?
|
||||
populate_api_url || exit $?
|
||||
populate_jwt_key || exit $?
|
||||
set_smtp_allowed_domains || exit $?
|
||||
set_private_email_domains || exit $?
|
||||
set_smtp_tls_enabled || exit $?
|
||||
generate_admin_password || exit $?
|
||||
printf "\n${YELLOW}+++ Building Docker containers +++${NC}\n"
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace AliasVault.Api.Controllers;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Api.Helpers;
|
||||
using AliasVault.Shared.Models.Spamok;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -41,30 +42,54 @@ public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContex
|
||||
|
||||
// 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 == to);
|
||||
.FirstOrDefaultAsync(x => x.Address == to);
|
||||
|
||||
if (emailClaim is null)
|
||||
{
|
||||
return Unauthorized("User does not have a claim to this email address.");
|
||||
return BadRequest(new ApiErrorResponse
|
||||
{
|
||||
Message = "No claim exists for this email address.",
|
||||
Code = "CLAIM_DOES_NOT_EXIST",
|
||||
Details = new { ProvidedEmail = to },
|
||||
StatusCode = StatusCodes.Status400BadRequest,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
});
|
||||
}
|
||||
|
||||
if (emailClaim.UserId != user.Id)
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse
|
||||
{
|
||||
Message = "Claim does not match user.",
|
||||
Code = "CLAIM_DOES_NOT_MATCH_USER",
|
||||
Details = new { ProvidedEmail = to },
|
||||
StatusCode = StatusCodes.Status400BadRequest,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
});
|
||||
}
|
||||
|
||||
// Retrieve emails from database.
|
||||
List<MailboxEmailApiModel> emails = await context.Emails.AsNoTracking().Select(x => new MailboxEmailApiModel()
|
||||
{
|
||||
Id = x.Id,
|
||||
Subject = x.Subject,
|
||||
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(x.From),
|
||||
FromDomain = x.FromDomain,
|
||||
FromLocal = x.FromLocal,
|
||||
ToDomain = x.ToDomain,
|
||||
ToLocal = x.ToLocal,
|
||||
Date = x.Date,
|
||||
DateSystem = x.DateSystem,
|
||||
SecondsAgo = (int)DateTime.UtcNow.Subtract(x.DateSystem).TotalSeconds,
|
||||
MessagePreview = x.MessagePreview ?? string.Empty,
|
||||
EncryptedSymmetricKey = x.EncryptedSymmetricKey,
|
||||
EncryptionKey = x.EncryptionKey.PublicKey,
|
||||
}).OrderByDescending(x => x.DateSystem).Take(75).ToListAsync();
|
||||
List<MailboxEmailApiModel> emails = await context.Emails.AsNoTracking()
|
||||
.Where(x => x.To == to)
|
||||
.Select(x => new MailboxEmailApiModel()
|
||||
{
|
||||
Id = x.Id,
|
||||
Subject = x.Subject,
|
||||
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(x.From),
|
||||
FromDomain = x.FromDomain,
|
||||
FromLocal = x.FromLocal,
|
||||
ToDomain = x.ToDomain,
|
||||
ToLocal = x.ToLocal,
|
||||
Date = x.Date,
|
||||
DateSystem = x.DateSystem,
|
||||
SecondsAgo = (int)DateTime.UtcNow.Subtract(x.DateSystem).TotalSeconds,
|
||||
MessagePreview = x.MessagePreview ?? string.Empty,
|
||||
EncryptedSymmetricKey = x.EncryptedSymmetricKey,
|
||||
EncryptionKey = x.EncryptionKey.PublicKey,
|
||||
})
|
||||
.OrderByDescending(x => x.DateSystem)
|
||||
.Take(50)
|
||||
.ToListAsync();
|
||||
|
||||
MailboxApiModel returnValue = new MailboxApiModel();
|
||||
returnValue.Address = to;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
namespace AliasVault.Api.Controllers;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Api.Helpers;
|
||||
using AliasVault.Api.Vault;
|
||||
@@ -20,11 +21,12 @@ using Microsoft.EntityFrameworkCore;
|
||||
/// <summary>
|
||||
/// Vault controller for handling CRUD operations on the database for encrypted vault entities.
|
||||
/// </summary>
|
||||
/// <param name="logger">ILogger instance.</param>
|
||||
/// <param name="dbContextFactory">DbContext instance.</param>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
/// <param name="timeProvider">ITimeProvider instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class VaultController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, ITimeProvider timeProvider) : AuthenticatedRequestController(userManager)
|
||||
public class VaultController(ILogger<VaultController> logger, IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, ITimeProvider timeProvider) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Default retention policy for vaults.
|
||||
@@ -149,17 +151,42 @@ public class VaultController(IDbContextFactory<AliasServerDbContext> dbContextFa
|
||||
// Register new email addresses.
|
||||
foreach (var email in newEmailAddresses)
|
||||
{
|
||||
// If email address is invalid according to the EmailAddressAttribute, skip it.
|
||||
if (!new EmailAddressAttribute().IsValid(email))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the email address is already claimed (by another user).
|
||||
var existingClaim = await context.UserEmailClaims
|
||||
.FirstOrDefaultAsync(x => x.Address == email);
|
||||
|
||||
if (existingClaim != null && existingClaim.UserId != userId)
|
||||
{
|
||||
// Email address is already claimed by another user. Log the error and continue.
|
||||
logger.LogWarning("{User} tried to claim email address: {Email} but it is already claimed by another user.", userId, email);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingEmailClaims.Contains(email))
|
||||
{
|
||||
await context.UserEmailClaims.AddAsync(new UserEmailClaim
|
||||
try
|
||||
{
|
||||
UserId = userId,
|
||||
Address = email,
|
||||
AddressLocal = email.Split('@')[0],
|
||||
AddressDomain = email.Split('@')[1],
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
});
|
||||
await context.UserEmailClaims.AddAsync(new UserEmailClaim
|
||||
{
|
||||
UserId = userId,
|
||||
Address = email,
|
||||
AddressLocal = email.Split('@')[0],
|
||||
AddressDomain = email.Split('@')[1],
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
});
|
||||
}
|
||||
catch (DbUpdateException ex)
|
||||
{
|
||||
// Error while adding email claim. Log the error and continue.
|
||||
logger.LogWarning(ex, "Error while adding UserEmailClaim with email: {Email} for user: {UserId}.", email, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,5 +23,22 @@ public class Config
|
||||
/// Email addresses that client vault users use will be registered at the server
|
||||
/// to get exclusive access to the email address.
|
||||
/// </summary>
|
||||
public List<string> SmtpAllowedDomains { get; set; } = [];
|
||||
public List<string> PrivateEmailDomains { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the public email domains that are allowed to be used by the client vault users.
|
||||
/// </summary>
|
||||
public List<string> PublicEmailDomains { get; set; } =
|
||||
[
|
||||
"spamok.com",
|
||||
"solarflarecorp.com",
|
||||
"spamok.nl",
|
||||
"3060.nl",
|
||||
"landmail.nl",
|
||||
"asdasd.nl",
|
||||
"spamok.de",
|
||||
"spamok.com.ua",
|
||||
"spamok.es",
|
||||
"spamok.fr",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
@using AliasVault.Shared.Models.Spamok
|
||||
@using System.Net
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Shared.Models.Spamok
|
||||
@using AliasVault.Shared.Models.WebApi
|
||||
@inherits ComponentBase
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
@inject HttpClient HttpClient
|
||||
@inject JsInteropService JsInteropService
|
||||
@inject DbService DbService
|
||||
@inject Config Config
|
||||
@using System.Timers
|
||||
@implements IDisposable
|
||||
|
||||
@if (EmailModalVisible)
|
||||
{
|
||||
@@ -15,10 +20,15 @@
|
||||
{
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex justify-between">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Email</h3>
|
||||
<button id="recent-email-refresh" @onclick="LoadRecentEmailsAsync" type="button" class="text-blue-700 border border-blue-700 hover:bg-blue-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:focus:ring-blue-800 dark:hover:bg-blue-500">
|
||||
Refresh
|
||||
</button>
|
||||
<div>
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Email</h3>
|
||||
</div>
|
||||
<div class="flex justify-end items-center space-x-2">
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="Auto-refresh enabled"></div>
|
||||
<button id="recent-email-refresh" @onclick="ManualRefresh" type="button" class="text-blue-700 border border-blue-700 hover:bg-blue-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:focus:ring-blue-800 dark:hover:bg-blue-500">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
@@ -80,11 +90,19 @@
|
||||
public string EmailAddress { get; set; } = string.Empty;
|
||||
|
||||
private List<MailboxEmailApiModel> MailboxEmails { get; set; } = new();
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private bool ShowComponent { get; set; } = false;
|
||||
private EmailApiModel Email { get; set; } = new();
|
||||
private bool EmailModalVisible { get; set; }
|
||||
private string Error { get; set; } = string.Empty;
|
||||
private Timer? RefreshTimer { get; set; }
|
||||
private bool IsRefreshing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Boolean for UI to indicate refreshing. This value is switched between false/true every 2 seconds.
|
||||
/// </summary>
|
||||
private bool RefreshTick { get; set; } = false;
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -96,6 +114,16 @@
|
||||
{
|
||||
ShowComponent = true;
|
||||
}
|
||||
|
||||
RefreshTimer = new Timer(2000);
|
||||
RefreshTimer.Elapsed += async (sender, e) => await TimerRefresh();
|
||||
RefreshTimer.Start();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
RefreshTimer?.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -110,7 +138,7 @@
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await LoadRecentEmailsAsync();
|
||||
await ManualRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,17 +147,7 @@
|
||||
/// </summary>
|
||||
private bool IsSpamOkDomain(string email)
|
||||
{
|
||||
return email.EndsWith("@spamok.nl") ||
|
||||
email.EndsWith("@spamok.de") ||
|
||||
email.EndsWith("@spamok.es") ||
|
||||
email.EndsWith("@spamok.fr") ||
|
||||
email.EndsWith("@spamok.com") ||
|
||||
email.EndsWith("@spamok.com.ua") ||
|
||||
email.EndsWith("@landmail.nl") ||
|
||||
email.EndsWith("@landmeel.nl") ||
|
||||
email.EndsWith("@asdasd.nl") ||
|
||||
email.EndsWith("@sdfsdf.nl") ||
|
||||
email.EndsWith("@solarflarecorp.com");
|
||||
return Config.PublicEmailDomains.Exists(x => email.EndsWith(x));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -137,7 +155,25 @@
|
||||
/// </summary>
|
||||
private bool IsAliasVaultDomain(string email)
|
||||
{
|
||||
return Config.SmtpAllowedDomains.Exists(x => email.EndsWith(x));
|
||||
return Config.PrivateEmailDomains.Exists(x => email.EndsWith(x));
|
||||
}
|
||||
|
||||
private async Task TimerRefresh()
|
||||
{
|
||||
IsRefreshing = true;
|
||||
StateHasChanged();
|
||||
await LoadRecentEmailsAsync();
|
||||
IsRefreshing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ManualRefresh()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
await LoadRecentEmailsAsync();
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadRecentEmailsAsync()
|
||||
@@ -148,7 +184,6 @@
|
||||
}
|
||||
|
||||
Error = string.Empty;
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Get email prefix, which is the part before the @ symbol.
|
||||
@@ -162,9 +197,6 @@
|
||||
{
|
||||
await LoadAliasVaultEmails();
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -175,7 +207,6 @@
|
||||
// Get email prefix, which is the part before the @ symbol.
|
||||
string emailPrefix = EmailAddress.Split('@')[0];
|
||||
|
||||
|
||||
if (IsSpamOkDomain(EmailAddress))
|
||||
{
|
||||
await ShowSpamOkEmailInModal(emailPrefix, emailId);
|
||||
@@ -223,31 +254,43 @@
|
||||
/// </summary>
|
||||
private async Task LoadAliasVaultEmails()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/EmailBox/{EmailAddress}");
|
||||
try
|
||||
{
|
||||
var mailbox = await HttpClient.GetFromJsonAsync<MailboxApiModel>($"api/v1/EmailBox/{EmailAddress}");
|
||||
if (mailbox?.Mails != null)
|
||||
var response = await HttpClient.SendAsync(request);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// Show maximum of 10 recent emails.
|
||||
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
||||
var mailbox = await response.Content.ReadFromJsonAsync<MailboxApiModel>();
|
||||
await UpdateMailboxEmails(mailbox);
|
||||
}
|
||||
|
||||
// Loop through emails and decrypt the subject locally.
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
var privateKeys = await context.EncryptionKeys.ToListAsync();
|
||||
foreach (var mail in MailboxEmails)
|
||||
else
|
||||
{
|
||||
var privateKey = privateKeys.First(x => x.PublicKey == mail.EncryptionKey);
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
var errorResponse = JsonSerializer.Deserialize<ApiErrorResponse>(errorContent);
|
||||
switch (response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.BadRequest:
|
||||
if (errorResponse != null)
|
||||
{
|
||||
switch (errorResponse.Code)
|
||||
{
|
||||
case "CLAIM_DOES_NOT_MATCH_USER":
|
||||
Error = "The current chosen email address is already in use. Please change the email address by editing this credential.";
|
||||
break;
|
||||
case "CLAIM_DOES_NOT_EXIST":
|
||||
Error = "An error occurred while trying to load the emails. Please try to edit and" +
|
||||
"save the credential entry to synchronize the database, then again.";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(errorResponse.Message);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
Console.WriteLine(ex);
|
||||
break;
|
||||
case HttpStatusCode.Unauthorized:
|
||||
throw new UnauthorizedAccessException(errorResponse?.Message);
|
||||
default:
|
||||
throw new WebException(errorResponse?.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,6 +301,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the mailbox emails and decrypt the subject locally.
|
||||
/// </summary>
|
||||
private async Task UpdateMailboxEmails(MailboxApiModel? mailbox)
|
||||
{
|
||||
if (mailbox?.Mails != null)
|
||||
{
|
||||
// Show maximum of 10 recent emails.
|
||||
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
||||
}
|
||||
|
||||
// Loop through emails and decrypt the subject locally.
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
var privateKeys = await context.EncryptionKeys.ToListAsync();
|
||||
foreach (var mail in MailboxEmails)
|
||||
{
|
||||
var privateKey = privateKeys.First(x => x.PublicKey == mail.EncryptionKey);
|
||||
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load recent emails from AliasVault.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
@inject JsInteropService JsInteropService
|
||||
@implements IDisposable
|
||||
|
||||
<label for="@_inputId" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="relative">
|
||||
<input type="@(_isPasswordVisible ? "text" : "password")" id="@_inputId" class="outline-0 shadow-sm bg-gray-50 border @(_copied ? "border-green-500 border-2" : "border-gray-300") text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-20 dark:bg-gray-700 dark:border-@(_copied ? "green-500" : "gray-600") dark:placeholder-gray-400 dark:text-white" value="@Value" @onclick="CopyToClipboard" readonly>
|
||||
<button type="button" class="absolute inset-y-1 right-1 flex items-center justify-center w-10 h-8 text-gray-500 bg-gray-200 rounded-md shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors duration-200 dark:bg-gray-600 dark:hover:bg-gray-500 dark:text-gray-300" @onclick="TogglePasswordVisibility">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
@if (_isPasswordVisible)
|
||||
{
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
}
|
||||
else
|
||||
{
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
|
||||
}
|
||||
</svg>
|
||||
</button>
|
||||
@if (_copied)
|
||||
{
|
||||
<span class="absolute inset-y-0 right-10 flex items-center pr-3 text-green-500 dark:text-green-400">
|
||||
Copied!
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The label for the input.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "Password";
|
||||
|
||||
/// <summary>
|
||||
/// The value to copy to the clipboard.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
private bool _copied => ClipboardCopyService.GetCopiedId() == _inputId;
|
||||
private readonly string _inputId = Guid.NewGuid().ToString();
|
||||
private bool _isPasswordVisible = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ClipboardCopyService.OnCopy += HandleCopy;
|
||||
}
|
||||
|
||||
private async Task CopyToClipboard()
|
||||
{
|
||||
await JsInteropService.CopyToClipboard(Value);
|
||||
ClipboardCopyService.SetCopied(_inputId);
|
||||
|
||||
// After 2 seconds, reset the copied state if it's still the same element
|
||||
await Task.Delay(2000);
|
||||
if (ClipboardCopyService.GetCopiedId() == _inputId)
|
||||
{
|
||||
ClipboardCopyService.SetCopied(string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void TogglePasswordVisibility()
|
||||
{
|
||||
_isPasswordVisible = !_isPasswordVisible;
|
||||
}
|
||||
|
||||
private void HandleCopy(string copiedElementId)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
ClipboardCopyService.OnCopy -= HandleCopy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
@inject Config Config
|
||||
@inject JsInteropService JsInteropService
|
||||
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="relative">
|
||||
<div class="flex">
|
||||
<input type="text" id="@Id" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@LocalPart" @oninput="OnLocalPartChanged">
|
||||
@if (!IsCustomDomain)
|
||||
{
|
||||
<span class="inline-flex items-center p-2.5 text-sm text-gray-900 bg-gray-200 border border-l-0 border-gray-300 rounded-r-lg dark:bg-gray-600 dark:text-gray-400 dark:border-gray-600 cursor-pointer" @onclick="TogglePopup">
|
||||
<span class="text-gray-500">@@</span>@SelectedDomain
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
@if (IsCustomDomain)
|
||||
{
|
||||
<button type="button" class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" @onclick="TogglePopup">
|
||||
Use domain chooser
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="button" class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200" @onclick="ToggleCustomDomain">
|
||||
Enter custom domain
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (IsPopupVisible)
|
||||
{
|
||||
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 z-30 overflow-y-auto h-full w-full" @onclick="ClosePopup">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-gray-800" @onclick:stopPropagation>
|
||||
<div class="mt-3 text-center">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Select Email Domain</h3>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
@if (ShowPrivateDomains)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300">Private Email (AliasVault server)</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">E2E encrypted, fully private.</p>
|
||||
@foreach (var domain in PrivateDomains)
|
||||
{
|
||||
<button class="mt-2 px-4 py-2 bg-primary-300 text-gray-700 rounded hover:bg-primary-400 focus:outline-none focus:ring-2 focus:ring-gray-400 mr-2" @onclick="() => SelectDomain(domain)">
|
||||
@domain
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="@(ShowPrivateDomains ? "border-t border-gray-200 dark:border-gray-600 pt-4" : "")">
|
||||
<h4 class="text-md font-semibold text-gray-700 dark:text-gray-300">Public Temp Email Providers</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Anonymous but limited privacy. Accessible to anyone that knows the address.</p>
|
||||
@foreach (var domain in PublicDomains)
|
||||
{
|
||||
<button class="mt-2 px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400 mr-2" @onclick="() => SelectDomain(domain)">
|
||||
@domain
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The id for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The label for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "Email Address";
|
||||
|
||||
/// <summary>
|
||||
/// The value of the input field. This should be the full email address.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> ValueChanged { get; set; }
|
||||
|
||||
private bool IsCustomDomain { get; set; } = false;
|
||||
private string LocalPart { get; set; } = string.Empty;
|
||||
private string SelectedDomain = string.Empty;
|
||||
private bool IsPopupVisible = false;
|
||||
|
||||
private List<string> PrivateDomains => Config.PrivateEmailDomains;
|
||||
private List<string> PublicDomains => Config.PublicEmailDomains;
|
||||
|
||||
private bool ShowPrivateDomains => PrivateDomains.Count > 0 && !(PrivateDomains.Count == 1 && PrivateDomains[0] == "DISABLED.TLD");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
IsCustomDomain = !PublicDomains.Contains(SelectedDomain) && !PrivateDomains.Contains(SelectedDomain);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
if (Value.Contains('@'))
|
||||
{
|
||||
SelectedDomain = Value.Split('@')[1];
|
||||
}
|
||||
else if (ShowPrivateDomains)
|
||||
{
|
||||
SelectedDomain = PrivateDomains[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedDomain = PublicDomains[0];
|
||||
}
|
||||
|
||||
IsCustomDomain = !PublicDomains.Contains(SelectedDomain) && !PrivateDomains.Contains(SelectedDomain);
|
||||
if (IsCustomDomain)
|
||||
{
|
||||
LocalPart = Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
LocalPart = Value.Contains('@') ? Value.Split('@')[0] : Value;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnLocalPartChanged(ChangeEventArgs e)
|
||||
{
|
||||
string newLocalPart = e.Value?.ToString() ?? string.Empty;
|
||||
|
||||
// Check if new value contains '@' symbol, if so, switch to custom domain mode.
|
||||
if (newLocalPart.Contains('@'))
|
||||
{
|
||||
IsCustomDomain = true;
|
||||
Value = newLocalPart;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
return;
|
||||
}
|
||||
|
||||
Value = $"{newLocalPart}@{SelectedDomain}";
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
private void TogglePopup()
|
||||
{
|
||||
IsPopupVisible = !IsPopupVisible;
|
||||
}
|
||||
|
||||
private void ClosePopup()
|
||||
{
|
||||
IsPopupVisible = false;
|
||||
}
|
||||
|
||||
private async Task SelectDomain(string domain)
|
||||
{
|
||||
// Remove the '@' symbol and everything after if it exists.
|
||||
LocalPart = LocalPart.Contains('@') ? LocalPart.Split('@')[0] : LocalPart;
|
||||
Value = $"{LocalPart}@{domain}";
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
IsCustomDomain = false;
|
||||
ClosePopup();
|
||||
}
|
||||
|
||||
private void ToggleCustomDomain()
|
||||
{
|
||||
IsCustomDomain = !IsCustomDomain;
|
||||
if (!IsCustomDomain && !Value.Contains('@'))
|
||||
{
|
||||
Value = $"{Value}@{(ShowPrivateDomains ? PrivateDomains[0] : PublicDomains[0])}";
|
||||
ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="relative">
|
||||
@if (Type == "textarea")
|
||||
{
|
||||
|
||||
@@ -92,7 +92,7 @@ else
|
||||
</div>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="email" Label="Email" @bind-Value="Obj.Alias.Email"></EditFormRow>
|
||||
<EditEmailFormRow Id="email" Label="Email" @bind-Value="Obj.Alias.Email"></EditEmailFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="username" Label="Username" @bind-Value="Obj.Username"></EditFormRow>
|
||||
@@ -100,7 +100,7 @@ else
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<div class="relative">
|
||||
<EditFormRow Id="password" Label="Password" @bind-Value="Obj.Password.Value"></EditFormRow>
|
||||
<button type="submit" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
|
||||
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,6 +240,7 @@ else
|
||||
// Create new Obj
|
||||
var alias = new Credential();
|
||||
alias.Alias = new Alias();
|
||||
alias.Alias.Email = string.Empty;
|
||||
alias.Service = new Service();
|
||||
alias.Passwords = new List<Password> { new Password() };
|
||||
|
||||
@@ -280,7 +281,24 @@ else
|
||||
Obj.Alias.AddressZipCode = identity.Address.ZipCode;
|
||||
Obj.Alias.AddressCountry = identity.Address.Country;
|
||||
Obj.Alias.Hobbies = identity.Hobbies[0];
|
||||
Obj.Alias.Email = identity.EmailPrefix + "@landmail.nl";
|
||||
|
||||
var defaultDomain = Config.PrivateEmailDomains[0];
|
||||
if (defaultDomain == "DISABLED.TLD")
|
||||
{
|
||||
if (Config.PublicEmailDomains.Count == 0)
|
||||
{
|
||||
Obj.Alias.Email = identity.EmailPrefix + "@example.com";
|
||||
}
|
||||
else
|
||||
{
|
||||
Obj.Alias.Email = identity.EmailPrefix + "@" + Config.PublicEmailDomains[0];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Obj.Alias.Email = identity.EmailPrefix + "@" + defaultDomain;
|
||||
}
|
||||
|
||||
Obj.Alias.PhoneMobile = identity.PhoneMobile;
|
||||
Obj.Alias.BankAccountIBAN = identity.BankAccountIBAN;
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ else
|
||||
<CopyPasteFormRow Label="Username" Value="@(Alias.Username)"></CopyPasteFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="Password" Value="@(Alias.Passwords.FirstOrDefault()?.Value ?? string.Empty)"></CopyPasteFormRow>
|
||||
<CopyPastePasswordFormRow Label="Password" Value="@(Alias.Passwords.FirstOrDefault()?.Value ?? string.Empty)"></CopyPastePasswordFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -66,6 +66,12 @@ public class MainBase : OwningComponentBase
|
||||
[Inject]
|
||||
public AuthService AuthService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Config instance with values from appsettings.json.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public Config Config { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LocalStorage.
|
||||
/// </summary>
|
||||
|
||||
@@ -23,9 +23,9 @@ if (string.IsNullOrEmpty(config.ApiUrl))
|
||||
throw new KeyNotFoundException("ApiUrl is not set in the configuration.");
|
||||
}
|
||||
|
||||
if (config.SmtpAllowedDomains == null || config.SmtpAllowedDomains.Count == 0)
|
||||
if (config.PrivateEmailDomains == null || config.PrivateEmailDomains.Count == 0)
|
||||
{
|
||||
throw new KeyNotFoundException("SmtpAllowedDomains is not set in the configuration.");
|
||||
throw new KeyNotFoundException("PrivateEmailDomains is not set in the configuration.");
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton(config);
|
||||
|
||||
@@ -458,11 +458,23 @@ public class DbService : IDisposable
|
||||
.Select(email => email!)
|
||||
.ToListAsync();
|
||||
|
||||
Console.WriteLine("Before filtering email addresses:");
|
||||
foreach (var email in emailAddresses)
|
||||
{
|
||||
Console.WriteLine(email);
|
||||
}
|
||||
|
||||
// Filter the list of email addresses to only include those that are in the allowed domains.
|
||||
emailAddresses = emailAddresses
|
||||
.Where(email => _config.SmtpAllowedDomains.Exists(domain => email.EndsWith(domain)))
|
||||
.Where(email => _config.PrivateEmailDomains.Exists(domain => email.EndsWith(domain)))
|
||||
.ToList();
|
||||
|
||||
Console.WriteLine("After filtering email addresses:");
|
||||
foreach (var email in emailAddresses)
|
||||
{
|
||||
Console.WriteLine(email);
|
||||
}
|
||||
|
||||
var databaseVersion = await GetCurrentDatabaseVersionAsync();
|
||||
var vaultObject = new Vault(encryptedDatabase, databaseVersion, publicEncryptionKey, emailAddresses, DateTime.Now, DateTime.Now);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/sh
|
||||
# Set the default API URL for localhost debugging
|
||||
DEFAULT_API_URL="http://localhost:81"
|
||||
DEFAULT_SMTP_ALLOWED_DOMAINS="localmail.tld"
|
||||
DEFAULT_PRIVATE_EMAIL_DOMAINS="localmail.tld"
|
||||
|
||||
# Use the provided API_URL environment variable if it exists, otherwise use the default
|
||||
API_URL=${API_URL:-$DEFAULT_API_URL}
|
||||
SMTP_ALLOWED_DOMAINS=${SMTP_ALLOWED_DOMAINS:-$DEFAULT_SMTP_ALLOWED_DOMAINS}
|
||||
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS}
|
||||
|
||||
# Replace the default URL with the actual API URL
|
||||
sed -i "s|http://localhost:5092|${API_URL}|g" /usr/share/nginx/html/appsettings.json
|
||||
@@ -14,10 +14,10 @@ sed -i "s|http://localhost:5092|${API_URL}|g" /usr/share/nginx/html/appsettings.
|
||||
# in order to be able to receive emails.
|
||||
|
||||
# Convert comma-separated list to JSON array
|
||||
json_array=$(echo $SMTP_ALLOWED_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
|
||||
json_array=$(echo $PRIVATE_EMAIL_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
|
||||
|
||||
# Use sed to update the SmtpAllowedDomains field in appsettings.json
|
||||
sed -i.bak "s|\"SmtpAllowedDomains\": \[.*\]|\"SmtpAllowedDomains\": $json_array|" /usr/share/nginx/html/appsettings.json
|
||||
# Use sed to update the PrivateEmailDomains field in appsettings.json
|
||||
sed -i.bak "s|\"PrivateEmailDomains\": \[.*\]|\"PrivateEmailDomains\": $json_array|" /usr/share/nginx/html/appsettings.json
|
||||
|
||||
# Start the application
|
||||
nginx -g "daemon off;"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"ApiUrl": "http://localhost:5092",
|
||||
"SmtpAllowedDomains": ["example.tld"]
|
||||
"PrivateEmailDomains": ["example.tld"]
|
||||
}
|
||||
|
||||
@@ -566,6 +566,10 @@ video {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -591,6 +595,11 @@ video {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.inset-y-1 {
|
||||
top: 0.25rem;
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bottom-1 {
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
@@ -603,10 +612,30 @@ video {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.right-10 {
|
||||
right: 2.5rem;
|
||||
}
|
||||
|
||||
.top-10 {
|
||||
top: 2.5rem;
|
||||
}
|
||||
|
||||
.right-4 {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.right-1 {
|
||||
right: 0.25rem;
|
||||
}
|
||||
|
||||
.right-5 {
|
||||
right: 1.25rem;
|
||||
}
|
||||
|
||||
.top-20 {
|
||||
top: 5rem;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -619,6 +648,10 @@ video {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.z-20 {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
@@ -731,6 +764,14 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@@ -803,6 +844,10 @@ video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -851,6 +896,14 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.min-w-full {
|
||||
min-width: 100%;
|
||||
}
|
||||
@@ -885,6 +938,43 @@ video {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-ping {
|
||||
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(-25%);
|
||||
animation-timing-function: cubic-bezier(0.8,0,1,1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0,0,0.2,1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1001,6 +1091,12 @@ video {
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-y-reverse: 0;
|
||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||
@@ -1053,6 +1149,10 @@ video {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.rounded-l-lg {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
@@ -1075,6 +1175,14 @@ video {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-l-0 {
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-blue-700 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(29 78 216 / var(--tw-border-opacity));
|
||||
@@ -1095,6 +1203,21 @@ video {
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(252 165 165 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-primary-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(248 185 99 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-primary-100 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(253 222 133 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
@@ -1120,6 +1243,11 @@ video {
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
|
||||
@@ -1145,11 +1273,6 @@ video {
|
||||
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-200 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(251 203 116 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
@@ -1175,9 +1298,19 @@ video {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-500 {
|
||||
.bg-gray-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-300 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(248 185 99 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
@@ -1212,6 +1345,10 @@ video {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -1267,10 +1404,19 @@ video {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.px-7 {
|
||||
padding-left: 1.75rem;
|
||||
padding-right: 1.75rem;
|
||||
}
|
||||
|
||||
.pr-10 {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.pr-20 {
|
||||
padding-right: 5rem;
|
||||
}
|
||||
|
||||
.pr-3 {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
@@ -1287,6 +1433,10 @@ video {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1565,6 +1715,16 @@ video {
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-300:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-primary-400:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(246 167 82 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-500:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||
@@ -1595,6 +1755,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -1615,6 +1780,12 @@ video {
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-2:focus {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-blue-300:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
|
||||
@@ -1650,6 +1821,20 @@ video {
|
||||
--tw-ring-color: rgb(252 165 165 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-indigo-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-400:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-offset-2:focus {
|
||||
--tw-ring-offset-width: 2px;
|
||||
}
|
||||
|
||||
.dark\:divide-gray-600:is(.dark *) > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-divide-opacity: 1;
|
||||
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
|
||||
@@ -1823,6 +2008,11 @@ video {
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-500:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-primary-500:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(244 149 65 / var(--tw-text-opacity));
|
||||
@@ -1833,6 +2023,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-blue-200:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(191 219 254 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-primary-500:focus:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
|
||||
51
src/AliasVault.Shared/Models/WebApi/ApiErrorResponse.cs
Normal file
51
src/AliasVault.Shared/Models/WebApi/ApiErrorResponse.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ApiErrorResponse.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the error response returned by the API.
|
||||
/// </summary>
|
||||
public class ApiErrorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the main error message.
|
||||
/// </summary>
|
||||
/// <value>A string containing a brief description of the error.</value>
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error code associated with this error.
|
||||
/// </summary>
|
||||
/// <value>A string representing a unique identifier for this type of error.</value>
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets additional details about the error.
|
||||
/// </summary>
|
||||
/// <value>An object containing any additional information about the error.</value>
|
||||
[JsonPropertyName("details")]
|
||||
public object Details { get; set; } = new { };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP status code associated with this error.
|
||||
/// </summary>
|
||||
/// <value>An integer representing the HTTP status code.</value>
|
||||
[JsonPropertyName("statusCode")]
|
||||
public int StatusCode { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp when the error occurred.
|
||||
/// </summary>
|
||||
/// <value>A DateTime representing when the error was generated.</value>
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -26,8 +26,8 @@ builder.Services.ConfigureLogging(builder.Configuration, Assembly.GetExecutingAs
|
||||
|
||||
// Create global config object, get values from environment variables.
|
||||
Config config = new Config();
|
||||
var emailDomains = Environment.GetEnvironmentVariable("SMTP_ALLOWED_DOMAINS")
|
||||
?? throw new KeyNotFoundException("SMTP_ALLOWED_DOMAINS environment variable is not set.");
|
||||
var emailDomains = Environment.GetEnvironmentVariable("PRIVATE_EMAIL_DOMAINS")
|
||||
?? throw new KeyNotFoundException("PRIVATE_EMAIL_DOMAINS environment variable is not set.");
|
||||
config.AllowedToDomains = emailDomains.Split(',').ToList();
|
||||
|
||||
var tlsEnabled = Environment.GetEnvironmentVariable("SMTP_TLS_ENABLED")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"SMTP_ALLOWED_DOMAINS": "example.tld",
|
||||
"PRIVATE_EMAIL_DOMAINS": "example.tld",
|
||||
"SMTP_TLS_ENABLED": "false"
|
||||
},
|
||||
"dotnetRunMessages": true
|
||||
|
||||
@@ -89,7 +89,7 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
await SetupPlaywrightBrowserAndContext();
|
||||
|
||||
// Intercept Blazor WASM app requests to override appsettings.json
|
||||
string[] smtpAllowedDomains = ["example.tld"];
|
||||
string[] privateEmailDomains = ["example.tld"];
|
||||
|
||||
await Context.RouteAsync(
|
||||
"**/appsettings.json",
|
||||
@@ -98,7 +98,7 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
var response = new
|
||||
{
|
||||
ApiUrl = ApiBaseUrl.TrimEnd('/'),
|
||||
SmtpAllowedDomains = smtpAllowedDomains,
|
||||
PrivateEmailDomains = privateEmailDomains,
|
||||
};
|
||||
await route.FulfillAsync(
|
||||
new RouteFulfillOptions
|
||||
@@ -114,7 +114,7 @@ public class ClientPlaywrightTest : PlaywrightTest
|
||||
var response = new
|
||||
{
|
||||
ApiUrl = ApiBaseUrl.TrimEnd('/'),
|
||||
SmtpAllowedDomains = smtpAllowedDomains,
|
||||
PrivateEmailDomains = privateEmailDomains,
|
||||
};
|
||||
await route.FulfillAsync(
|
||||
new RouteFulfillOptions
|
||||
|
||||
Reference in New Issue
Block a user