mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-30 17:48:18 -05:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cfc8d528d | ||
|
|
7a4e1721c8 | ||
|
|
11d79c4874 | ||
|
|
7cd35b0a92 | ||
|
|
d0f62a26c0 | ||
|
|
01198502a3 | ||
|
|
229ad109a7 | ||
|
|
837b16d971 | ||
|
|
4010d1b93f | ||
|
|
f7ce60ae68 | ||
|
|
5e61bd5db2 | ||
|
|
a2e8a438de | ||
|
|
92904dcf55 | ||
|
|
e4f2ca630b | ||
|
|
ed80ad24c1 | ||
|
|
0c368ab84b | ||
|
|
dee2044ed6 | ||
|
|
f6f6072b3f | ||
|
|
4bfe72d750 | ||
|
|
330f59dc10 | ||
|
|
a20d981427 | ||
|
|
bd2274db75 | ||
|
|
6cfa6f4ef5 | ||
|
|
8a40d2b1b9 | ||
|
|
237958ba0f | ||
|
|
79db3a54c7 | ||
|
|
2029745f8b | ||
|
|
ea4d498502 | ||
|
|
05838f5dca | ||
|
|
79872163e2 | ||
|
|
35d0f77dd6 | ||
|
|
6660cd20bd | ||
|
|
e236ba454f | ||
|
|
6ec66e4d64 | ||
|
|
14898c0c83 | ||
|
|
d08bec9df7 | ||
|
|
9107dfa789 | ||
|
|
351f6f4d16 |
25
.github/workflows/docker-compose-pull.yml
vendored
25
.github/workflows/docker-compose-pull.yml
vendored
@@ -20,8 +20,16 @@ jobs:
|
||||
- name: Get repository and branch information
|
||||
id: repo-info
|
||||
run: |
|
||||
echo "REPO_FULL_NAME=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
# Check if this is a PR from a fork
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
|
||||
# If PR is from a fork, use main branch from lanedirt/AliasVault
|
||||
echo "REPO_FULL_NAME=lanedirt/AliasVault" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=main" >> $GITHUB_ENV
|
||||
else
|
||||
# Otherwise use the current repository and branch
|
||||
echo "REPO_FULL_NAME=${GITHUB_REPOSITORY}" >> $GITHUB_ENV
|
||||
echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Download install script from current branch
|
||||
run: |
|
||||
@@ -34,10 +42,23 @@ jobs:
|
||||
echo "SMTP_PORT=2525" > .env
|
||||
|
||||
- name: Set permissions and run install.sh
|
||||
id: install_script
|
||||
continue-on-error: true
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
./install.sh install --verbose
|
||||
|
||||
- name: Check if failure was due to version mismatch
|
||||
if: steps.install_script.outcome == 'failure'
|
||||
run: |
|
||||
if grep -q "Install script needs updating to match version" <<< "$(./install.sh install --verbose 2>&1)"; then
|
||||
echo "Test skipped: Install script version is newer than latest release version. This is expected behavior if the install script is run on a branch that is ahead of the latest release."
|
||||
exit 0
|
||||
else
|
||||
echo "Test failed due to an unexpected error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up Docker Compose
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
|
||||
|
||||
14
.github/workflows/publish-docker-images.yml
vendored
14
.github/workflows/publish-docker-images.yml
vendored
@@ -21,6 +21,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
run: |
|
||||
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
|
||||
@@ -43,6 +49,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Databases/AliasServerDb/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:${{ github.ref_name }}
|
||||
|
||||
@@ -51,6 +58,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Api/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:${{ github.ref_name }}
|
||||
|
||||
@@ -59,6 +67,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Client/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:${{ github.ref_name }}
|
||||
|
||||
@@ -67,6 +76,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/AliasVault.Admin/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:${{ github.ref_name }}
|
||||
|
||||
@@ -75,6 +85,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:${{ github.ref_name }}
|
||||
|
||||
@@ -83,6 +94,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Services/AliasVault.SmtpService/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:${{ github.ref_name }}
|
||||
|
||||
@@ -91,6 +103,7 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Services/AliasVault.TaskRunner/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:${{ github.ref_name }}
|
||||
|
||||
@@ -99,5 +112,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: src/Utilities/AliasVault.InstallCli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
|
||||
23
.github/workflows/sonarcloud-code-analysis.yml
vendored
23
.github/workflows/sonarcloud-code-analysis.yml
vendored
@@ -1,10 +1,13 @@
|
||||
# This workflow will perform a SonarCloud code analysis on every push to the main branch or when a pull request is opened, synchronized, or reopened.
|
||||
# This workflow will perform a SonarCloud code analysis on every push to the main branch or
|
||||
# when a pull request is opened, synchronized, or reopened. The "pull_request_target" event is
|
||||
# used to ensure that the analysis is done on the source branch of the pull request which has
|
||||
# access to the SonarCloud token secret.
|
||||
name: SonarCloud code analysis
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,11 +26,13 @@ jobs:
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu' # Alternative distribution options are available.
|
||||
distribution: 'zulu'
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout code of PR branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
@@ -57,7 +62,11 @@ jobs:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs"
|
||||
if ('${{ github.event_name }}' -eq 'pull_request_target') {
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs"
|
||||
} else {
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs"
|
||||
}
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
|
||||
|
||||
28
install.sh
28
install.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# @version 0.10.1
|
||||
# @version 0.10.3
|
||||
|
||||
# Repository information used for downloading files and images from GitHub
|
||||
REPO_OWNER="lanedirt"
|
||||
@@ -511,8 +511,9 @@ generate_admin_password() {
|
||||
printf "${CYAN}> Generating admin password...${NC}\n"
|
||||
PASSWORD=$(openssl rand -base64 12)
|
||||
|
||||
if ! docker pull ${GITHUB_CONTAINER_REGISTRY}-installcli:latest > /dev/null 2>&1; then
|
||||
printf "${YELLOW}> Pre-built image not found, building locally...${NC}"
|
||||
# Build locally if in build mode or if pre-built image is not available
|
||||
if grep -q "^DEPLOYMENT_MODE=build" "$ENV_FILE" 2>/dev/null || ! docker pull ${GITHUB_CONTAINER_REGISTRY}-installcli:latest > /dev/null 2>&1; then
|
||||
printf "${CYAN}> Building InstallCli locally...${NC}"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile .
|
||||
else
|
||||
@@ -533,23 +534,18 @@ generate_admin_password() {
|
||||
)
|
||||
fi
|
||||
HASH=$(docker run --rm installcli hash-password "$PASSWORD")
|
||||
if [ -z "$HASH" ]; then
|
||||
printf "${RED}> Error: Failed to generate password hash${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
HASH=$(docker run --rm ${GITHUB_CONTAINER_REGISTRY}-installcli:latest hash-password "$PASSWORD")
|
||||
if [ -z "$HASH" ]; then
|
||||
printf "${RED}> Error: Failed to generate password hash${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$HASH" ]; then
|
||||
update_env_var "ADMIN_PASSWORD_HASH" "$HASH"
|
||||
update_env_var "ADMIN_PASSWORD_GENERATED" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
printf " ==> New admin password: $PASSWORD\n"
|
||||
if [ -z "$HASH" ]; then
|
||||
printf "${RED}> Error: Failed to generate password hash${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
update_env_var "ADMIN_PASSWORD_HASH" "$HASH"
|
||||
update_env_var "ADMIN_PASSWORD_GENERATED" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
printf " ==> New admin password: $PASSWORD\n"
|
||||
}
|
||||
|
||||
# Function to set default ports
|
||||
@@ -1755,7 +1751,7 @@ handle_migrate_db() {
|
||||
printf "${CYAN}> Stopping services to ensure database is not in use...${NC}\n"
|
||||
docker compose stop api admin task-runner smtp
|
||||
|
||||
if ! docker pull ${GITHUB_CONTAINER_REGISTRY}-installcli:0.10.0 > /dev/null 2>&1; then
|
||||
if ! docker pull ${GITHUB_CONTAINER_REGISTRY}-installcli:0.10.3 > /dev/null 2>&1; then
|
||||
printf "${YELLOW}> Pre-built image not found, building locally...${NC}"
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile .
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
WORKDIR /app
|
||||
EXPOSE 3002
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["src/AliasVault.Admin/AliasVault.Admin.csproj", "src/AliasVault.Admin/"]
|
||||
RUN dotnet restore "src/AliasVault.Admin/AliasVault.Admin.csproj"
|
||||
RUN dotnet restore "src/AliasVault.Admin/AliasVault.Admin.csproj" -a "$TARGETARCH"
|
||||
COPY . .
|
||||
|
||||
WORKDIR "/src/src/AliasVault.Admin"
|
||||
RUN dotnet publish "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" \
|
||||
-a "$TARGETARCH" \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\Generators\AliasVault.Generators.Identity\AliasVault.Generators.Identity.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared.Server\AliasVault.Shared.Server.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared\AliasVault.Shared.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.Auth\AliasVault.Auth.csproj" />
|
||||
|
||||
@@ -105,10 +105,10 @@ public class EmailBoxController(IAliasServerDbContextFactory dbContextFactory, U
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of emails for the provided email address.
|
||||
/// Returns a list of emails for the provided list of email addresses.
|
||||
/// </summary>
|
||||
/// <param name="model">The request model extracted from POST body.</param>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
/// <returns>List of emails in JSON format.</returns>
|
||||
[HttpPost(template: "bulk", Name = "GetEmailBoxBulk")]
|
||||
public async Task<IActionResult> GetEmailBoxBulk([FromBody] MailboxBulkRequest model)
|
||||
{
|
||||
@@ -154,6 +154,7 @@ public class EmailBoxController(IAliasServerDbContextFactory dbContextFactory, U
|
||||
MessagePreview = x.MessagePreview ?? string.Empty,
|
||||
EncryptedSymmetricKey = x.EncryptedSymmetricKey,
|
||||
EncryptionKey = x.EncryptionKey.PublicKey,
|
||||
HasAttachments = x.Attachments.Any(),
|
||||
})
|
||||
.OrderByDescending(x => x.DateSystem)
|
||||
.Skip((model.Page - 1) * model.PageSize)
|
||||
|
||||
@@ -25,7 +25,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
public class EmailController(ILogger<VaultController> logger, IAliasServerDbContextFactory dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the newest version of the vault for the current user.
|
||||
/// Get the email with the specified ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The email ID to open.</param>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
@@ -105,6 +105,36 @@ public class EmailController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the attachment bytes for the specified email and attachment ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The email ID.</param>
|
||||
/// <param name="attachmentId">The attachment ID.</param>
|
||||
/// <returns>Attachment bytes in encrypted form.</returns>
|
||||
[HttpGet(template: "{id}/attachments/{attachmentId}", Name = "GetEmailAttachment")]
|
||||
public async Task<IActionResult> GetEmailAttachment(int id, int attachmentId)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var (email, errorResult) = await AuthenticateAndRetrieveEmailAsync(id, context);
|
||||
if (errorResult != null)
|
||||
{
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
// Find the requested attachment
|
||||
var attachment = await context.EmailAttachments
|
||||
.FirstOrDefaultAsync(x => x.Id == attachmentId && x.EmailId == email!.Id);
|
||||
|
||||
if (attachment == null)
|
||||
{
|
||||
return NotFound("Attachment not found.");
|
||||
}
|
||||
|
||||
// Return the encrypted bytes
|
||||
return File(attachment.Bytes, attachment.MimeType, attachment.Filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates the user and retrieves the requested email.
|
||||
/// </summary>
|
||||
|
||||
58
src/AliasVault.Api/Controllers/IdentityController.cs
Normal file
58
src/AliasVault.Api/Controllers/IdentityController.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IdentityController.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.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Api.Controllers.Abstracts;
|
||||
using AliasVault.Api.Helpers;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for generating identities taking into account existing information on the AliasVault server.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
/// <param name="dbContextFactory">DbContextFactory instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class IdentityController(UserManager<AliasVaultUser> userManager, IAliasServerDbContextFactory dbContextFactory) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify that provided email address is not already taken by another user.
|
||||
/// </summary>
|
||||
/// <param name="email">The full email address to check.</param>
|
||||
/// <returns>True if the email address is already taken, false otherwise.</returns>
|
||||
[HttpPost("CheckEmail/{email}")]
|
||||
public async Task<IActionResult> CheckEmail(string email)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
bool isTaken = await EmailClaimExistsAsync(email);
|
||||
return Ok(new { isTaken });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that provided email address is not already taken by another user.
|
||||
/// </summary>
|
||||
/// <param name="email">The email address to check.</param>
|
||||
/// <returns>True if the email address is already taken, false otherwise.</returns>
|
||||
private async Task<bool> EmailClaimExistsAsync(string email)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var sanitizedEmail = EmailHelper.SanitizeEmail(email);
|
||||
var claimExists = await context.UserEmailClaims.FirstOrDefaultAsync(c => c.Address == sanitizedEmail);
|
||||
|
||||
return claimExists != null;
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
// Update user email claims if email addresses have been supplied.
|
||||
if (model.EmailAddressList.Count > 0)
|
||||
{
|
||||
await UpdateUserEmailClaims(context, user.Id, model.EmailAddressList);
|
||||
await UpdateUserEmailClaims(context, user, model.EmailAddressList);
|
||||
}
|
||||
|
||||
// Sync user public key if supplied.
|
||||
@@ -371,14 +371,14 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
/// Updates the user's email claims based on the provided email address list.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context.</param>
|
||||
/// <param name="userId">The ID of the user.</param>
|
||||
/// <param name="user">The user object.</param>
|
||||
/// <param name="newEmailAddresses">The list of new email addresses to claim.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
private async Task UpdateUserEmailClaims(AliasServerDbContext context, string userId, List<string> newEmailAddresses)
|
||||
private async Task UpdateUserEmailClaims(AliasServerDbContext context, AliasVaultUser user, List<string> newEmailAddresses)
|
||||
{
|
||||
// Get all existing user email claims.
|
||||
var existingEmailClaims = await context.UserEmailClaims
|
||||
.Where(x => x.UserId == userId)
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.Select(x => x.Address)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -386,7 +386,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
foreach (var email in newEmailAddresses)
|
||||
{
|
||||
// Sanitize email address.
|
||||
var sanitizedEmail = email.Trim().ToLower();
|
||||
var sanitizedEmail = EmailHelper.SanitizeEmail(email);
|
||||
|
||||
// If email address is invalid according to the EmailAddressAttribute, skip it.
|
||||
if (!new EmailAddressAttribute().IsValid(sanitizedEmail))
|
||||
@@ -398,10 +398,10 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
var existingClaim = await context.UserEmailClaims
|
||||
.FirstOrDefaultAsync(x => x.Address == sanitizedEmail);
|
||||
|
||||
if (existingClaim != null && existingClaim.UserId != userId)
|
||||
if (existingClaim != null && existingClaim.UserId != user.Id)
|
||||
{
|
||||
// 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, sanitizedEmail);
|
||||
logger.LogWarning("{User} tried to claim email address: {Email} but it is already claimed by another user.", user.UserName, sanitizedEmail);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
{
|
||||
context.UserEmailClaims.Add(new UserEmailClaim
|
||||
{
|
||||
UserId = userId,
|
||||
UserId = user.Id,
|
||||
Address = sanitizedEmail,
|
||||
AddressLocal = sanitizedEmail.Split('@')[0],
|
||||
AddressDomain = sanitizedEmail.Split('@')[1],
|
||||
@@ -422,7 +422,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
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}.", sanitizedEmail, userId);
|
||||
logger.LogWarning(ex, "Error while adding UserEmailClaim with email: {Email} for user: {UserId}.", sanitizedEmail, user.UserName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 3001
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
# Copy the project files and restore dependencies
|
||||
COPY ["src/AliasVault.Api/AliasVault.Api.csproj", "src/AliasVault.Api/"]
|
||||
RUN dotnet restore "src/AliasVault.Api/AliasVault.Api.csproj"
|
||||
RUN dotnet restore "src/AliasVault.Api/AliasVault.Api.csproj" -a "$TARGETARCH"
|
||||
COPY . .
|
||||
|
||||
# Build and publish
|
||||
WORKDIR "/src/src/AliasVault.Api"
|
||||
RUN dotnet publish "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" \
|
||||
-a "$TARGETARCH" \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
# Final stage
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
|
||||
24
src/AliasVault.Api/Helpers/EmailHelper.cs
Normal file
24
src/AliasVault.Api/Helpers/EmailHelper.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailHelper.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.Api.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// EmailHelper class which contains helper methods for email.
|
||||
/// </summary>
|
||||
public static class EmailHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Sanitize email address by trimming and converting to lowercase.
|
||||
/// </summary>
|
||||
/// <param name="email">Email address to sanitize.</param>
|
||||
/// <returns>Sanitized email address.</returns>
|
||||
public static string SanitizeEmail(string email)
|
||||
{
|
||||
return email.Trim().ToLower();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
ENV MSBUILDDEBUGPATH=/src/msbuild-logs
|
||||
@@ -12,26 +13,29 @@ RUN mkdir -p /src/msbuild-logs
|
||||
|
||||
# Install Python which is required by the WebAssembly tools
|
||||
RUN apt-get update && apt-get install -y python3 && apt-get clean
|
||||
# Create the debug directory and install Python which is required by the WebAssembly tools
|
||||
RUN mkdir -p /src/msbuild-logs && apt-get update && apt-get install -y python3 && apt-get clean
|
||||
|
||||
# Install the WebAssembly tools
|
||||
RUN dotnet workload install wasm-tools
|
||||
|
||||
# Copy the project files and restore dependencies
|
||||
COPY ["src/AliasVault.Client/AliasVault.Client.csproj", "src/AliasVault.Client/"]
|
||||
RUN dotnet restore "src/AliasVault.Client/AliasVault.Client.csproj"
|
||||
RUN dotnet restore "src/AliasVault.Client/AliasVault.Client.csproj" -a "$TARGETARCH"
|
||||
COPY . .
|
||||
|
||||
# Build the Client project
|
||||
WORKDIR "/src/src/AliasVault.Client"
|
||||
RUN dotnet build "AliasVault.Client.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
|
||||
RUN dotnet build "AliasVault.Client.csproj" \
|
||||
-c "$BUILD_CONFIGURATION" \
|
||||
-o /app/build \
|
||||
-a "$TARGETARCH"
|
||||
|
||||
# Publish the Client project
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
ARG TARGETARCH
|
||||
RUN dotnet publish "AliasVault.Client.csproj" \
|
||||
-c "$BUILD_CONFIGURATION" \
|
||||
-a "$TARGETARCH" \
|
||||
--no-restore \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false \
|
||||
|
||||
@@ -3,42 +3,72 @@
|
||||
@inject JsInteropService JsInteropService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
@inject EmailService EmailService
|
||||
@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">
|
||||
<div id="emailModal" class="relative p-8 bg-white w-3/4 flex-col flex rounded-lg shadow-xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
@if (IsSpamOk)
|
||||
{
|
||||
<a target="_blank" href="https://spamok.com/@(Email!.ToLocal)/@(Email!.Id)">@Email.Subject</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Email?.Subject</span>
|
||||
}
|
||||
</h2>
|
||||
<button @onclick="Close" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">From: @(Email?.FromLocal)@@@(Email?.FromDomain)</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">To: @(Email?.ToLocal)@@@(Email?.ToDomain)</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Date: @Email?.DateSystem</p>
|
||||
</div>
|
||||
<div class="mt-4 text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<iframe class="w-full overscroll-y-auto" style="height:500px;" srcdoc="@EmailBody">
|
||||
</iframe>
|
||||
<div id="emailModal" class="relative bg-white w-3/4 flex flex-col rounded-lg shadow-xl max-h-[90vh]">
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
@if (IsSpamOk)
|
||||
{
|
||||
<a target="_blank" href="https://spamok.com/@(Email!.ToLocal)/@(Email!.Id)">@Email.Subject</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Email?.Subject</span>
|
||||
}
|
||||
</h2>
|
||||
<button @onclick="Close" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">From: @(Email?.FromLocal)@@@(Email?.FromDomain)</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">To: @(Email?.ToLocal)@@@(Email?.ToDomain)</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Date: @Email?.DateSystem</p>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div class="text-gray-700 dark:text-gray-300">
|
||||
<div>
|
||||
<iframe class="w-full overscroll-y-auto" style="height:500px;" srcdoc="@EmailBody">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
@if (Email?.Attachments?.Any() == true)
|
||||
{
|
||||
<div class="border-t border-gray-200 dark:border-gray-600 pt-4">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Attachments:</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
@foreach (var attachment in Email.Attachments)
|
||||
{
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
|
||||
</svg>
|
||||
<button @onclick="() => DownloadAttachment(attachment)"
|
||||
class="text-primary hover:underline text-sm truncate attachment-link">
|
||||
(@(Math.Ceiling((double)attachment.Filesize / 1024)) KB) @attachment.Filename
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
@@ -192,4 +222,56 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download an attachment.
|
||||
/// </summary>
|
||||
private async Task DownloadAttachment(AttachmentApiModel attachment)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsSpamOk)
|
||||
{
|
||||
var client = HttpClientFactory.CreateClient("EmailClient");
|
||||
var response = await client.GetAsync($"https://api.spamok.com/v2/Attachment/{Email!.Id}/{attachment.Id}/download");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
await JsInteropService.DownloadFileFromStream(attachment.Filename, bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Failed to download attachment", true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = await HttpClient.GetAsync($"v1/Email/{Email!.Id}/attachments/{attachment.Id}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// Get attachment bytes from API.
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
// Decrypt the attachment locally with email's encryption key.
|
||||
var decryptedBytes = await EmailService.DecryptEmailAttachment(Email, bytes);
|
||||
|
||||
// Offer the decrypted attachment as download to the user's browser.
|
||||
if (decryptedBytes != null)
|
||||
{
|
||||
await JsInteropService.DownloadFileFromStream(attachment.Filename, decryptedBytes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Failed to download attachment", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage($"Error downloading attachment: {ex.Message}", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
Subject
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Date & Time
|
||||
Date
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@using AliasVault.Shared.Core
|
||||
@implements IDisposable
|
||||
|
||||
<footer class="relative lg:fixed bottom-0 left-0 right-0 dark:bg-gray-900 @(ShowBorder ? "border-t border-gray-200 dark:border-gray-700" : "")">
|
||||
<footer class="relative -z-10 lg:fixed bottom-0 left-0 right-0 dark:bg-gray-900 @(ShowBorder ? "border-t border-gray-200 dark:border-gray-700" : "")">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<div class="flex flex-col lg:flex-row justify-between items-center">
|
||||
<p class="text-sm text-center text-gray-500 mb-4 lg:mb-0">
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<ConfirmModal />
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator" />
|
||||
<TopMenu />
|
||||
<div class="flex pt-16 mb-4 lg:mb-16 overflow-hidden bg-gray-100 dark:bg-gray-900 relative z-20">
|
||||
<div id="main-content" class="relative z-10 w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-100 dark:bg-gray-900">
|
||||
<div class="flex pt-16 mb-4 lg:mb-16 overflow-hidden bg-gray-100 dark:bg-gray-900 relative">
|
||||
<div id="main-content" class="relative w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-100 dark:bg-gray-900">
|
||||
<main>
|
||||
<GlobalNotificationDisplay />
|
||||
@Body
|
||||
|
||||
@@ -27,8 +27,8 @@ else
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
<div class="grid grid-cols-2 px-4 pt-6 md:grid-cols-3 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="col-span-full lg:col-auto">
|
||||
<div class="grid grid-cols-1 px-4 pt-6 md:grid-cols-2 lg:grid-cols-3 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-1">
|
||||
<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="items-center flex space-x-4">
|
||||
<DisplayFavicon FaviconBytes="@Alias.Service.Logo" />
|
||||
@@ -53,7 +53,7 @@ else
|
||||
<AttachmentViewer Attachments="@Alias.Attachments" />
|
||||
}
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<div class="col-span-1 md:col-span-2 lg:col-span-2">
|
||||
<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">
|
||||
<h3 class="mb-2 text-xl font-semibold dark:text-white">Login credentials</h3>
|
||||
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
|
||||
@@ -66,8 +66,14 @@ else
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center justify-between mb-2 mr-4">
|
||||
<div>
|
||||
<div class="text-gray-800 dark:text-gray-200 mb-2">
|
||||
<div class="text-gray-800 dark:text-gray-200 mb-2 flex items-center">
|
||||
@email.Subject
|
||||
@if (email.HasAttachments)
|
||||
{
|
||||
<svg class="attachment-indicator w-4 h-4 ml-2 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-100 line-clamp-2">
|
||||
@email.MessagePreview
|
||||
@@ -231,7 +237,8 @@ else
|
||||
Subject = email.Subject,
|
||||
MessagePreview = email.MessagePreview,
|
||||
CredentialId = credentialInfo.Id,
|
||||
CredentialName = credentialInfo.ServiceName
|
||||
CredentialName = credentialInfo.ServiceName,
|
||||
HasAttachments = email.HasAttachments,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
|
||||
@@ -56,4 +56,9 @@ public class MailListViewModel
|
||||
/// Gets or sets the message preview.
|
||||
/// </summary>
|
||||
public string MessagePreview { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the email has attachments.
|
||||
/// </summary>
|
||||
public bool HasAttachments { get; set; }
|
||||
}
|
||||
|
||||
@@ -49,20 +49,43 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
/// <returns>Task.</returns>
|
||||
public async Task<Credential> GenerateRandomIdentity(Credential credential)
|
||||
{
|
||||
// Generate a random identity using the IIdentityGenerator implementation.
|
||||
var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(dbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
|
||||
const int MaxAttempts = 5;
|
||||
var attempts = 0;
|
||||
bool isEmailTaken;
|
||||
|
||||
// Generate random values for the Identity properties
|
||||
credential.Username = identity.NickName;
|
||||
credential.Alias.FirstName = identity.FirstName;
|
||||
credential.Alias.LastName = identity.LastName;
|
||||
credential.Alias.NickName = identity.NickName;
|
||||
credential.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female";
|
||||
credential.Alias.BirthDate = identity.BirthDate;
|
||||
do
|
||||
{
|
||||
// Generate a random identity using the IIdentityGenerator implementation
|
||||
var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(dbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
|
||||
|
||||
// Set the email
|
||||
var emailDomain = GetDefaultEmailDomain();
|
||||
credential.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}";
|
||||
// Generate random values for the Identity properties
|
||||
credential.Username = identity.NickName;
|
||||
credential.Alias.FirstName = identity.FirstName;
|
||||
credential.Alias.LastName = identity.LastName;
|
||||
credential.Alias.NickName = identity.NickName;
|
||||
credential.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female";
|
||||
credential.Alias.BirthDate = identity.BirthDate;
|
||||
|
||||
// Set the email
|
||||
var emailDomain = GetDefaultEmailDomain();
|
||||
credential.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}";
|
||||
|
||||
// Check if email is already taken
|
||||
try
|
||||
{
|
||||
var response = await httpClient.PostAsync($"v1/Identity/CheckEmail/{credential.Alias.Email}", null);
|
||||
var result = await response.Content.ReadFromJsonAsync<Dictionary<string, bool>>();
|
||||
isEmailTaken = result?["isTaken"] ?? false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If the API call fails, assume email is not taken to allow operation to continue
|
||||
isEmailTaken = false;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
}
|
||||
while (isEmailTaken && attempts < MaxAttempts);
|
||||
|
||||
// Generate password
|
||||
credential.Passwords.First().Value = GenerateRandomPassword();
|
||||
|
||||
@@ -61,6 +61,31 @@ public sealed class EmailService(DbService dbService, JsInteropService jsInterop
|
||||
return emailList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts an email attachment using the email's encryption key.
|
||||
/// </summary>
|
||||
/// <param name="email">The email containing the encryption information.</param>
|
||||
/// <param name="encryptedBytes">The encrypted attachment bytes.</param>
|
||||
/// <returns>Decrypted attachment bytes.</returns>
|
||||
public async Task<byte[]?> DecryptEmailAttachment(EmailApiModel email, byte[] encryptedBytes)
|
||||
{
|
||||
await EnsureEncryptionKeys();
|
||||
var privateKey = _encryptionKeys.First(x => x.PublicKey == email.EncryptionKey);
|
||||
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
var decryptedBase64 = await jsInteropService.SymmetricDecryptBytes(encryptedBytes, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
return decryptedBase64;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
globalNotificationService.AddErrorMessage(ex.Message, true);
|
||||
logger.LogError(ex, "Error decrypting email attachment.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt the contents of a single email.
|
||||
/// </summary>
|
||||
|
||||
@@ -248,6 +248,23 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
|
||||
where TComponent : class =>
|
||||
await jsRuntime.InvokeVoidAsync("window.registerVisibilityCallback", objRef);
|
||||
|
||||
/// <summary>
|
||||
/// Symmetrically decrypts a byte array using the provided encryption key.
|
||||
/// </summary>
|
||||
/// <param name="cipherBytes">Cipher bytes to decrypt.</param>
|
||||
/// <param name="encryptionKey">Encryption key to use.</param>
|
||||
/// <returns>Decrypted bytes.</returns>
|
||||
public async Task<byte[]> SymmetricDecryptBytes(byte[] cipherBytes, string encryptionKey)
|
||||
{
|
||||
if (cipherBytes == null || cipherBytes.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var base64Ciphertext = Convert.ToBase64String(cipherBytes);
|
||||
return await jsRuntime.InvokeAsync<byte[]>("cryptoInterop.decryptBytes", base64Ciphertext, encryptionKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a WebAuthn get credential operation.
|
||||
/// </summary>
|
||||
|
||||
@@ -690,6 +690,10 @@ video {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.-z-10 {
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
.col-span-1 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
@@ -936,6 +940,10 @@ video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.max-h-\[90vh\] {
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -1028,6 +1036,10 @@ video {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -2280,6 +2292,11 @@ video {
|
||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-primary-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-primary-700:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(184 112 47 / var(--tw-border-opacity));
|
||||
@@ -2300,11 +2317,6 @@ video {
|
||||
border-color: rgb(234 179 8 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-primary-500:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(244 149 65 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-800:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
@@ -2644,10 +2656,6 @@ video {
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.sm\:-top-2 {
|
||||
top: -0.5rem;
|
||||
}
|
||||
@@ -2680,6 +2688,10 @@ video {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sm\:flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -2718,6 +2730,10 @@ video {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
|
||||
.md\:col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.md\:ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* AES (symmetric) encryption and decryption functions.
|
||||
* @type {{encrypt: (function(*, *): Promise<string>), decrypt: (function(*, *): Promise<string>)}}
|
||||
* @type {{encrypt: (function(*, *): Promise<string>), decrypt: (function(*, *): Promise<string>), decryptBytes: (function(*, *): Promise<Uint8Array>)}}
|
||||
*/
|
||||
window.cryptoInterop = {
|
||||
encrypt: async function (plaintext, base64Key) {
|
||||
@@ -59,6 +59,30 @@ window.cryptoInterop = {
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(decrypted);
|
||||
},
|
||||
decryptBytes: async function (base64Ciphertext, base64Key) {
|
||||
const key = await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)),
|
||||
{
|
||||
name: "AES-GCM",
|
||||
length: 256,
|
||||
},
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
|
||||
const ivAndCiphertext = Uint8Array.from(atob(base64Ciphertext), c => c.charCodeAt(0));
|
||||
const iv = ivAndCiphertext.slice(0, 12);
|
||||
const ciphertext = ivAndCiphertext.slice(12);
|
||||
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
return new Uint8Array(decrypted);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ public class UserEmailClaim
|
||||
public string Address { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email adress local part.
|
||||
/// Gets or sets the email address local part.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string AddressLocal { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email adress domain part.
|
||||
/// Gets or sets the email address domain part.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string AddressDomain { get; set; } = null!;
|
||||
|
||||
@@ -151,3 +151,109 @@ Juliana
|
||||
Charlie
|
||||
Lucia
|
||||
Stella
|
||||
Adriana
|
||||
Beatrice
|
||||
Bianca
|
||||
Calliope
|
||||
Carmen
|
||||
Celeste
|
||||
Dakota
|
||||
Diana
|
||||
Esther
|
||||
Florence
|
||||
Francesca
|
||||
Georgia
|
||||
Harlow
|
||||
Haven
|
||||
Holly
|
||||
Hope
|
||||
India
|
||||
Indie
|
||||
Iris
|
||||
Juniper
|
||||
Kaia
|
||||
Keira
|
||||
Lara
|
||||
Laura
|
||||
Laurel
|
||||
Luna
|
||||
Magnolia
|
||||
Maeve
|
||||
Marina
|
||||
Marlowe
|
||||
Nina
|
||||
Noelle
|
||||
Octavia
|
||||
Olive
|
||||
Ophelia
|
||||
Phoenix
|
||||
Poppy
|
||||
Primrose
|
||||
Ramona
|
||||
River
|
||||
Rosalie
|
||||
Rosemary
|
||||
Sage
|
||||
Salem
|
||||
Selena
|
||||
Sienna
|
||||
Summer
|
||||
Sylvie
|
||||
Thea
|
||||
Tessa
|
||||
Wren
|
||||
Winter
|
||||
Willa
|
||||
Ada
|
||||
Aspen
|
||||
Blair
|
||||
Brynn
|
||||
Cassidy
|
||||
Cecilia
|
||||
Daisy
|
||||
Dawn
|
||||
Daphne
|
||||
Ember
|
||||
Fiona
|
||||
Flora
|
||||
Freya
|
||||
Gemma
|
||||
Giselle
|
||||
Harmony
|
||||
Heidi
|
||||
Imogen
|
||||
Indie
|
||||
Jessie
|
||||
June
|
||||
Kaia
|
||||
Lena
|
||||
Lola
|
||||
Mabel
|
||||
Maisie
|
||||
Margot
|
||||
Matilda
|
||||
Mira
|
||||
Morgan
|
||||
Nell
|
||||
Nadia
|
||||
Odette
|
||||
Opal
|
||||
Pearl
|
||||
Phoebe
|
||||
Raven
|
||||
Reese
|
||||
Robin
|
||||
Rowan
|
||||
Ruth
|
||||
Sabrina
|
||||
Sasha
|
||||
Sierra
|
||||
Skye
|
||||
Sloane
|
||||
Talia
|
||||
Thora
|
||||
Vera
|
||||
Willa
|
||||
Winnie
|
||||
Yara
|
||||
Zara
|
||||
|
||||
@@ -140,3 +140,108 @@ Levi
|
||||
Alan
|
||||
Jorge
|
||||
Carson
|
||||
Felix
|
||||
Oliver
|
||||
Theodore
|
||||
Harrison
|
||||
Maxwell
|
||||
Sebastian
|
||||
Xavier
|
||||
Dominick
|
||||
Lincoln
|
||||
Elliott
|
||||
Walter
|
||||
Simon
|
||||
Dean
|
||||
Hugo
|
||||
Malcolm
|
||||
Leon
|
||||
Oscar
|
||||
Calvin
|
||||
Raymond
|
||||
Edgar
|
||||
Franklin
|
||||
Arthur
|
||||
Lawrence
|
||||
Dennis
|
||||
Russell
|
||||
Douglas
|
||||
Leonard
|
||||
Gregory
|
||||
Harold
|
||||
Frederick
|
||||
Martin
|
||||
Curtis
|
||||
Stanley
|
||||
Gilbert
|
||||
Harvey
|
||||
Francis
|
||||
Eugene
|
||||
Ralph
|
||||
Roy
|
||||
Albert
|
||||
Bruce
|
||||
Ronald
|
||||
Keith
|
||||
Craig
|
||||
Roger
|
||||
Randy
|
||||
Gary
|
||||
Dennis
|
||||
Edwin
|
||||
Don
|
||||
Glen
|
||||
Gordon
|
||||
Howard
|
||||
Earl
|
||||
Leo
|
||||
Lloyd
|
||||
Milton
|
||||
Norman
|
||||
Roland
|
||||
Vernon
|
||||
Warren
|
||||
Alfred
|
||||
Bernard
|
||||
Chester
|
||||
Clarence
|
||||
Clifford
|
||||
Clyde
|
||||
Dale
|
||||
Dan
|
||||
Darrell
|
||||
Floyd
|
||||
Herman
|
||||
Jerome
|
||||
Maurice
|
||||
Neil
|
||||
Ray
|
||||
Rodney
|
||||
Roland
|
||||
Stuart
|
||||
Wallace
|
||||
Wayne
|
||||
Wendell
|
||||
Barry
|
||||
Cecil
|
||||
Claude
|
||||
Daryl
|
||||
Edmund
|
||||
Everett
|
||||
Ferdinand
|
||||
Forrest
|
||||
Gerald
|
||||
Hugh
|
||||
Irving
|
||||
Leslie
|
||||
Marvin
|
||||
Morris
|
||||
Nelson
|
||||
Perry
|
||||
Phillip
|
||||
Roderick
|
||||
Ross
|
||||
Terrence
|
||||
Wade
|
||||
Winston
|
||||
Zachariah
|
||||
|
||||
@@ -165,3 +165,107 @@ Shaw
|
||||
Snyder
|
||||
Mason
|
||||
Dixon
|
||||
Blackwood
|
||||
Shepherd
|
||||
Frost
|
||||
Hawkins
|
||||
Pearson
|
||||
Fleming
|
||||
Dawson
|
||||
Palmer
|
||||
Nash
|
||||
Barker
|
||||
Thornton
|
||||
Fitzgerald
|
||||
Winters
|
||||
Mckenzie
|
||||
Chandler
|
||||
Griffith
|
||||
Cunningham
|
||||
Doyle
|
||||
Fletcher
|
||||
Hicks
|
||||
Walton
|
||||
Briggs
|
||||
Pearce
|
||||
Nichols
|
||||
Blake
|
||||
Hodges
|
||||
Benson
|
||||
Marsh
|
||||
Whitaker
|
||||
Skinner
|
||||
Robbins
|
||||
Goodwin
|
||||
Kirby
|
||||
Savage
|
||||
Hensley
|
||||
Hancock
|
||||
Pratt
|
||||
Gallagher
|
||||
Yates
|
||||
Dennis
|
||||
Swanson
|
||||
Steele
|
||||
Bauer
|
||||
Holt
|
||||
Barber
|
||||
Schultz
|
||||
Foley
|
||||
Fowler
|
||||
Wise
|
||||
Malone
|
||||
Cannon
|
||||
Tate
|
||||
Stark
|
||||
Welch
|
||||
Dyer
|
||||
Booth
|
||||
Payne
|
||||
Shannon
|
||||
Harmon
|
||||
Woodward
|
||||
Morse
|
||||
Jacobson
|
||||
Knowles
|
||||
Blanchard
|
||||
Dillon
|
||||
Stokes
|
||||
Buckley
|
||||
Dickerson
|
||||
Middleton
|
||||
Sellers
|
||||
Cobb
|
||||
Stephenson
|
||||
Roach
|
||||
Moody
|
||||
Beard
|
||||
Mccarthy
|
||||
Garner
|
||||
Mcguire
|
||||
Sloan
|
||||
Ballard
|
||||
Shields
|
||||
Orr
|
||||
Savage
|
||||
Graves
|
||||
Dempsey
|
||||
Weeks
|
||||
Mckay
|
||||
Cooke
|
||||
Riddle
|
||||
Gates
|
||||
Atkins
|
||||
Farrell
|
||||
Lowery
|
||||
Huffman
|
||||
Livingston
|
||||
Davenport
|
||||
Hendricks
|
||||
Kerr
|
||||
Pollard
|
||||
Hoover
|
||||
Wolfe
|
||||
Bowman
|
||||
Underwood
|
||||
Frazier
|
||||
|
||||
@@ -102,3 +102,110 @@ Juul
|
||||
Lise
|
||||
Myrthe
|
||||
Veerle
|
||||
Aafke
|
||||
Alicia
|
||||
Amira
|
||||
Aniek
|
||||
Annabel
|
||||
Annelies
|
||||
Anouk
|
||||
Astrid
|
||||
Babette
|
||||
Bianca
|
||||
Britt
|
||||
Carlijn
|
||||
Chantal
|
||||
Claire
|
||||
Dagmar
|
||||
Danique
|
||||
Daphne
|
||||
Denise
|
||||
Dominique
|
||||
Doris
|
||||
Eefje
|
||||
Elena
|
||||
Eline
|
||||
Elisa
|
||||
Elisabeth
|
||||
Ellen
|
||||
Esther
|
||||
Eveline
|
||||
Fabienne
|
||||
Felice
|
||||
Fleur
|
||||
Frederique
|
||||
Gwen
|
||||
Hanna
|
||||
Heleen
|
||||
Helena
|
||||
Ilona
|
||||
Imke
|
||||
Inge
|
||||
Irene
|
||||
Iris
|
||||
Janna
|
||||
Janneke
|
||||
Jasmine
|
||||
Jennifer
|
||||
Jessica
|
||||
Joelle
|
||||
Judith
|
||||
Julia
|
||||
Karin
|
||||
Karlijn
|
||||
Kim
|
||||
Kirsten
|
||||
Kyra
|
||||
Laura
|
||||
Lena
|
||||
Lianne
|
||||
Liesbeth
|
||||
Linda
|
||||
Lisanne
|
||||
Lisette
|
||||
Louise
|
||||
Maartje
|
||||
Manon
|
||||
Margot
|
||||
Marieke
|
||||
Marijke
|
||||
Marlies
|
||||
Marloes
|
||||
Marthe
|
||||
Melissa
|
||||
Michelle
|
||||
Nadine
|
||||
Natalie
|
||||
Nicole
|
||||
Nina
|
||||
Noortje
|
||||
Paulien
|
||||
Petra
|
||||
Rachel
|
||||
Renee
|
||||
Robin
|
||||
Rosa
|
||||
Roxanne
|
||||
Sabine
|
||||
Sandra
|
||||
Saskia
|
||||
Silke
|
||||
Simone
|
||||
Suzanne
|
||||
Sylvie
|
||||
Tamara
|
||||
Tanja
|
||||
Tara
|
||||
Thea
|
||||
Thirza
|
||||
Tina
|
||||
Tineke
|
||||
Ursula
|
||||
Victoria
|
||||
Wendy
|
||||
Wilma
|
||||
Xandra
|
||||
Yasmin
|
||||
Yvette
|
||||
Yvonne
|
||||
Zara
|
||||
|
||||
@@ -99,3 +99,114 @@ Mijs
|
||||
Mika
|
||||
Felix
|
||||
Merlijn
|
||||
Alexander
|
||||
Aron
|
||||
Arthur
|
||||
Axel
|
||||
Bas
|
||||
Bastiaan
|
||||
Berend
|
||||
Björn
|
||||
Casper
|
||||
Cees
|
||||
Chris
|
||||
Christian
|
||||
Christiaan
|
||||
Colin
|
||||
Cornelis
|
||||
Dani
|
||||
Dennis
|
||||
Dirk
|
||||
Dominic
|
||||
Eduard
|
||||
Eelco
|
||||
Erik
|
||||
Erwin
|
||||
Ezra
|
||||
Faas
|
||||
Filip
|
||||
Florian
|
||||
Frank
|
||||
Frederik
|
||||
Freek
|
||||
Gerard
|
||||
Gerrit
|
||||
Giel
|
||||
Gijs
|
||||
Glenn
|
||||
Govert
|
||||
Harm
|
||||
Harold
|
||||
Hendrik
|
||||
Henrik
|
||||
Huub
|
||||
Ian
|
||||
Ivo
|
||||
Jacob
|
||||
Jake
|
||||
Jan
|
||||
Jarno
|
||||
Jason
|
||||
Jeffrey
|
||||
Jeremy
|
||||
Jim
|
||||
Jimmy
|
||||
Johan
|
||||
Johannes
|
||||
Jonas
|
||||
Jonathan
|
||||
Jos
|
||||
Joshua
|
||||
Justin
|
||||
Kay
|
||||
Kevin
|
||||
Kjeld
|
||||
Klaas
|
||||
Lennard
|
||||
Lennart
|
||||
Leon
|
||||
Lex
|
||||
Liam
|
||||
Loek
|
||||
Lorenzo
|
||||
Louis
|
||||
Lowie
|
||||
Maarten
|
||||
Magnus
|
||||
Maikel
|
||||
Marc
|
||||
Marcel
|
||||
Marco
|
||||
Martijn
|
||||
Mathias
|
||||
Matthijs
|
||||
Maurits
|
||||
Menno
|
||||
Michiel
|
||||
Nathan
|
||||
Nico
|
||||
Oscar
|
||||
Pascal
|
||||
Patrick
|
||||
Paul
|
||||
Peter
|
||||
Philip
|
||||
Pieter
|
||||
Pim
|
||||
Quincy
|
||||
Remco
|
||||
Rick
|
||||
Rik
|
||||
Robert
|
||||
Rogier
|
||||
Rowan
|
||||
Ruud
|
||||
Simon
|
||||
Stefan
|
||||
Steven
|
||||
Thom
|
||||
Victor
|
||||
Vincent
|
||||
Willem
|
||||
Wouter
|
||||
Yannick
|
||||
|
||||
@@ -104,3 +104,104 @@ van Asselt
|
||||
Timmermans
|
||||
van Vliet
|
||||
van Rijn
|
||||
van Schaik
|
||||
Bosman
|
||||
Wolters
|
||||
van Hout
|
||||
Hermans
|
||||
van Rooij
|
||||
de Vos
|
||||
van Donselaar
|
||||
Evers
|
||||
van den Brink
|
||||
Verkerk
|
||||
Groeneveld
|
||||
van Duijn
|
||||
Schuurman
|
||||
Hoogendoorn
|
||||
van Zanten
|
||||
Koopman
|
||||
Cornelissen
|
||||
van Driel
|
||||
Teunissen
|
||||
Versteeg
|
||||
van Deursen
|
||||
Schipper
|
||||
van Kempen
|
||||
Bouwman
|
||||
van der Valk
|
||||
Nijhuis
|
||||
van der Werf
|
||||
van den Akker
|
||||
Verhoef
|
||||
Wessels
|
||||
van der Poel
|
||||
Driessen
|
||||
van Oosten
|
||||
Lambrechts
|
||||
van der Vlist
|
||||
Hoogeveen
|
||||
van Gils
|
||||
Rietveld
|
||||
Barendrecht
|
||||
van der Spek
|
||||
Stam
|
||||
van der Linde
|
||||
Boersma
|
||||
van Dijk
|
||||
Schepers
|
||||
van der Kolk
|
||||
Roelofs
|
||||
van der Velden
|
||||
van den Burg
|
||||
Westra
|
||||
van der Steen
|
||||
Pronk
|
||||
van der Veer
|
||||
Rozendaal
|
||||
van den Bos
|
||||
Konings
|
||||
van der Wiel
|
||||
Noordam
|
||||
van der Laan
|
||||
Schut
|
||||
van der Vlugt
|
||||
Witteveen
|
||||
van der Zwan
|
||||
Boogaard
|
||||
van der Waal
|
||||
Stolk
|
||||
van der Windt
|
||||
Rutten
|
||||
van der Zanden
|
||||
Spaans
|
||||
van der Zwaan
|
||||
Roos
|
||||
van der Zijl
|
||||
Schoenmaker
|
||||
van Diepen
|
||||
Romeijn
|
||||
van Doesburg
|
||||
Schippers
|
||||
van Eck
|
||||
Rijken
|
||||
van Egmond
|
||||
Schrama
|
||||
van Eijk
|
||||
Ruijter
|
||||
van Engelen
|
||||
Sanders
|
||||
van Es
|
||||
Schenk
|
||||
van Essen
|
||||
van Gaal
|
||||
van Geenen
|
||||
van Gent
|
||||
van Gestel
|
||||
van Gool
|
||||
van Grinsven
|
||||
van Gurp
|
||||
van Haaften
|
||||
van Haren
|
||||
van Hattem
|
||||
van Hees
|
||||
|
||||
@@ -67,25 +67,55 @@ public class UsernameEmailGenerator
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
// Use first initial + last name
|
||||
if (_random.Next(2) == 0)
|
||||
switch (_random.Next(4))
|
||||
{
|
||||
parts.Add(identity.FirstName.Substring(0, 1).ToLower() + identity.LastName.ToLower());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use full name
|
||||
parts.Add((identity.FirstName + identity.LastName).ToLower());
|
||||
case 0:
|
||||
// First initial + last name
|
||||
parts.Add(identity.FirstName.Substring(0, 1).ToLower() + identity.LastName.ToLower());
|
||||
break;
|
||||
case 1:
|
||||
// Full name
|
||||
parts.Add((identity.FirstName + identity.LastName).ToLower());
|
||||
break;
|
||||
case 2:
|
||||
// First name + last initial
|
||||
parts.Add(identity.FirstName.ToLower() + identity.LastName.Substring(0, 1).ToLower());
|
||||
break;
|
||||
case 3:
|
||||
// First 3 chars of first name + last name
|
||||
parts.Add(identity.FirstName.Substring(0, Math.Min(3, identity.FirstName.Length)).ToLower() + identity.LastName.ToLower());
|
||||
break;
|
||||
}
|
||||
|
||||
// Add birth year
|
||||
if (_random.Next(2) == 0)
|
||||
// Add birth year variations
|
||||
if (_random.Next(3) != 0)
|
||||
{
|
||||
parts.Add(identity.BirthDate.Year.ToString().Substring(2));
|
||||
switch (_random.Next(2))
|
||||
{
|
||||
case 0:
|
||||
parts.Add(identity.BirthDate.Year.ToString().Substring(2));
|
||||
break;
|
||||
case 1:
|
||||
parts.Add(identity.BirthDate.Year.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (_random.Next(2) == 0)
|
||||
{
|
||||
// Add random numbers for more uniqueness
|
||||
parts.Add(_random.Next(10, 999).ToString());
|
||||
}
|
||||
|
||||
// Join parts and sanitize
|
||||
// Join parts with random symbols, possibly multiple
|
||||
var emailPrefix = string.Join(GetRandomSymbol(), parts);
|
||||
|
||||
// Add extra random symbol at random position
|
||||
if (_random.Next(2) == 0)
|
||||
{
|
||||
int position = _random.Next(emailPrefix.Length);
|
||||
emailPrefix = emailPrefix.Insert(position, GetRandomSymbol());
|
||||
}
|
||||
|
||||
emailPrefix = SanitizeEmailPrefix(emailPrefix);
|
||||
|
||||
// Adjust length
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
# Copy the project files and restore dependencies
|
||||
COPY ["src/Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj", "src/Services/AliasVault.SmtpService/"]
|
||||
RUN dotnet restore "./src/Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj"
|
||||
RUN dotnet restore "./src/Services/AliasVault.SmtpService/AliasVault.SmtpService.csproj" -a "$TARGETARCH"
|
||||
COPY . .
|
||||
|
||||
# Build and publish the application
|
||||
WORKDIR "/src/src/Services/AliasVault.SmtpService"
|
||||
RUN dotnet publish "./AliasVault.SmtpService.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "./AliasVault.SmtpService.csproj" -c "$BUILD_CONFIGURATION" \
|
||||
-a "$TARGETARCH" \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
@@ -334,7 +334,7 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
|
||||
var insertedId = await InsertEmailIntoDatabase(message, new MailAddress(toAddress.AsAddress()), userPublicKey);
|
||||
logger.LogInformation(
|
||||
"Email for {ToAddress} successfully saved into database with ID {insertedId}.",
|
||||
"Email for {ToAddress} successfully saved into database with ID {InsertedId}.",
|
||||
toAddress.User + "@" + toAddress.Host,
|
||||
insertedId);
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
generate_random_string() {
|
||||
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-10} | head -n 1
|
||||
LC_ALL=C tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w ${1:-10} | head -n 1
|
||||
}
|
||||
|
||||
generate_random_attachment() {
|
||||
local temp_file="/tmp/test_attachment_$(generate_random_string 8).txt"
|
||||
echo "This is a test attachment content - $(generate_random_string 32)" > "$temp_file"
|
||||
echo "$temp_file"
|
||||
}
|
||||
|
||||
print_logo() {
|
||||
@@ -23,25 +29,54 @@ print_logo() {
|
||||
|
||||
send_email() {
|
||||
local recipient="$1"
|
||||
local with_attachment="$2"
|
||||
local subject_suffix=$(generate_random_string 8)
|
||||
local content_suffix=$(generate_random_string 20)
|
||||
local boundary="boundary-$(generate_random_string 16)"
|
||||
local attachment_content="This is a test attachment content - $(generate_random_string 32)"
|
||||
local attachment_name="test_attachment_$(generate_random_string 8).txt"
|
||||
|
||||
cat > temp_email.txt << EOF
|
||||
From: sender@example.com
|
||||
To: $recipient
|
||||
Subject: Test Email - $subject_suffix
|
||||
|
||||
This is a test email.
|
||||
|
||||
Random content: $content_suffix
|
||||
EOF
|
||||
|
||||
curl --url "smtp://localhost:25" \
|
||||
--mail-from "sender@example.com" \
|
||||
--mail-rcpt "$recipient" \
|
||||
--upload-file temp_email.txt
|
||||
|
||||
rm temp_email.txt
|
||||
if [[ "$with_attachment" =~ ^[Yy]$ ]]; then
|
||||
{
|
||||
echo "From: sender@example.com"
|
||||
echo "To: $recipient"
|
||||
echo "Subject: Test Email with Attachment - $subject_suffix"
|
||||
echo "MIME-Version: 1.0"
|
||||
echo "Content-Type: multipart/mixed; boundary=$boundary"
|
||||
echo ""
|
||||
echo "--$boundary"
|
||||
echo "Content-Type: text/plain; charset=utf-8"
|
||||
echo ""
|
||||
echo "This is a test email with attachment."
|
||||
echo ""
|
||||
echo "Random content: $content_suffix"
|
||||
echo ""
|
||||
echo "--$boundary"
|
||||
echo "Content-Type: application/octet-stream"
|
||||
echo "Content-Transfer-Encoding: base64"
|
||||
echo "Content-Disposition: attachment; filename=\"$attachment_name\""
|
||||
echo ""
|
||||
echo "$attachment_content" | base64
|
||||
echo ""
|
||||
echo "--$boundary--"
|
||||
} | curl --url "smtp://localhost:25" \
|
||||
--mail-from "sender@example.com" \
|
||||
--mail-rcpt "$recipient" \
|
||||
--upload-file -
|
||||
else
|
||||
{
|
||||
echo "From: sender@example.com"
|
||||
echo "To: $recipient"
|
||||
echo "Subject: Test Email - $subject_suffix"
|
||||
echo ""
|
||||
echo "This is a test email."
|
||||
echo ""
|
||||
echo "Random content: $content_suffix"
|
||||
} | curl --url "smtp://localhost:25" \
|
||||
--mail-from "sender@example.com" \
|
||||
--mail-rcpt "$recipient" \
|
||||
--upload-file -
|
||||
fi
|
||||
}
|
||||
|
||||
print_logo
|
||||
@@ -49,19 +84,18 @@ print_logo
|
||||
while true; do
|
||||
if [[ -z "$recipient" ]]; then
|
||||
read -p "Enter the recipient's email address: " recipient
|
||||
read -p "Do you want to send emails with attachments? (y/N): " with_attachment
|
||||
fi
|
||||
|
||||
send_email "$recipient"
|
||||
send_email "$recipient" "$with_attachment"
|
||||
|
||||
read -p "Send another email? (Press Enter for same recipient, or type a new email, or 'q' to quit): " next_action
|
||||
read -p "Send another email? (Press Enter for same recipient/settings, or type a new email, or 'q' to quit): " next_action
|
||||
|
||||
if [[ "$next_action" == "q" ]]; then
|
||||
echo "Exiting the script. Goodbye!"
|
||||
exit 0
|
||||
elif [[ -n "$next_action" ]]; then
|
||||
recipient="$next_action"
|
||||
else
|
||||
# If next_action is empty (user pressed Enter), keep the same recipient
|
||||
:
|
||||
read -p "Do you want to send emails with attachments? (y/N): " with_attachment
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
# Copy the project files and restore dependencies
|
||||
COPY ["src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj", "src/Services/AliasVault.TaskRunner/"]
|
||||
RUN dotnet restore "./src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj"
|
||||
RUN dotnet restore "./src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj" -a "$TARGETARCH"
|
||||
COPY . .
|
||||
|
||||
# Build and publish the application
|
||||
WORKDIR "/src/src/Services/AliasVault.TaskRunner"
|
||||
RUN dotnet publish "./AliasVault.TaskRunner.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "./AliasVault.TaskRunner.csproj" \
|
||||
-c "$BUILD_CONFIGURATION" \
|
||||
-a "$TARGETARCH" \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "AliasVault.TaskRunner.dll"]
|
||||
|
||||
@@ -25,12 +25,12 @@ public static class AppInfo
|
||||
/// <summary>
|
||||
/// Gets the minor version number.
|
||||
/// </summary>
|
||||
public const int VersionMinor = 10;
|
||||
public const int VersionMinor = 11;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the patch version number.
|
||||
/// </summary>
|
||||
public const int VersionPatch = 2;
|
||||
public const int VersionPatch = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the build number, typically used in CI/CD pipelines.
|
||||
|
||||
@@ -18,4 +18,9 @@ public class MailboxEmailApiModel : EmailApiModelBase
|
||||
/// Gets or sets the preview of the email message.
|
||||
/// </summary>
|
||||
public string MessagePreview { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the email has attachments.
|
||||
/// </summary>
|
||||
public bool HasAttachments { get; set; }
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.5.0">
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.49.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
namespace AliasVault.E2ETests.Tests.Client.Shard1;
|
||||
|
||||
using System.Text;
|
||||
using AliasVault.IntegrationTests.SmtpServer;
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
@@ -46,7 +47,7 @@ public class EmailDecryptionTests : ClientPlaywrightTest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if received email encrypted by server can be successfully decrypted by client
|
||||
/// Test if received email without attachments encrypted by server can be successfully decrypted by client
|
||||
/// and then be deleted by client.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
@@ -56,7 +57,7 @@ public class EmailDecryptionTests : ClientPlaywrightTest
|
||||
{
|
||||
// Create credential which should automatically create claim on server during database sync.
|
||||
const string serviceName = "Test Service";
|
||||
const string email = "testclaim@example.tld";
|
||||
const string email = "testclaim2@example.tld";
|
||||
await CreateCredentialEntry(new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceName },
|
||||
@@ -72,7 +73,7 @@ public class EmailDecryptionTests : ClientPlaywrightTest
|
||||
Assert.That(publicKey, Is.Not.Null, "Public key for user not found in database. Check if public key creation is working correctly.");
|
||||
Assert.That(publicKey.PublicKey, Has.Length.GreaterThanOrEqualTo(100), "Public key exists but length does not match expected. Check if public key creation is working correctly.");
|
||||
|
||||
// Email the SMTP server which will save the email in encrypted form in the database..
|
||||
// Email the SMTP server which will save the email in encrypted form in the database.
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
|
||||
message.To.Add(new MailboxAddress("Test Recipient", email));
|
||||
@@ -93,6 +94,7 @@ public class EmailDecryptionTests : ClientPlaywrightTest
|
||||
HtmlBody = htmlBody,
|
||||
};
|
||||
message.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
await SendMessageToSmtpServer(message);
|
||||
|
||||
// Assert that email was received by the server.
|
||||
@@ -138,11 +140,149 @@ public class EmailDecryptionTests : ClientPlaywrightTest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that adding a credential with email domain that is not in the known list to not get added as claim.
|
||||
/// Test if received email including attachment encrypted by server can be successfully decrypted by client
|
||||
/// and then be deleted by client.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(2)]
|
||||
public async Task EmailEncryptionDecryptionAttachmentDeleteTest()
|
||||
{
|
||||
// Create credential which should automatically create claim on server during database sync.
|
||||
const string serviceName = "Test Service";
|
||||
const string email = "testclaim@example.tld";
|
||||
await CreateCredentialEntry(new Dictionary<string, string>
|
||||
{
|
||||
{ "service-name", serviceName },
|
||||
{ "email", email },
|
||||
});
|
||||
|
||||
// Assert that the claim was created on the server.
|
||||
var claim = await ApiDbContext.UserEmailClaims.Where(x => x.Address == email).FirstOrDefaultAsync();
|
||||
Assert.That(claim, Is.Not.Null, "Claim for email address not found in database. Check if credential creation and claim creation are working correctly.");
|
||||
|
||||
// Assert that the users public key was created on the server.
|
||||
var publicKey = await ApiDbContext.UserEncryptionKeys.Where(x => x.UserId == claim.UserId).FirstOrDefaultAsync();
|
||||
Assert.That(publicKey, Is.Not.Null, "Public key for user not found in database. Check if public key creation is working correctly.");
|
||||
Assert.That(publicKey!.PublicKey, Has.Length.GreaterThanOrEqualTo(100), "Public key exists but length does not match expected. Check if public key creation is working correctly.");
|
||||
|
||||
// Email the SMTP server which will save the email in encrypted form in the database..
|
||||
var message = new MimeMessage();
|
||||
message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
|
||||
message.To.Add(new MailboxAddress("Test Recipient", email));
|
||||
const string textSubject = "Encrypted Email Subject";
|
||||
const string textBody = "This is a test email plain.";
|
||||
const string htmlBody = @"
|
||||
<html>
|
||||
<body>
|
||||
<h1>Test Email</h1>
|
||||
<p>This is a test email with HTML content.</p>
|
||||
<p>Sample anchor tag: <a href=""https://example.com"">Example Link</a></p>
|
||||
</body>
|
||||
</html>";
|
||||
message.Subject = textSubject;
|
||||
var bodyBuilder = new BodyBuilder
|
||||
{
|
||||
TextBody = textBody,
|
||||
HtmlBody = htmlBody,
|
||||
};
|
||||
var attachment = new MimePart("text", "plain")
|
||||
{
|
||||
Content = new MimeContent(new MemoryStream(Encoding.UTF8.GetBytes("This is an attachment."))),
|
||||
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
|
||||
ContentTransferEncoding = ContentEncoding.Base64,
|
||||
FileName = "attachment.txt",
|
||||
};
|
||||
bodyBuilder.Attachments.Add(attachment);
|
||||
message.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
await SendMessageToSmtpServer(message);
|
||||
|
||||
// Assert that email was received by the server.
|
||||
var emailReceived = await ApiDbContext.Emails.FirstOrDefaultAsync(x => x.To == email);
|
||||
Assert.That(emailReceived, Is.Not.Null, "Email not received by server. Check SMTP server and email encryption/decryption logic.");
|
||||
|
||||
// Assert that the attachment is stored in the database.
|
||||
var attachmentReceived = await ApiDbContext.EmailAttachments.FirstOrDefaultAsync(x => x.EmailId == emailReceived.Id);
|
||||
Assert.That(attachmentReceived, Is.Not.Null, "Attachment not found in database. Check email attachment encryption logic.");
|
||||
|
||||
// Assert that the attachment content is encrypted
|
||||
var attachmentContent = Encoding.UTF8.GetString(attachmentReceived!.Bytes);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(attachmentContent, Does.Not.Contain("This is an attachment."), "Attachment content stored as plain text in database. Check attachment encryption logic.");
|
||||
Assert.That(attachmentContent, Is.Not.Empty, "Attachment content is empty. Check attachment encryption logic.");
|
||||
});
|
||||
|
||||
// Assert that subject is not stored as plain text in the database.
|
||||
Assert.That(emailReceived!.Subject, Does.Not.Contain(textSubject), "Email subject stored as plain text in database. Check email encryption logic.");
|
||||
|
||||
// Attempt to click on email refresh button to get new emails.
|
||||
await Page.Locator("id=recent-email-refresh").First.ClickAsync();
|
||||
await WaitForUrlAsync("credentials/**", "Subject");
|
||||
|
||||
// Check if the email is visible on the page now.
|
||||
var emailContent = await Page.TextContentAsync("body");
|
||||
Assert.That(emailContent, Does.Contain(textSubject), "Email not (correctly) decrypted and displayed on the credential page. Check email decryption logic.");
|
||||
|
||||
// Navigate to the email index page and ensure that the decrypted email is also readable there.
|
||||
await NavigateUsingBlazorRouter("emails");
|
||||
await WaitForUrlAsync("emails", "Inbox");
|
||||
|
||||
// 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.");
|
||||
|
||||
// Assert that the attachment indicator is visible on the page.
|
||||
var attachmentIndicator = await Page.Locator(".attachment-indicator").First.GetAttributeAsync("class");
|
||||
Assert.That(attachmentIndicator, Is.Not.Null, "Attachment indicator not visible on email page. Check email attachment decryption logic.");
|
||||
|
||||
// Attempt to click on the email subject to open the modal.
|
||||
await Page.Locator("text=" + textSubject).First.ClickAsync();
|
||||
await WaitForUrlAsync("emails**", "Delete");
|
||||
|
||||
// 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.");
|
||||
|
||||
// Assert that email attachment metadata is visible in the modal.
|
||||
var body = await Page.TextContentAsync("body");
|
||||
Assert.That(body, Does.Contain("attachment.txt"), "Attachment metadata not visible in email modal. Check email attachment parse logic.");
|
||||
|
||||
// Assert that clicking on the attachment link downloads it.
|
||||
await Page.Locator(".attachment-link").First.ClickAsync();
|
||||
var download = await Page.WaitForDownloadAsync();
|
||||
|
||||
// Get the path of the downloaded file
|
||||
var downloadedFilePath = await download.PathAsync();
|
||||
|
||||
// Read the content of the downloaded file
|
||||
var downloadedContent = await File.ReadAllBytesAsync(downloadedFilePath);
|
||||
|
||||
// Compare with the original attachment content
|
||||
var originalContent = Encoding.UTF8.GetBytes("This is an attachment.");
|
||||
Assert.That(downloadedContent, Is.EqualTo(originalContent), "Downloaded attachment content does not match the original content.");
|
||||
|
||||
// Clean up: delete the downloaded file
|
||||
File.Delete(downloadedFilePath);
|
||||
|
||||
// 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.
|
||||
body = await Page.TextContentAsync("body");
|
||||
Assert.That(body, Does.Not.Contain(textSubject), "Email not deleted from page after deletion. Check email deletion logic.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test that adding a credential with email domain that is not in the known list to not get added as claim.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(3)]
|
||||
public async Task EmailUnknownDomainNoClaimTest()
|
||||
{
|
||||
// Create credential which should automatically create claim on server during database sync.
|
||||
@@ -165,7 +305,7 @@ public class EmailDecryptionTests : ClientPlaywrightTest
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(3)]
|
||||
[Order(4)]
|
||||
public async Task EmailDuplicateClaimTest()
|
||||
{
|
||||
// Create credential which should automatically create claim on server during database sync.
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.3"/>
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="NUnit" Version="4.3.2"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.5.0"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -27,18 +27,18 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.2">
|
||||
<PackageReference Include="coverlet.msbuild" Version="6.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.5.0">
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.6.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
# Copy csproj files and restore as distinct layers
|
||||
COPY ["src/Utilities/AliasVault.InstallCli/AliasVault.InstallCli.csproj", "src/Utilities/AliasVault.InstallCli/"]
|
||||
COPY ["src/Databases/AliasServerDb/AliasServerDb.csproj", "src/Databases/AliasServerDb/"]
|
||||
RUN dotnet restore "src/Utilities/AliasVault.InstallCli/AliasVault.InstallCli.csproj"
|
||||
RUN dotnet restore "src/Utilities/AliasVault.InstallCli/AliasVault.InstallCli.csproj" -a "$TARGETARCH"
|
||||
|
||||
# Copy the entire source code
|
||||
COPY . .
|
||||
|
||||
# Build and publish in one step
|
||||
RUN dotnet publish "src/Utilities/AliasVault.InstallCli/AliasVault.InstallCli.csproj" \
|
||||
-c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
-c "$BUILD_CONFIGURATION" \
|
||||
-a "$TARGETARCH" \
|
||||
-o /app/publish \
|
||||
/p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
Reference in New Issue
Block a user