Merge pull request #365 from lanedirt/364-update-docker-setup-to-run-https-by-default

Update docker setup to run https by default
This commit is contained in:
Leendert de Borst
2024-11-15 18:56:34 +01:00
committed by GitHub
52 changed files with 381 additions and 164 deletions

View File

@@ -1,4 +1,4 @@
API_URL=
HOSTNAME=
JWT_KEY=
DATA_PROTECTION_CERT_PASS=
ADMIN_PASSWORD_HASH=

View File

@@ -32,29 +32,43 @@ jobs:
run: |
# Wait for a few seconds
sleep 10
- name: Test if localhost:80 (WASM app) responds
- name: Test if localhost:443 (WASM app) responds
uses: nick-fields/retry@v3
with:
timeout_minutes: 2
max_attempts: 3
command: |
http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:80)
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
if [ "$http_code" -ne 200 ]; then
echo "Service did not respond with 200 OK. Check if client app is configured correctly."
echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly."
exit 1
else
echo "Service responded with 200 OK"
fi
- name: Test if localhost:81 (WebApi) responds
- name: Test if localhost:443/api (WebApi) responds
uses: nick-fields/retry@v3
with:
timeout_minutes: 2
max_attempts: 3
command: |
http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:81)
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/api)
if [ "$http_code" -ne 200 ]; then
echo "Service did not respond with expected 200 OK. Check if WebApi is configured correctly."
echo "Service did not respond with expected 200 OK. Check if WebApi and/or nginx is configured correctly."
exit 1
else
echo "Service responded with $http_code"
fi
- name: Test if localhost:443/admin (Admin) responds
uses: nick-fields/retry@v3
with:
timeout_minutes: 2
max_attempts: 3
command: |
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443/admin/user/login)
if [ "$http_code" -ne 200 ]; then
echo "Service did not respond with expected 200 OK. Check if admin app and/or nginx is configured correctly."
exit 1
else
echo "Service responded with $http_code"
@@ -73,16 +87,13 @@ jobs:
echo "SmtpService responded on port 2525"
fi
- name: Test if localhost:8080 (Admin) responds
uses: nick-fields/retry@v3
with:
timeout_minutes: 2
max_attempts: 3
command: |
http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/user/login)
if [ "$http_code" -ne 200 ]; then
echo "Service did not respond with expected 200 OK. Check if admin app is configured correctly."
exit 1
else
echo "Service responded with $http_code"
fi
- name: Test install.sh --reset-password output
run: |
output=$(./install.sh --reset-password)
if ! echo "$output" | grep -E '^Password: [a-zA-Z0-9]{8,}$'; then
echo "Password reset output format is incorrect. Expected format: 'Password: <at least 8 chars>'"
echo "Actual output: $output"
exit 1
else
echo "Password reset output format is correct"
fi

5
.gitignore vendored
View File

@@ -393,3 +393,8 @@ src/Tests/AliasVault.E2ETests/appsettings.Development.json
# Draw.io diagram temp files
*.drawio.*
# Certificates
certificates/**/*.crt
certificates/**/*.key
certificates/**/*.pfx
certificates/**/*.pem

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM nginx:alpine
# Install OpenSSL for certificate generation
RUN apk add --no-cache openssl
# Copy configuration and entrypoint script
COPY nginx.conf /etc/nginx/nginx.conf
COPY entrypoint.sh /docker-entrypoint.sh
# Create SSL directory
RUN mkdir -p /etc/nginx/ssl && chmod 755 /etc/nginx/ssl \
&& chmod +x /docker-entrypoint.sh
EXPOSE 80 443
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@@ -59,9 +59,9 @@ $ chmod +x install.sh && ./install.sh
Note: if you do not wish to run the script, you can set up the environment variables and build the Docker image and containers manually instead. See the [manual setup instructions](docs/install/1-manually-setup-docker.md) for more information.
### 2. Ready to use
The install script executed in step #1 will output the URL where the app is available. By default this is http://localhost:80 for the client and http://localhost:8080 for the admin.
The install script executed in step #1 will output the URL where the app is available. By default this is https://localhost for the client and https://localhost/admin for the admin portal.
> Note: If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file.
> Note: If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file for the `nginx` (reverse-proxy) container.
#### Note for first time build:
- When running the init script for the first time, it may take a few minutes for Docker to download all dependencies. Subsequent builds will be faster.

View File

@@ -1,4 +1,6 @@
This is the default location where (self-generated) certificates are stored.
# Certificates directory structure
For example, the API and Admin projects make use of the .NET DataProtection API that depends on
certificates for encrypting various types of application data such as authentication cookies, anti-forgery tokens etc.
This directory contains certificates for AliasVault.
- `app`: Certificates that AliasVault uses to protect application data at rest (e.g. .NET DataProtection keys)
- `ssl`: SSL/TLS certificates for AliasVault hosted services

View File

