mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 07:39:07 -04:00
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:
@@ -1,4 +1,4 @@
|
||||
API_URL=
|
||||
HOSTNAME=
|
||||
JWT_KEY=
|
||||
DATA_PROTECTION_CERT_PASS=
|
||||
ADMIN_PASSWORD_HASH=
|
||||
|
||||
49
.github/workflows/docker-compose-build.yml
vendored
49
.github/workflows/docker-compose-build.yml
vendored
@@ -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
5
.gitignore
vendored
@@ -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
15
Dockerfile
Normal 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"]
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
7
certificates/ssl/README.md
Normal file
7
certificates/ssl/README.md
Normal 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.
|
||||
@@ -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
20
entrypoint.sh
Normal 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;"
|
||||
35
install.sh
35
install.sh
@@ -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
85
nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
base.OnInitialized();
|
||||
|
||||
// Redirect to users page.
|
||||
NavigationService.RedirectTo("/users");
|
||||
NavigationService.RedirectTo("users");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
29
src/AliasVault.Admin/entrypoint.sh
Normal file
29
src/AliasVault.Admin/entrypoint.sh
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Start the application
|
||||
echo "Starting application..."
|
||||
dotnet /app/AliasVault.Api.dll
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -35,7 +35,7 @@ else
|
||||
|
||||
private async Task MakeWebApiCall()
|
||||
{
|
||||
await HttpClient.GetAsync("api/v1/Test");
|
||||
await HttpClient.GetAsync("v1/Test");
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
|
||||
@@ -35,7 +35,7 @@ else
|
||||
|
||||
private async Task MakeWebApiCall()
|
||||
{
|
||||
await HttpClient.GetAsync("api/v1/Test");
|
||||
await HttpClient.GetAsync("v1/Test");
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 "]"}')
|
||||
|
||||
@@ -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 / {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user