@@ -0,0 +1,7 @@
# SSL certificates directory structure
This directory contains SSL/TLS certificates for various AliasVault services:
- `admin`: Certificate for the Admin UI.
- `api`: Certificate for the API service.
- `client`: Certificate for the Client UI.

View File

@@ -1,25 +1,29 @@
services:
admin:
image: aliasvault-admin
nginx:
build:
context: .
dockerfile: src/AliasVault.Admin/Dockerfile
dockerfile: Dockerfile
ports:
- "8080:8082"
- "80:80"
- "443:443"
volumes:
- ./certificates:/certificates:rw
- ./database:/database:rw
- ./logs:/logs:rw
- ./certificates/ssl:/etc/nginx/ssl:rw
depends_on:
- admin
- client
- api
- smtp
restart: always
env_file:
- .env
client:
image: aliasvault-client
build:
context: .
dockerfile: src/AliasVault.Client/Dockerfile
ports:
- "80:8080"
volumes:
- ./logs/msbuild:/src/msbuild-logs:rw
expose:
- "3000"
restart: always
env_file:
- .env
@@ -29,16 +33,31 @@ services:
build:
context: .
dockerfile: src/AliasVault.Api/Dockerfile
ports:
- "81:8081"
expose:
- "3001"
volumes:
- ./certificates:/certificates:rw
- ./database:/database:rw
- ./certificates/app:/certificates/app:rw
- ./logs:/logs:rw
env_file:
- .env
restart: always
admin:
image: aliasvault-admin
build:
context: .
dockerfile: src/AliasVault.Admin/Dockerfile
expose:
- "3002"
volumes:
- ./database:/database:rw
- ./certificates/app:/certificates/app:rw
- ./logs:/logs:rw
restart: always
env_file:
- .env
smtp:
image: aliasvault-smtp
build:

20
entrypoint.sh Normal file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
# Create SSL directory if it doesn't exist
mkdir -p /etc/nginx/ssl
# Generate self-signed SSL certificate if not exists
if [ ! -f /etc/nginx/ssl/cert.pem ] || [ ! -f /etc/nginx/ssl/key.pem ]; then
echo "Generating new SSL certificate..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/key.pem \
-out /etc/nginx/ssl/cert.pem \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
# Set proper permissions
chmod 644 /etc/nginx/ssl/cert.pem
chmod 600 /etc/nginx/ssl/key.pem
fi
# Start nginx
nginx -g "daemon off;"

View File

@@ -140,22 +140,22 @@ create_env_file() {
fi
}
# Function to check and populate the .env file with API_URL
populate_api_url() {
printf "${CYAN}> Checking API_URL...${NC}\n"
if ! grep -q "^API_URL=" "$ENV_FILE" || [ -z "$(grep "^API_URL=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
DEFAULT_API_URL="http://localhost:81"
read -p "Enter the base URL where the API will be hosted (press Enter for default: $DEFAULT_API_URL): " USER_API_URL
API_URL=${USER_API_URL:-$DEFAULT_API_URL}
if grep -q "^API_URL=" "$ENV_FILE"; then
awk -v url="$API_URL" '/^API_URL=/ {$0="API_URL="url} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
# Function to populate HOSTNAME
populate_hostname() {
printf "${CYAN}> Checking HOSTNAME...${NC}\n"
if ! grep -q "^HOSTNAME=" "$ENV_FILE" || [ -z "$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
DEFAULT_HOSTNAME="localhost"
read -p "Enter the hostname where AliasVault will be hosted (press Enter for default: $DEFAULT_HOSTNAME): " USER_HOSTNAME
HOSTNAME=${USER_HOSTNAME:-$DEFAULT_HOSTNAME}
if grep -q "^HOSTNAME=" "$ENV_FILE"; then
awk -v hostname="$HOSTNAME" '/^HOSTNAME=/ {$0="HOSTNAME="hostname} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
else
echo "API_URL=${API_URL}" >> "$ENV_FILE"
echo "HOSTNAME=${HOSTNAME}" >> "$ENV_FILE"
fi
printf "${GREEN}> API_URL has been set to $API_URL in $ENV_FILE.${NC}\n"
printf "${GREEN}> HOSTNAME has been set to $HOSTNAME in $ENV_FILE.${NC}\n"
else
API_URL=$(grep "^API_URL=" "$ENV_FILE" | cut -d '=' -f2)
printf "${GREEN}> API_URL already exists in $ENV_FILE with value: $API_URL${NC}\n"
HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
printf "${GREEN}> HOSTNAME already exists in $ENV_FILE with value: $HOSTNAME${NC}\n"
fi
}
@@ -359,7 +359,7 @@ main() {
printf "${YELLOW}+++ Initializing .env file +++${NC}\n"
printf "\n"
create_env_file || exit $?
populate_api_url || exit $?
populate_hostname || exit $?
populate_jwt_key || exit $?
populate_data_protection_cert_pass || exit $?
set_private_email_domains || exit $?
@@ -381,14 +381,14 @@ main() {
printf "${CYAN}To configure the server, login to the admin panel:${NC}\n"
printf "\n"
if [ "$ADMIN_PASSWORD" != "" ]; then
printf "Admin Panel: http://localhost:8080/\n"
printf "Admin Panel: https://$HOSTNAME/admin\n"
printf "Username: admin\n"
printf "Password: $ADMIN_PASSWORD\n"
printf "\n"
printf "${YELLOW}(!) Caution: Make sure to backup the above credentials in a safe place, they won't be shown again!${NC}\n"
printf "\n"
else
printf "Admin Panel: http://localhost:8080/\n"
printf "Admin Panel: https://$HOSTNAME/admin\n"
printf "Username: admin\n"
printf "Password: (Previously set. Run this command with --reset-password to generate a new one.)\n"
printf "\n"
@@ -397,8 +397,7 @@ main() {
printf "\n"
printf "${CYAN}In order to start using AliasVault and create your own vault, log into the client website:${NC}\n"
printf "\n"
printf "Client Website: http://localhost:80/\n"
printf "You can create your own account from there.\n"
printf "Client Website: https://$HOSTNAME/\n"
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
}

85
nginx.conf Normal file
View File

@@ -0,0 +1,85 @@
events {
worker_connections 1024;
}
http {
upstream client {
server client:3000;
}
upstream api {
server api:3001;
}
upstream admin {
server admin:3002;
}
# Preserve any existing X-Forwarded-* headers, this is relevant if AliasVault
# is running behind another reverse proxy.
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Enable gzip compression, which reduces the amount of data that needs to be transferred
# to speed up WASM load times.
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# Client app (root path)
location / {
proxy_pass http://client;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Admin interface
location /admin {
proxy_pass http://admin;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Add WebSocket support for Blazor server
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
# API endpoints
location /api {
proxy_pass http://api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

View File

@@ -1,9 +1,10 @@
<a href="/">
@using AliasVault.Admin.Services
@inject NavigationService NavigationService
<a href="@NavigationService.BaseUri">
<div class="text-5xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
<img src="img/logo.svg" alt="AliasVault" class="w-20 h-20 mr-2" />
<span>AliasVault</span>
<span class="ps-2 self-center hidden sm:flex text-lg font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
</div>
</a>
</a>

View File

@@ -1,22 +1,18 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8082
EXPOSE 3002
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Copy the project files and restore dependencies
COPY ["src/AliasVault.Admin/AliasVault.Admin.csproj", "src/AliasVault.Admin/"]
RUN dotnet restore "src/AliasVault.Admin/AliasVault.Admin.csproj"
COPY . .
# Build the WebApi project
WORKDIR "/src/src/AliasVault.Admin"
RUN dotnet build "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
# Publish the application to the /app/publish directory in the container
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
@@ -24,6 +20,7 @@ RUN dotnet publish "AliasVault.Admin.csproj" -c "$BUILD_CONFIGURATION" -o /app/p
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
EXPOSE 8082
ENV ASPNETCORE_URLS=http://+:8082
ENV ASPNETCORE_URLS=http://+:3002
ENV ASPNETCORE_PATHBASE=/admin
ENTRYPOINT ["dotnet", "AliasVault.Admin.dll"]

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<base href="/"/>
<base href="@NavigationService.BaseUri"/>
<link rel="stylesheet" href="@VersionService.GetVersionedPath("css/tailwind.css")"/>
<link rel="stylesheet" href="@VersionService.GetVersionedPath("css/app.css")"/>
<link rel="stylesheet" href="AliasVault.Admin.styles.css"/>

View File

@@ -5,7 +5,7 @@
<nav class="fixed z-30 w-full border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4 bg-primary-100">
<div class="flex justify-between items-center max-w-screen-2xl mx-auto">
<div class="flex justify-start items-center">
<a href="/" class="flex mr-14 flex-shrink-0">
<a href="@NavigationService.BaseUri" class="flex mr-14 flex-shrink-0">
<img src="/img/logo.svg" class="mr-3 h-8" alt="AliasVault Logo">
<span class="self-center hidden sm:flex text-2xl font-semibold whitespace-nowrap dark:text-white">AliasVault</span>
<span class="ps-2 self-center hidden sm:flex text-sm font-bold whitespace-nowrap text-white bg-red-600 rounded-full px-2 py-1 ml-2">Admin</span>
@@ -13,16 +13,16 @@
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1">
<ul class="flex flex-col mt-4 space-x-6 text-sm font-medium lg:flex-row xl:space-x-8 lg:mt-0">
<NavLink href="/users" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="users" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Users
</NavLink>
<NavLink href="/emails" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="emails" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Emails
</NavLink>
<NavLink href="/logging/general" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="logging/general" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
General logs
</NavLink>
<NavLink href="/logging/auth" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="logging/auth" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Auth logs
</NavLink>
</ul>
@@ -57,7 +57,7 @@
</ul>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
<li>
<a href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
<a href="user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
</li>
</ul>
</div>

View File

@@ -16,6 +16,6 @@
base.OnInitialized();
// Redirect to users page.
NavigationService.RedirectTo("/users");
NavigationService.RedirectTo("users");
}
}

View File

@@ -18,6 +18,7 @@ using AliasVault.Cryptography.Server;
using AliasVault.Logging;
using AliasVault.RazorComponents.Services;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
@@ -104,8 +105,24 @@ else
app.UseHsts();
}
// If the ASPNETCORE_PATHBASE environment variable is set, use it as the path base for the application.
// This is required for running the admin interface behind a reverse proxy on the same port as the client app.
// E.g. default Docker Compose setup makes admin app available on /admin path.
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_PATHBASE")))
{
app.UsePathBase(Environment.GetEnvironmentVariable("ASPNETCORE_PATHBASE"));
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto,
});
app.UseStaticFiles();
app.UseRouting();
app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();

View File

@@ -0,0 +1,29 @@
#!/bin/sh
# Create SSL directory if it doesn't exist
mkdir -p /app/ssl
# Generate self-signed SSL certificate if not exists
if [ ! -f /app/ssl/admin.crt ] || [ ! -f /app/ssl/admin.key ]; then
echo "Generating new SSL certificate..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /app/ssl/admin.key \
-out /app/ssl/admin.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
# Set proper permissions
chmod 644 /app/ssl/admin.crt
chmod 600 /app/ssl/admin.key
# Create PFX for ASP.NET Core
openssl pkcs12 -export -out /app/ssl/admin.pfx \
-inkey /app/ssl/admin.key \
-in /app/ssl/admin.crt \
-password pass:YourSecurePassword
fi
export ASPNETCORE_Kestrel__Certificates__Default__Path=/app/ssl/admin.pfx
export ASPNETCORE_Kestrel__Certificates__Default__Password=YourSecurePassword
# Start the application
dotnet AliasVault.Admin.dll

View File

@@ -17,7 +17,7 @@ using Microsoft.AspNetCore.Mvc;
/// Base controller that concrete controllers can extend from if all requests require authentication.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
[Route("api/v{version:apiVersion}/[controller]")]
[Route("v{version:apiVersion}/[controller]")]
[ApiController]
[Authorize]
public abstract class AuthenticatedRequestController(UserManager<AliasVaultUser> userManager) : ControllerBase

View File

@@ -39,7 +39,7 @@ using SecureRemotePassword;
/// <param name="cache">IMemoryCache instance for persisting SRP values during multistep login process.</param>
/// <param name="timeProvider">ITimeProvider instance. This returns the time which can be mutated for testing.</param>
/// <param name="authLoggingService">AuthLoggingService instance. This is used to log auth attempts to the database.</param>
[Route("api/v{version:apiVersion}/[controller]")]
[Route("v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, SignInManager<AliasVaultUser> signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService) : ControllerBase

View File

@@ -21,7 +21,7 @@ using Microsoft.EntityFrameworkCore;
/// </summary>
/// <param name="dbContextFactory">AliasServerDbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
[Route("api/v{version:apiVersion}/[controller]")]
[Route("v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class SecurityController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)

View File

@@ -24,7 +24,7 @@ using Microsoft.EntityFrameworkCore;
/// <param name="urlEncoder">UrlEncoder instance.</param>
/// <param name="authLoggingService">AuthLoggingService instance. This is used to log auth attempts to the database.</param>
/// <param name="userManager">UserManager instance.</param>
[Route("api/v{version:apiVersion}/[controller]")]
[Route("v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class TwoFactorAuthController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UrlEncoder urlEncoder, AuthLoggingService authLoggingService, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)

View File

@@ -1,22 +1,18 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8081
EXPOSE 3001
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
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"
COPY . .
# Build the WebApi project
WORKDIR "/src/src/AliasVault.Api"
RUN dotnet build "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
# Publish the application to the /app/publish directory in the container
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
@@ -24,8 +20,7 @@ RUN dotnet publish "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/pub
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY /src/AliasVault.Api/entrypoint.sh /app
RUN chmod +x /app/entrypoint.sh
EXPOSE 8081
ENV ASPNETCORE_URLS=http://+:8081
ENTRYPOINT ["/app/entrypoint.sh"]
ENV ASPNETCORE_URLS=http://+:3001
ENV ASPNETCORE_PATHBASE=/api
ENTRYPOINT ["dotnet", "AliasVault.Api.dll"]

View File

@@ -157,6 +157,12 @@ if (app.Environment.IsDevelopment())
app.UseCors("CorsPolicy");
// If the ASPNETCORE_PATHBASE environment variable is set, use it as the path base
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_PATHBASE")))
{
app.UsePathBase(Environment.GetEnvironmentVariable("ASPNETCORE_PATHBASE"));
}
app.UseAuthentication();
app.UseAuthorization();

View File

@@ -1,5 +0,0 @@
#!/bin/sh
# Start the application
echo "Starting application..."
dotnet /app/AliasVault.Api.dll

View File

@@ -207,7 +207,7 @@ else
var username = _loginModel.Username.ToLowerInvariant().Trim();
// Send request to server with username to get server ephemeral public key.
var result = await Http.PostAsJsonAsync("api/v1/Auth/login", new LoginInitiateRequest(username));
var result = await Http.PostAsJsonAsync("v1/Auth/login", new LoginInitiateRequest(username));
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)
@@ -238,7 +238,7 @@ else
username);
// 4. Client sends proof of session key to server.
result = await Http.PostAsJsonAsync("api/v1/Auth/validate", new ValidateLoginRequest(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof));
result = await Http.PostAsJsonAsync("v1/Auth/validate", new ValidateLoginRequest(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof));
responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)
@@ -280,7 +280,7 @@ else
var username = _loginModel.Username.ToLowerInvariant().Trim();
// Validate 2-factor auth code auth and login
var result = await Http.PostAsJsonAsync("api/v1/Auth/validate-recovery-code", new ValidateLoginRequestRecoveryCode(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModelRecoveryCode.RecoveryCode));
var result = await Http.PostAsJsonAsync("v1/Auth/validate-recovery-code", new ValidateLoginRequestRecoveryCode(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModelRecoveryCode.RecoveryCode));
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)
@@ -338,7 +338,7 @@ else
var username = _loginModel.Username.ToLowerInvariant().Trim();
// Validate 2-factor auth code auth and login
var result = await Http.PostAsJsonAsync("api/v1/Auth/validate-2fa", new ValidateLoginRequest2Fa(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModel2Fa.TwoFactorCode ?? 0));
var result = await Http.PostAsJsonAsync("v1/Auth/validate-2fa", new ValidateLoginRequest2Fa(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof, _loginModel2Fa.TwoFactorCode ?? 0));
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)

View File

@@ -169,7 +169,7 @@
try
{
var response = await Http.PostAsJsonAsync("api/v1/Auth/validate-username", new { Username });
var response = await Http.PostAsJsonAsync("v1/Auth/validate-username", new { Username });
if (response.IsSuccessStatusCode)
{

View File

@@ -123,7 +123,7 @@ else
await StatusCheck();
// Send request to server with email to get user salt.
var result = await Http.PostAsJsonAsync("api/v1/Auth/login", new LoginInitiateRequest(Username!));
var result = await Http.PostAsJsonAsync("v1/Auth/login", new LoginInitiateRequest(Username!));
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)
@@ -234,7 +234,7 @@ else
// If user has no valid authentication an automatic redirect to login page will take place.
try
{
await Http.GetAsync("api/v1/Auth/status");
await Http.GetAsync("v1/Auth/status");
}
catch (Exception ex)
{

View File

@@ -3,13 +3,12 @@ WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
# Add environment variable for opting out of telemetry which fixes
# "error MSB4166: Child node "8" exited prematurely." issues.
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
ENV MSBUILDDEBUGPATH=/src/msbuild-logs
WORKDIR /src
# 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
@@ -30,12 +29,13 @@ RUN dotnet publish "AliasVault.Client.csproj" -c "$BUILD_CONFIGURATION" -o /app/
# Final stage
FROM nginx:1.24.0 AS final
WORKDIR /usr/share/nginx/html
COPY --from=publish /app/publish/wwwroot .
COPY /src/AliasVault.Client/nginx.conf /etc/nginx/nginx.conf
COPY /src/AliasVault.Client/entrypoint.sh /app/
COPY /src/AliasVault.Client/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 3000
ENV ASPNETCORE_URLS=http://+:3000
ENTRYPOINT ["/app/entrypoint.sh"]

View File

@@ -147,7 +147,7 @@
try
{
var response = await HttpClient.DeleteAsync($"api/v1/Email/{Email.Id}");
var response = await HttpClient.DeleteAsync($"v1/Email/{Email.Id}");
if (response.IsSuccessStatusCode)
{
GlobalNotificationService.AddSuccessMessage("Email deleted successfully", true);

View File

@@ -285,7 +285,7 @@
/// </summary>
private async Task LoadAliasVaultEmails()
{
var request = new HttpRequestMessage(HttpMethod.Get, $"api/v1/EmailBox/{EmailAddress}");
var request = new HttpRequestMessage(HttpMethod.Get, $"v1/EmailBox/{EmailAddress}");
try
{
var response = await HttpClient.SendAsync(request);
@@ -351,7 +351,7 @@
/// </summary>
private async Task ShowAliasVaultEmailInModal(int emailId)
{
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"api/v1/Email/{emailId}");
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"v1/Email/{emailId}");
if (mail != null)
{
// Decrypt the email content locally.

View File

@@ -138,7 +138,7 @@ else
Addresses = emailClaimList,
};
var request = new HttpRequestMessage(HttpMethod.Post, $"api/v1/EmailBox/bulk");
var request = new HttpRequestMessage(HttpMethod.Post, $"v1/EmailBox/bulk");
request.Content = new StringContent(JsonSerializer.Serialize(requestModel), Encoding.UTF8, "application/json");
try
@@ -245,7 +245,7 @@ else
/// </summary>
private async Task ShowAliasVaultEmailInModal(int emailId)
{
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"api/v1/Email/{emailId}");
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"v1/Email/{emailId}");
if (mail != null)
{
// Decrypt the email content locally.

View File

@@ -108,7 +108,7 @@ else
if (firstRender)
{
// Get the QR code and secret for the authenticator app.
var response = await Http.GetFromJsonAsync<PasswordChangeInitiateResponse>("api/v1/Auth/change-password/initiate");
var response = await Http.GetFromJsonAsync<PasswordChangeInitiateResponse>("v1/Auth/change-password/initiate");
if (response == null)
{
@@ -191,7 +191,7 @@ else
PasswordChangeModel = new PasswordChangeModel();
// 4. Client sends proof of session key to server.
var response = await Http.PostAsJsonAsync("api/v1/Vault/change-password", vaultPasswordChangeObject);
var response = await Http.PostAsJsonAsync("v1/Vault/change-password", vaultPasswordChangeObject);
var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)

View File

@@ -70,7 +70,7 @@
{
IsLoading = true;
StateHasChanged();
var sessionsResponse = await Http.GetFromJsonAsync<List<RefreshTokenModel>>("api/v1/Security/sessions");
var sessionsResponse = await Http.GetFromJsonAsync<List<RefreshTokenModel>>("v1/Security/sessions");
if (sessionsResponse is not null)
{
Sessions = sessionsResponse;
@@ -89,7 +89,7 @@
{
try
{
var response = await Http.DeleteAsync($"api/v1/Security/sessions/{id}");
var response = await Http.DeleteAsync($"v1/Security/sessions/{id}");
if (response.IsSuccessStatusCode)
{
GlobalNotificationService.AddSuccessMessage("Session revoked successfully.", true);

View File

@@ -73,7 +73,7 @@
{
IsLoading = true;
StateHasChanged();
var authlogResponse = await Http.GetFromJsonAsync<List<AuthLogModel>>("api/v1/Security/authlogs");
var authlogResponse = await Http.GetFromJsonAsync<List<AuthLogModel>>("v1/Security/authlogs");
if (authlogResponse is not null)
{
AuthLogs = authlogResponse;

View File

@@ -47,7 +47,7 @@
IsLoading = true;
StateHasChanged();
var twoFactorResponse = await Http.GetFromJsonAsync<TwoFactorEnabledResult>("api/v1/TwoFactorAuth/status");
var twoFactorResponse = await Http.GetFromJsonAsync<TwoFactorEnabledResult>("v1/TwoFactorAuth/status");
if (twoFactorResponse is not null)
{
TwoFactorEnabled = twoFactorResponse.TwoFactorEnabled;

View File

@@ -48,7 +48,7 @@ else
// Check on server if 2FA is enabled
if (firstRender)
{
var response = await Http.GetFromJsonAsync<TwoFactorEnabledResult>("api/v1/TwoFactorAuth/status");
var response = await Http.GetFromJsonAsync<TwoFactorEnabledResult>("v1/TwoFactorAuth/status");
if (response is not null && !response.TwoFactorEnabled)
{
GlobalNotificationService.AddErrorMessage("Two-factor authentication is not enabled.");
@@ -63,7 +63,7 @@ else
private async Task DisableTwoFactor()
{
var response = await Http.PostAsync("api/v1/TwoFactorAuth/disable", null);
var response = await Http.PostAsync("v1/TwoFactorAuth/disable", null);
if (response.IsSuccessStatusCode)
{
GlobalNotificationService.AddSuccessMessage("Two-factor authentication is now successfully disabled.");

View File

@@ -74,7 +74,7 @@ else
if (firstRender)
{
// Get the QR code and secret for the authenticator app.
var response = await Http.PostAsync("api/v1/TwoFactorAuth/enable", null);
var response = await Http.PostAsync("v1/TwoFactorAuth/enable", null);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TwoFactorSetupResult>();
@@ -100,7 +100,7 @@ else
private async Task VerifySetup()
{
var response = await Http.PostAsJsonAsync("api/v1/TwoFactorAuth/verify", VerifyModel.Code);
var response = await Http.PostAsJsonAsync("v1/TwoFactorAuth/verify", VerifyModel.Code);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TwoFactorVerifyResult>();

View File

@@ -35,7 +35,7 @@ else
private async Task MakeWebApiCall()
{
await HttpClient.GetAsync("api/v1/Test");
await HttpClient.GetAsync("v1/Test");
IsLoading = false;
StateHasChanged();

View File

@@ -35,7 +35,7 @@ else
private async Task MakeWebApiCall()
{
await HttpClient.GetAsync("api/v1/Test");
await HttpClient.GetAsync("v1/Test");
IsLoading = false;
StateHasChanged();

View File

@@ -54,12 +54,11 @@ builder.Services.AddScoped(sp =>
{
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient("AliasVault.Api");
if (builder.Configuration["ApiUrl"] is null)
{
throw new InvalidOperationException("The 'ApiUrl' configuration value is required.");
}
var apiConfig = sp.GetRequiredService<Config>();
httpClient.BaseAddress = new Uri(builder.Configuration["ApiUrl"]!);
// Ensure the API URL ends with a forward slash
var baseUrl = apiConfig.ApiUrl.TrimEnd('/') + "/";
httpClient.BaseAddress = new Uri(baseUrl);
return httpClient;
});
builder.Services.AddTransient<AliasVaultApiHandlerService>();

View File

@@ -46,7 +46,7 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca
var accessToken = await GetAccessTokenAsync();
var refreshToken = await GetRefreshTokenAsync();
var tokenInput = new TokenModel { Token = accessToken, RefreshToken = refreshToken };
using var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/Auth/refresh")
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/Auth/refresh")
{
Content = JsonContent.Create(tokenInput),
};
@@ -319,7 +319,7 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca
RefreshToken = await GetRefreshTokenAsync(),
};
using var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/Auth/revoke")
using var request = new HttpRequestMessage(HttpMethod.Post, "v1/Auth/revoke")
{
Content = JsonContent.Create(tokenInput),
};

View File

@@ -50,7 +50,7 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP
var srpSignup = Srp.PasswordChangeAsync(client, salt, username, passwordHashString);
var registerRequest = new RegisterRequest(srpSignup.Username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings);
var result = await httpClient.PostAsJsonAsync("api/v1/Auth/register", registerRequest);
var result = await httpClient.PostAsJsonAsync("v1/Auth/register", registerRequest);
var responseContent = await result.Content.ReadAsStringAsync();
if (!result.IsSuccessStatusCode)

View File

@@ -375,7 +375,7 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
try
{
var apiReturn =
await httpClient.GetFromJsonAsync<FaviconExtractModel>($"api/v1/Favicon/Extract?url={url}");
await httpClient.GetFromJsonAsync<FaviconExtractModel>($"v1/Favicon/Extract?url={url}");
if (apiReturn?.Image is not null)
{
credentialObject.Service.Logo = apiReturn.Image;

View File

@@ -106,7 +106,7 @@ public sealed class DbService : IDisposable
{
try
{
var vaultsToMerge = await _httpClient.GetFromJsonAsync<VaultMergeResponse>($"api/v1/Vault/merge?currentRevisionNumber={_vaultRevisionNumber}");
var vaultsToMerge = await _httpClient.GetFromJsonAsync<VaultMergeResponse>($"v1/Vault/merge?currentRevisionNumber={_vaultRevisionNumber}");
if (vaultsToMerge == null || vaultsToMerge.Vaults.Count == 0)
{
// No vaults to merge found, set error state.
@@ -558,7 +558,7 @@ public sealed class DbService : IDisposable
// Load from webapi.
try
{
var response = await _httpClient.GetFromJsonAsync<VaultGetResponse>("api/v1/Vault");
var response = await _httpClient.GetFromJsonAsync<VaultGetResponse>("v1/Vault");
if (response is not null)
{
if (response.Status == VaultStatus.MergeRequired)
@@ -621,7 +621,7 @@ public sealed class DbService : IDisposable
try
{
var response = await _httpClient.PostAsJsonAsync("api/v1/Vault", vaultObject);
var response = await _httpClient.PostAsJsonAsync("v1/Vault", vaultObject);
// Ensure the request was successful
response.EnsureSuccessStatusCode();

View File

@@ -1,19 +1,32 @@
#!/bin/sh
# Set the default API URL for localhost debugging
DEFAULT_API_URL="http://localhost:81"
# Set the default hostname for localhost debugging
DEFAULT_HOSTNAME="localhost"
DEFAULT_PRIVATE_EMAIL_DOMAINS="localmail.tld"
DEFAULT_SUPPORT_EMAIL=""
# Use the provided API_URL environment variable if it exists, otherwise use the default
API_URL=${API_URL:-$DEFAULT_API_URL}
# Use the provided HOSTNAME environment variable if it exists, otherwise use the default
HOSTNAME=${HOSTNAME:-$DEFAULT_HOSTNAME}
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS}
SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}
# Replace the default URL with the actual API URL
sed -i "s|http://localhost:5092|${API_URL}|g" /usr/share/nginx/html/appsettings.json
# Replace the default SMTP allowed domains with the actual allowed SMTP domains
# Note: this is used so the client knows which email addresses should be registered with the AliasVault server
# in order to be able to receive emails.
# Create SSL directory if it doesn't exist
mkdir -p /etc/nginx/ssl
# Generate self-signed SSL certificate if not exists
if [ ! -f /etc/nginx/ssl/nginx.crt ] || [ ! -f /etc/nginx/ssl/nginx.key ]; then
echo "Generating new SSL certificate..."
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/nginx.key \
-out /etc/nginx/ssl/nginx.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
# Set proper permissions
chmod 644 /etc/nginx/ssl/nginx.crt
chmod 600 /etc/nginx/ssl/nginx.key
fi
# Replace the default URL with the actual API URL constructed from hostname
sed -i "s|http://localhost:5092|https://${HOSTNAME}/api|g" /usr/share/nginx/html/appsettings.json
# Convert comma-separated list to JSON array
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 "]"}')

View File

@@ -16,7 +16,7 @@ http {
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 8080;
listen 3000;
server_name localhost;
location / {

View File

@@ -1,7 +1,9 @@
<nav class="flex mb-4">
@inject NavigationManager NavigationManager
<nav class="flex mb-4">
<ol class="inline-flex items-center space-x-1 text-sm font-medium md:space-x-2">
<li class="inline-flex items-center">
<a href="/" class="inline-flex items-center text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-primary-500">
<a href="@NavigationManager.BaseUri" class="inline-flex items-center text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-primary-500">
<svg class="w-5 h-5 mr-2.5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"></path></svg>
Home
</a>

View File

@@ -27,7 +27,7 @@ public class ApiLoggingTests : ClientPlaywrightTest
// Call webapi endpoint that throws an exception.
try
{
await Page.GotoAsync(ApiBaseUrl + "api/v1/Test/Error");
await Page.GotoAsync(ApiBaseUrl + "v1/Test/Error");
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
}
catch
@@ -38,7 +38,7 @@ public class ApiLoggingTests : ClientPlaywrightTest
// Read from database to check if the log entry was created.
var logEntry = await ApiDbContext.Logs.Where(x => x.Application == "AliasVault.Api").OrderByDescending(x => x.Id).FirstOrDefaultAsync();
Assert.That(logEntry, Is.Not.Null, "Log entry for triggered exception not found in database. Check Serilog configuration and /api/v1/Test/Error endpoint.");
Assert.That(logEntry, Is.Not.Null, "Log entry for triggered exception not found in database. Check Serilog configuration and /v1/Test/Error endpoint.");
Assert.That(logEntry.Exception, Does.Contain("Test error"), "Log entry in database does not contain expected message. Check exception and Serilog configuration.");
}
}

View File

@@ -9,12 +9,12 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net9.0\InitializationCLI.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.InstallCli.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net9.0\InitializationCLI.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.InstallCli.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

View File

@@ -1,28 +1,28 @@
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Copy csproj files and restore as distinct layers
COPY ["src/Utilities/InitializationCLI/InitializationCLI.csproj", "src/Utilities/InitializationCLI/"]
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/InitializationCLI/InitializationCLI.csproj"
RUN dotnet restore "src/Utilities/AliasVault.InstallCli/AliasVault.InstallCli.csproj"
# Copy the entire source code
COPY . .
# Build the project
RUN dotnet build "src/Utilities/InitializationCLI/InitializationCLI.csproj" \
RUN dotnet build "src/Utilities/AliasVault.InstallCli/AliasVault.InstallCli.csproj" \
-c "$BUILD_CONFIGURATION" -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "src/Utilities/InitializationCLI/InitializationCLI.csproj" \
RUN dotnet publish "src/Utilities/AliasVault.InstallCli/AliasVault.InstallCli.csproj" \
-c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "InitializationCLI.dll"]
ENTRYPOINT ["dotnet", "AliasVault.InstallCli.dll"]

View File

@@ -30,7 +30,7 @@ public static class DataProtectionExtensions
{
var certPassword = Environment.GetEnvironmentVariable("DATA_PROTECTION_CERT_PASS")
?? throw new KeyNotFoundException("DATA_PROTECTION_CERT_PASS is not set in configuration or environment variables.");
var certPath = "../../certificates/AliasVault.DataProtection.pfx";
var certPath = "../../certificates/app/AliasVault.DataProtection.pfx";
if (certPassword == "Development")
{
certPath = Path.Combine(AppContext.BaseDirectory, "AliasVault.DataProtection.Development.pfx");
@@ -44,7 +44,7 @@ public static class DataProtectionExtensions
}
else
{
cert = X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword);
cert = X509CertificateLoader.LoadPkcs12FromFile(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable);
}
services.AddDataProtection()