Compare commits

...

63 Commits
0.6.0 ... 0.7.0

Author SHA1 Message Date
Leendert de Borst
48b96b4151 Merge pull request #390 from lanedirt/389-prepare-070-release
Update documentation for 0.7.0 release
2024-11-20 17:01:20 +01:00
Leendert de Borst
e9064643a6 Add busy timeout to SQLite connections to prevent errors (#389) 2024-11-20 16:52:20 +01:00
Leendert de Borst
667592411f Update for 0.7.0 release (#389) 2024-11-20 16:51:48 +01:00
Leendert de Borst
dfdf4981cb Add LetsEncrypt ssl certificate generation to docker setup (#388)
* Add LetsEncrypt scaffolding to docker compose setup (#367)

* Update install.sh (#367)

* Add certificate request logic (#367)

* Update domain validation regex (#367)

* Update install.sh (#367)

* Update install.sh (#367)

* Update nginx.conf for LetsEncrypt validation (#367)

* Update nginx.conf (#367)

* Add certbot volume mapping to nginx (#367)

* Update nginx conf to template to use env vars (#367)

* Update nginx certbot root (#367)

* Update install.sh (#367)

* Update nginx ssl letsencrypt paths (#367)

* Update install.sh (#367)

* Use conditional nginx.conf include instead of vars (#367)

* Update install.sh so it doesn't restart docker stack but expects it to be running already (#367)

* Update permissions (#367)

* Update install.sh (#367)

* Refactor and cleanup (#367)
2024-11-20 16:25:35 +01:00
Leendert de Borst
0f377bdec6 Merge pull request #384 from lanedirt/383-add-try-catch-around-favicon-extractor-to-prevent-hostname-could-not-be-parsed-exceptions
Log failed favicon extraction as information instead of warning
2024-11-20 10:13:30 +01:00
Leendert de Borst
ba17474e62 Merge pull request #385 from lanedirt/370-from-is-always-empty-in-email-popup-in-client
Fix email from value which was empty
2024-11-20 10:13:22 +01:00
Leendert de Borst
c09ad99739 Merge pull request #387 from lanedirt/386-admin-menu-absolute-urls-do-not-work-when-ran-from-subdirectory
Update absolute urls to relative URLs in admin
2024-11-20 10:13:16 +01:00
Leendert de Borst
799efe1772 Update absolute urls to relative URLs in admin (#386) 2024-11-19 21:51:37 +01:00
Leendert de Borst
1d79400df5 Fix email from value which didn't show (#370) 2024-11-19 21:42:50 +01:00
Leendert de Borst
cc4a2e087f Update FaviconController to log failed favicon extraction as information instead of warning (#383) 2024-11-19 21:30:57 +01:00
Leendert de Borst
64a76f3b9f Merge pull request #381 from lanedirt/372-installsh-reset-password-throws-sed-notice-error
Fix bug in reset-password regex check
2024-11-18 20:28:28 +01:00
Leendert de Borst
7c1aaab291 Fix bug in reset-password regex check (#372) 2024-11-18 20:21:55 +01:00
Leendert de Borst
63556d163a Merge pull request #380 from lanedirt/374-publish-docker-images-on-release
Add -y flag to install.sh for uninstall action
2024-11-18 20:13:13 +01:00
Leendert de Borst
c49c0e4ad5 Update install.sh (#374) 2024-11-18 19:51:05 +01:00
Leendert de Borst
3f2121f272 Merge pull request #379 from lanedirt/374-publish-docker-images-on-release
Publish docker images on release
2024-11-18 19:42:55 +01:00
Leendert de Borst
ebdcf778be Update README.md (#374) 2024-11-18 19:00:10 +01:00
Leendert de Borst
fb669df9cf Update docs (#374) 2024-11-18 17:18:28 +01:00
Leendert de Borst
cedf7d0733 Update README.md (#374) 2024-11-18 17:13:17 +01:00
Leendert de Borst
00db83f478 Update github actions to use new install.sh (#374) 2024-11-18 16:39:09 +01:00
Leendert de Borst
03b7f92a44 Fix admin absolute redirect issues (#374) 2024-11-18 16:32:20 +01:00
Leendert de Borst
d542a4273d Fix DataProtection issues (#374) 2024-11-18 16:32:06 +01:00
Leendert de Borst
dcb27ca543 Update install.sh to generate/download external dependencies (#374) 2024-11-18 16:31:11 +01:00
Leendert de Borst
78635b8ba1 Combine all CLI actions to a single file (#374) 2024-11-18 13:06:10 +01:00
Leendert de Borst
e18d31ee9b Fix 404 dark mode text (#374) 2024-11-18 12:56:31 +01:00
Leendert de Borst
0db5fb64a8 Run install and build in verbose mode in workflows (#374) 2024-11-18 11:33:41 +01:00
Leendert de Borst
e36d28eb99 Update README (#374) 2024-11-18 11:33:18 +01:00
Leendert de Borst
dd331f75c9 Fix regex (#374) 2024-11-18 11:15:10 +01:00
dependabot[bot]
aa11697ee2 Bump NUnit.Analyzers from 4.3.0 to 4.4.0
Bumps [NUnit.Analyzers](https://github.com/nunit/nunit.analyzers) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/nunit/nunit.analyzers/releases)
- [Changelog](https://github.com/nunit/nunit.analyzers/blob/master/CHANGES.md)
- [Commits](https://github.com/nunit/nunit.analyzers/compare/4.3.0...4.4.0)

---
updated-dependencies:
- dependency-name: NUnit.Analyzers
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 11:05:37 +01:00
dependabot[bot]
fdd698dd0a Bump Microsoft.IdentityModel.Tokens from 8.2.0 to 8.2.1
Bumps [Microsoft.IdentityModel.Tokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 8.2.0 to 8.2.1.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/8.2.0...8.2.1)

---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.Tokens
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 11:05:29 +01:00
Leendert de Borst
c8df588401 Fix admin password check (#374) 2024-11-18 11:05:07 +01:00
dependabot[bot]
a8373338c2 Bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /src/AliasVault.Admin directory: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).
Bumps the npm_and_yarn group with 1 update in the /src/AliasVault.Client directory: [cross-spawn](https://github.com/moxystudio/node-cross-spawn).


Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

Updates `cross-spawn` from 7.0.3 to 7.0.5
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.5)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: cross-spawn
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 10:47:17 +01:00
Leendert de Borst
15abd1f51b Update workflows (#374) 2024-11-18 10:46:22 +01:00
Leendert de Borst
71407cc86d Publish InstallCli image used for resetting admin password (#374) 2024-11-18 10:46:00 +01:00
Leendert de Borst
85a3fed127 Create docker-compose-pull.yml (#374) 2024-11-18 10:44:05 +01:00
Leendert de Borst
6b8f0d6cdf Add separate install/build.sh files (#374) 2024-11-18 10:43:50 +01:00
Leendert de Borst
43441831d4 Convert repository name to lowercase (#374) 2024-11-18 09:41:39 +01:00
Leendert de Borst
319cff8fe1 Merge pull request #375 from lanedirt/374-publish-docker-images-on-release
Publish docker images on release
2024-11-18 09:38:47 +01:00
Leendert de Borst
5904204465 Add docker image publish workflow (#374) 2024-11-18 09:33:52 +01:00
Leendert de Borst
6c8cc92a67 Merge pull request #365 from lanedirt/364-update-docker-setup-to-run-https-by-default
Update docker setup to run https by default
2024-11-15 18:56:34 +01:00
Leendert de Borst
693860acef Update Dockerfile (#364) 2024-11-15 18:48:50 +01:00
Leendert de Borst
f7626ec15b Update ApiLoggingTests to set correct base url (#364) 2024-11-15 18:40:09 +01:00
Leendert de Borst
03c6bbc81f Update README.md (#364) 2024-11-15 17:27:57 +01:00
Leendert de Borst
bbe7ef1b2b Update install.sh (#364) 2024-11-15 17:25:59 +01:00
Leendert de Borst
027b95da15 Fix dataprotection certificate errors (#364) 2024-11-15 17:20:57 +01:00
Leendert de Borst
e9c33a808f Make apps work when run in local debug mode (#364) 2024-11-15 16:58:20 +01:00
Leendert de Borst
2545e1204f Update API and admin apps to be able to run under subdirectories (#364) 2024-11-15 16:58:20 +01:00
Leendert de Borst
970d334b59 Make all apps available through single container and HTTPS port (#364) 2024-11-15 16:58:20 +01:00
Leendert de Borst
50a18dc461 Add -k flag to ignore self-signed certs, refactor (#364) 2024-11-15 16:58:20 +01:00
Leendert de Borst
0dcc77eb0d Update docker-compose-build.yml (#364) 2024-11-15 16:58:20 +01:00
Leendert de Borst
cd84592be1 Fix AliasVault.InstallCli dockerfile names (#364) 2024-11-15 16:58:20 +01:00
Leendert de Borst
df6de32a4a Update docker setup to run under HTTPS by default (#364) 2024-11-15 16:58:20 +01:00
Leendert de Borst
3d24772caa Merge pull request #366 from lanedirt/362-tweak-dataprotection-certificate-tweaks-so-its-not-dependent-on-local-machine-keystore
Change DataProtection certificate generation so its not dependent on local machine keystore
2024-11-15 16:56:00 +01:00
Leendert de Borst
1a106e59fc Update CertificateGenerator.cs (#362) 2024-11-13 21:20:02 +01:00
Leendert de Borst
290460c095 Merge pull request #361 from lanedirt/360-upgrade-all-projects-to-net-9
Upgrade all projects to .NET 9
2024-11-13 17:55:55 +01:00
Leendert de Borst
17802dc216 Fix dataprotection, refactor (#360) 2024-11-13 17:24:03 +01:00
Leendert de Borst
0de52a396a Add .NET 9 to sonarcloud workflow explicitly (#360) 2024-11-13 17:06:15 +01:00
Leendert de Borst
64705e582d Update E2E github workflow to use new .NET 9 (#360) 2024-11-13 16:50:37 +01:00
Leendert de Borst
b09cdcec1e Fix E2E tests by switching to new KestrelTestServer (#360) 2024-11-13 16:44:44 +01:00
Leendert de Borst
87bb34f3ba Update dotnet version in github workflows (#360) 2024-11-13 14:27:50 +01:00
Leendert de Borst
5b53208a3e Update .gitignore to also ignore sqlite bak files (#360) 2024-11-13 14:25:57 +01:00
Leendert de Borst
7a687bba43 Update dockerfiles to use .NET9 (#360) 2024-11-13 14:16:16 +01:00
Leendert de Borst
aafac49bcb Disable DataProtection temporary (#360) 2024-11-13 12:48:11 +01:00
Leendert de Borst
201af7b88a Upgrade all projects to .NET 9 (#360) 2024-11-13 11:47:05 +01:00
117 changed files with 1900 additions and 1068 deletions

View File

@@ -1,7 +1,8 @@
API_URL=
HOSTNAME=
JWT_KEY=
DATA_PROTECTION_CERT_PASS=
ADMIN_PASSWORD_HASH=
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
PRIVATE_EMAIL_DOMAINS=
SMTP_TLS_ENABLED=false
LETSENCRYPT_ENABLED=false

View File

@@ -1,3 +1,4 @@
# This workflow will test if building the Docker Compose containers from scratch works.
name: Docker Compose Build
on:
@@ -20,7 +21,7 @@ jobs:
- name: Set permissions and run install.sh
run: |
chmod +x install.sh
./install.sh
./install.sh build --verbose
- name: Set up Docker Compose
run: |
@@ -32,29 +33,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 +88,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 '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
echo "Password reset output format is incorrect. Expected format: 'New admin password: <at least 8 base64 chars>'"
echo "Actual output: $output"
exit 1
else
echo "Password reset output format is correct"
fi

View File

@@ -0,0 +1,100 @@
# This workflow will test if pulling the latest Docker Compose containers from the registry works.
name: Docker Compose Pull
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
test-docker:
runs-on: ubuntu-latest
services:
docker:
image: docker:26.0.0
options: --privileged
steps:
- uses: actions/checkout@v2
- name: Set permissions and run install.sh
run: |
chmod +x install.sh
./install.sh install --verbose
- name: Set up Docker Compose
run: |
# Change the exposed host port of the SmtpService from 25 to 2525 because port 25 is not allowed in GitHub Actions
sed -i 's/25\:25/2525\:25/g' docker-compose.yml
docker compose -f docker-compose.yml up -d
- name: Wait for services to be up
run: |
# Wait for a few seconds
sleep 10
- name: Test if localhost:443 (WASM app) 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)
if [ "$http_code" -ne 200 ]; then
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:443/api (WebApi) 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/api)
if [ "$http_code" -ne 200 ]; then
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"
fi
- name: Test if localhost:2525 (SmtpService) responds
uses: nick-fields/retry@v3
with:
timeout_minutes: 2
max_attempts: 3
command: |
if ! nc -zv localhost 2525 2>&1 | grep -q 'succeeded'; then
echo "SmtpService did not respond on port 2525. Check if the SmtpService service is running."
exit 1
else
echo "SmtpService responded on port 2525"
fi
- name: Test install.sh reset-password output
run: |
output=$(./install.sh reset-password)
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
echo "Password reset output format is incorrect. Expected format: 'New admin password: <at least 8 base64 chars>'"
echo "Actual output: $output"
exit 1
else
echo "Password reset output format is correct"
fi

View File

@@ -1,3 +1,4 @@
# This workflow will test if running the E2E Admin tests via Playwright CLI works.
name: .NET E2E Admin Tests (Playwright)
on:
@@ -16,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.304
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet workload install wasm-tools
@@ -25,7 +26,7 @@ jobs:
run: dotnet build
- name: Ensure browsers are installed
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install --with-deps
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps
- name: Run AdminTests with retry
uses: nick-fields/retry@v3

View File

@@ -1,3 +1,4 @@
# This workflow will test if running the E2E Client tests via Playwright CLI works.
name: .NET E2E Client Tests (Playwright with Sharding)
on:
@@ -20,7 +21,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.304
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet workload install wasm-tools
@@ -29,7 +30,7 @@ jobs:
run: dotnet build
- name: Ensure browsers are installed
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install --with-deps
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps
- name: Run ClientTests with retry (Shard ${{ matrix.shard }})
uses: nick-fields/retry@v3

View File

@@ -1,3 +1,4 @@
# This workflow will test if running the E2E Misc tests via Playwright CLI works.
name: .NET E2E Misc Tests (Playwright)
on:
@@ -16,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.304
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet workload install wasm-tools
@@ -25,7 +26,7 @@ jobs:
run: dotnet build
- name: Ensure browsers are installed
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install --with-deps
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps
- name: Run remaining tests with retry
uses: nick-fields/retry@v3

View File

@@ -1,6 +1,4 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
# This workflow will test if running the integration tests works.
name: .NET Integration Tests
on:
@@ -19,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.304
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet workload install wasm-tools

View File

@@ -1,6 +1,4 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
# This workflow will test if running the unit tests works.
name: .NET Unit Tests
on:
@@ -18,7 +16,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.304
dotnet-version: 9.0.x
- name: Install dependencies
run: dotnet workload install wasm-tools

View File

@@ -0,0 +1,87 @@
# This workflow will publish new Docker images to the GitHub Container Registry when a new release is published.
name: Publish Docker Images
on:
release:
types: [published]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Convert repository name to lowercase
run: |
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}
- name: Build and push API image
uses: docker/build-push-action@v5
with:
context: .
file: src/AliasVault.Api/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-api:${{ github.ref_name }}
- name: Build and push Client image
uses: docker/build-push-action@v5
with:
context: .
file: src/AliasVault.Client/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-client:${{ github.ref_name }}
- name: Build and push Admin image
uses: docker/build-push-action@v5
with:
context: .
file: src/AliasVault.Admin/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:${{ github.ref_name }}
- name: Build and push SMTP image
uses: docker/build-push-action@v5
with:
context: .
file: src/Services/AliasVault.SmtpService/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:${{ github.ref_name }}
- name: Build and push Reverse Proxy image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:${{ github.ref_name }}
- name: Build and push InstallCli image
uses: docker/build-push-action@v5
with:
context: .
file: src/Utilities/AliasVault.InstallCli/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}

View File

@@ -1,3 +1,4 @@
# This workflow will perform a SonarCloud code analysis on every push to the main branch or when a pull request is opened, synchronized, or reopened.
name: SonarCloud code analysis
on:
push:
@@ -10,6 +11,14 @@ jobs:
name: Build and analyze
runs-on: windows-latest
steps:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0.x'
- name: Install WASM workload
run: dotnet workload install wasm-tools
- name: Set up JDK 17
uses: actions/setup-java@v3
with:

7
.gitignore vendored
View File

@@ -268,6 +268,7 @@ ServiceFabricBackup/
# SQLite files
*.sqlite
*.sqlite.*
*.sqlite-shm
*.sqlite-wal
@@ -392,3 +393,9 @@ src/Tests/AliasVault.E2ETests/appsettings.Development.json
# Draw.io diagram temp files
*.drawio.*
# Certificates
certificates/**/*.crt
certificates/**/*.key
certificates/**/*.pfx
certificates/**/*.pem
certificates/letsencrypt/**

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM nginx:alpine
# Install OpenSSL
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

@@ -10,7 +10,7 @@
Open-source password and alias manager
</h3>
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/OGameX/releases)
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/AliasVault/releases)
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-unit-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=integration tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
@@ -35,44 +35,67 @@ A live demo of the app is available at the official website at [app.aliasvault.n
<img width="700" alt="Screenshot of AliasVault" src="docs/img/screenshot.png">
## Installation
To install AliasVault on your local machine, follow the steps below. Note: the install process is tested on MacOS and Linux. It should work on Windows too, but you might need to adjust some commands.
### Requirements:
- Access to a terminal
- Docker
- Git
Choose one of the following installation methods:
### 1. Clone and run install script
AliasVault comes with a install script that prepares the .env file, builds the Docker image, and starts the AliasVault containers.
### Option 1: Quick Install (Pre-built Images)
This method uses pre-built Docker images and works on minimal hardware specifications:
- Linux (Ubuntu or RHEL based distros recommended)
- 512MB RAM
- 1 vCPU
- At least 16GB disk space
- Docker installed
```bash
# Clone this Git repository to "AliasVault" directory
$ git clone https://github.com/lanedirt/AliasVault.git
# Download install script
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/main/install.sh
# Go to the project directory
$ cd AliasVault
# Make install script executable and run it.
$ chmod +x install.sh && ./install.sh
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
chmod +x install.sh
./install.sh install
```
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.
### Option 2: Build from Source
### 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.
Building from source requires more resources:
- Minimum 2GB RAM (more RAM will speed up build time)
- At least 1 vCPU
- 40GB+ disk space (for dependencies and build artifacts)
- Docker installed
- Git installed
> Note: If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file.
```bash
# Clone the repository
git clone https://github.com/lanedirt/AliasVault.git
cd AliasVault
#### 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.
# Make build script executable and run it. This will create the .env file, build the Docker images from source, and start the AliasVault containers.
chmod +x install.sh
./install.sh build
```
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.
### Post-Installation
The install script will output the URL where the app is available. By default this is:
- Client: https://localhost
- Admin portal: https://localhost/admin
> 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.
#### First Time Setup Notes:
- When building from source for the first time, it may take several minutes for Docker to download and compile all dependencies. Subsequent builds will be faster.
- A SQLite database file will be created in `./database/AliasServerDb.sqlite`. This file will store all (encrypted) password vaults. It should be kept secure and not shared.
#### Other useful commands:
- To reset the admin password, run the install.sh script with the `--reset-admin-password` flag.
- To uninstall AliasVault, make the uninstall script executable with `chmod +x uninstall.sh` first, then run the script: `./uninstall.sh`.
This will remove all containers, images, and volumes related to AliasVault. It will keep all files and configuration intact however, so you can easily reinstall AliasVault later.
#### Useful Commands:
- To reset the admin password: `./install.sh reset-password`
- To uninstall AliasVault: `./install.sh uninstall`
This will remove all containers, images, and volumes related to AliasVault while keeping configuration files intact for future reinstallation.
- If something goes wrong you can run the install script in verbose mode to get more information: `./install.sh [command] --verbose`
## Security & Architecture
## Security Architecture
AliasVault takes security seriously and implements various measures to protect your data:
- All sensitive user data is encrypted end-to-end using industry-standard encryption algorithms. This includes the complete vault contents and all received emails.
@@ -81,7 +104,7 @@ AliasVault takes security seriously and implements various measures to protect y
For detailed information about our encryption implementation and security architecture, see the following documents:
- [SECURITY.md](SECURITY.md)
- [Security Architecture (Diagram)](docs/security-architecture.md)
- [Security Architecture Diagram](docs/security-architecture.md)
## Tech stack / credits
The following technologies, frameworks and libraries are used in this project:

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.

30
docker-compose.build.yml Normal file
View File

@@ -0,0 +1,30 @@
services:
reverse-proxy:
image: aliasvault-reverse-proxy
build:
context: .
dockerfile: Dockerfile
client:
image: aliasvault-client
build:
context: .
dockerfile: src/AliasVault.Client/Dockerfile
api:
image: aliasvault-api
build:
context: .
dockerfile: src/AliasVault.Api/Dockerfile
admin:
image: aliasvault-admin
build:
context: .
dockerfile: src/AliasVault.Admin/Dockerfile
smtp:
image: aliasvault-smtp
build:
context: .
dockerfile: src/Services/AliasVault.SmtpService/Dockerfile

View File

@@ -0,0 +1,7 @@
services:
certbot:
image: certbot/certbot
volumes:
- ./certificates/letsencrypt:/etc/letsencrypt:rw
- ./certificates/letsencrypt/www:/var/www/certbot:rw
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

View File

@@ -1,49 +1,58 @@
services:
admin:
image: aliasvault-admin
build:
context: .
dockerfile: src/AliasVault.Admin/Dockerfile
reverse-proxy:
image: ghcr.io/lanedirt/aliasvault-reverse-proxy:latest
ports:
- "8080:8082"
- "80:80"
- "443:443"
volumes:
- ./certificates:/certificates:rw
- ./database:/database:rw
- ./logs:/logs:rw
- ./certificates/ssl:/etc/nginx/ssl:rw
- ./certificates/letsencrypt:/etc/nginx/ssl-letsencrypt:rw
- ./certificates/letsencrypt/www:/var/www/certbot: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"
image: ghcr.io/lanedirt/aliasvault-client:latest
volumes:
- ./logs/msbuild:/src/msbuild-logs:rw
expose:
- "3000"
restart: always
env_file:
- .env
api:
image: aliasvault-api
build:
context: .
dockerfile: src/AliasVault.Api/Dockerfile
ports:
- "81:8081"
image: ghcr.io/lanedirt/aliasvault-api:latest
expose:
- "3001"
volumes:
- ./certificates:/certificates:rw
- ./database:/database:rw
- ./certificates/app:/certificates/app:rw
- ./logs:/logs:rw
env_file:
- .env
restart: always
admin:
image: ghcr.io/lanedirt/aliasvault-admin:latest
expose:
- "3002"
volumes:
- ./database:/database:rw
- ./certificates/app:/certificates/app:rw
- ./logs:/logs:rw
restart: always
env_file:
- .env
smtp:
image: aliasvault-smtp
build:
context: .
dockerfile: src/Services/AliasVault.SmtpService/Dockerfile
image: ghcr.io/lanedirt/aliasvault-smtp:latest
ports:
- "25:25"
- "587:587"
@@ -53,3 +62,7 @@ services:
env_file:
- .env
restart: always
networks:
aliasvault:
name: aliasvault_default

View File

@@ -9,91 +9,114 @@ This README provides step-by-step instructions for manually setting up AliasVaul
## Steps
1. **Create .env file**
1. **Create required directories**
Create the following directories in your project root:
```bash
mkdir -p certificates/ssl certificates/app database logs/msbuild
```
2. **Create .env file**
Copy the `.env.example` file to create a new `.env` file:
```
```bash
cp .env.example .env
```
2. **Generate and set JWT_KEY**
3. **Set HOSTNAME**
Update the .env file and set the JWT_KEY environment variable to a random 32-char string. This key is used for JWT token generation and should be kept secure.
Generate a random 32 char string for the JWT:
Update the .env file with your hostname (default is localhost):
```bash
HOSTNAME=localhost
```
4. **Generate and set JWT_KEY**
Generate a random 32-char string for JWT token generation:
```bash
openssl rand -base64 32
```
Add the generated key to the .env file:
Add the generated key to the .env file:
```bash
JWT_KEY=your_generated_key_here
```
JWT_KEY=your_32_char_string_here
3. **Set PRIVATE_EMAIL_DOMAINS**
5. **Generate and set DATA_PROTECTION_CERT_PASS**
Update the .env file and set the PRIVATE_EMAIL_DOMAINS value the allowed domains that can be used for email addresses. Separate multiple domains with commas.
Generate a random password for the data protection certificate:
```bash
openssl rand -base64 32
```
Add it to the .env file:
```bash
DATA_PROTECTION_CERT_PASS=your_generated_password_here
```
6. **Set PRIVATE_EMAIL_DOMAINS**
Update the .env file with allowed email domains. Use DISABLED.TLD to disable email support:
```bash
PRIVATE_EMAIL_DOMAINS=yourdomain.com,anotherdomain.com
```
Replace `yourdomain.com,anotherdomain.com` with your actual allowed domains.
4. **Set SMTP_TLS_ENABLED**
Decide whether to enable TLS for email and add it to the .env file:
Or to disable email:
```bash
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
```
SMTP_TLS_ENABLED=true
7. **Set SUPPORT_EMAIL (Optional)**
Add a support email address if desired:
```bash
SUPPORT_EMAIL=support@yourdomain.com
```
Or set it to `false` if you don't want to enable TLS.
5. **Generate admin password**
Set the admin password hash in the .env file. The password hash is generated using the `InitializationCLI` utility.
8. **Generate admin password**
Build the Docker image for password hashing:
```
docker build -t initcli -f src/Utilities/InitializationCLI/Dockerfile .
```bash
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile .
```
Generate the password hash:
```
docker run --rm initcli "<your_prefered_admin_password_here>"
```bash
docker run --rm installcli "your_preferred_admin_password_here"
```
Add the password hash and generation timestamp to the .env file:
```
ADMIN_PASSWORD_HASH=<output_of_step_above>
```bash
ADMIN_PASSWORD_HASH=<output_from_previous_command>
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
```
6. **Build and start Docker containers**
9. **Build and start Docker containers**
Build the Docker Compose stack:
```
docker-compose build
```
Build the Docker Compose stack:
```bash
docker compose build
```
Start the Docker Compose stack:
```
docker-compose up -d
```
Start the Docker Compose stack:
```bash
docker compose up -d
```
7. **Access AliasVault**
10. **Access AliasVault**
AliasVault should now be running. You can access it as follows:
AliasVault should now be running. You can access it at:
- Admin Panel: http://localhost:8080/
- Admin Panel: https://localhost/admin
- Username: admin
- Password: [Use the ADMIN_PASSWORD generated in step 5]
- Password: [Use the password you set in step 8]
- Client Website: http://localhost:80/
- Client Website: https://localhost/
- Create your own account from here
## Important Notes
- Make sure to save the admin password (ADMIN_PASSWORD) generated in step 5 in a secure location. It won't be shown again.
- If you need to reset the admin password in the future, you'll need to generate a new hash and update the .env file manually.
Afterwards restart the docker containers which will update the admin password in the database.
- Make sure to save the admin password you used in step 8 in a secure location.
- If you need to reset the admin password in the future, repeat step 8 and restart the Docker containers.
- Always keep your .env file secure and do not share it, as it contains sensitive information.
## Troubleshooting
@@ -101,10 +124,10 @@ Afterwards restart the docker containers which will update the admin password in
If you encounter any issues during the setup:
1. Check the Docker logs:
```bash
docker compose logs
```
docker-compose logs
```
2. Ensure all required ports (8080 and 80) are available and not being used by other services.
2. Ensure all required ports (80 and 443) are available and not being used by other services.
3. Verify that all environment variables in the .env file are set correctly.
For further assistance, please refer to the project documentation or seek support through the appropriate channels.

33
entrypoint.sh Normal file
View File

@@ -0,0 +1,33 @@
#!/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
# Create the appropriate SSL configuration based on LETSENCRYPT_ENABLED
if [ "${LETSENCRYPT_ENABLED}" = "true" ]; then
cat > /etc/nginx/ssl.conf << EOF
ssl_certificate /etc/nginx/ssl-letsencrypt/live/${HOSTNAME}/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl-letsencrypt/live/${HOSTNAME}/privkey.pem;
EOF
else
cat > /etc/nginx/ssl.conf << EOF
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
EOF
fi
# Start nginx
nginx -g "daemon off;"

1153
install.sh
View File

File diff suppressed because it is too large Load Diff

100
nginx.conf Normal file
View File

@@ -0,0 +1,100 @@
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 _;
# Handle ACME challenge for Let's Encrypt certificate validation
location /.well-known/acme-challenge/ {
allow all;
root /var/www/certbot;
try_files $uri =404;
default_type "text/plain";
add_header Cache-Control "no-cache";
break;
}
# Redirect all other HTTP traffic to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name _;
# Include the appropriate SSL certificate configuration generated
# by the entrypoint script.
include /etc/nginx/ssl.conf;
# 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;
}
# 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;
}
}
}

View File

@@ -1,27 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-AliasVault.Admin-1DAADE35-C01B-43BB-B440-AA5E1E0B672D</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<NoWarn>1701;1702;NU1900</NoWarn>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Admin.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.Admin.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.Admin.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.Admin.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.10">
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

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

@@ -72,7 +72,7 @@ public class AuthBase : OwningComponentBase
// Redirect to home if the user is already authenticated
if (SignInManager.IsSignedIn(user))
{
NavigationService.RedirectTo("/");
NavigationService.RedirectTo("./");
}
}
}

View File

@@ -29,7 +29,7 @@
<div class="ml-3 text-sm">
<label for="remember" class="font-medium text-gray-900 dark:text-white">Remember me</label>
</div>
<a href="/user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">Lost Password?</a>
<a href="user/forgot-password" class="ml-auto text-sm text-primary-700 hover:underline dark:text-primary-500">Lost Password?</a>
</div>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Login to your account</button>

View File

@@ -34,7 +34,7 @@
catch
{
// Redirect to the home page with hard refresh.
NavigationService.RedirectTo("/", true);
NavigationService.RedirectTo("./", true);
}
}
}

View File

@@ -16,7 +16,7 @@ public class Config
/// Gets or sets the admin password hash which is generated by install.sh and will be set
/// as the default password for the admin user.
/// </summary>
public string AdminPasswordHash { get; set; } = "false";
public string AdminPasswordHash { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the last time the password was changed. This is used to check if the

View File

@@ -1,22 +1,18 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8082
EXPOSE 3002
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 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>
@@ -75,27 +75,27 @@
<nav class="bg-white dark:bg-gray-900">
<ul id="mobileMenu" class="flex-col mt-0 pt-16 w-full text-sm font-medium lg:hidden">
<li class="block border-b dark:border-gray-700">
<NavLink href="/" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="./" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Home
</NavLink>
</li>
<li class="block border-b dark:border-gray-700">
<NavLink href="/users" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="users" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Users
</NavLink>
</li>
<li class="block border-b dark:border-gray-700">
<NavLink href="/emails" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="emails" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Emails
</NavLink>
</li>
<li class="block border-b dark:border-gray-700">
<NavLink href="/logging/general" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="logging/general" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
General logs
</NavLink>
</li>
<li class="block border-b dark:border-gray-700">
<NavLink href="/logging/auth" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
<NavLink href="logging/auth" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Auth logs
</NavLink>
</li>

View File

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

View File

@@ -89,11 +89,11 @@ else
GlobalNotificationService.AddSuccessMessage("User successfully deleted.");
GlobalLoadingSpinner.Hide();
NavigationService.RedirectTo("/users");
NavigationService.RedirectTo("users");
}
private void Cancel()
{
NavigationService.RedirectTo("/users/" + Id);
NavigationService.RedirectTo("users/" + Id);
}
}

View File

@@ -16,7 +16,7 @@ else
Description="View details of the user below.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<LinkButton Color="danger" Href="@($"/users/{Id}/delete")" Text="Delete user" />
<LinkButton Color="danger" Href="@($"users/{Id}/delete")" Text="Delete user" />
</CustomActions>
</PageHeader>
@@ -133,7 +133,7 @@ else
{
// Error loading user.
GlobalNotificationService.AddErrorMessage("This user does not exist (anymore). Please try again.");
NavigationService.RedirectTo("/users");
NavigationService.RedirectTo("users");
return;
}

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;
@@ -32,7 +33,7 @@ var adminPasswordHash = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_HASH"
config.AdminPasswordHash = adminPasswordHash;
var lastPasswordChanged = Environment.GetEnvironmentVariable("ADMIN_PASSWORD_GENERATED") ?? throw new KeyNotFoundException("ADMIN_PASSWORD_GENERATED environment variable is not set.");
config.LastPasswordChanged = DateTime.ParseExact(lastPasswordChanged, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
config.LastPasswordChanged = DateTime.Parse(lastPasswordChanged, CultureInfo.InvariantCulture);
builder.Services.AddSingleton(config);
@@ -85,7 +86,6 @@ builder.Services.AddIdentityCore<AdminUser>(options =>
.AddDefaultTokenProviders();
builder.Services.AddAliasVaultDataProtection("AliasVault.Admin");
builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromDays(30);
@@ -105,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

@@ -72,7 +72,6 @@ public static class StartupTasks
// Clear existing recovery codes
await userManager.GenerateNewTwoFactorRecoveryCodesAsync(adminUser, 0);
await userManager.UpdateAsync(adminUser);
Console.WriteLine("Admin password hash updated.");

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

@@ -357,9 +357,9 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
"integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",

View File

@@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AliasVault.Api</RootNamespace>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<DefineConstants Condition="'$(E2ETEST)' == 'true'">$(DefineConstants);E2ETEST</DefineConstants>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -21,14 +22,14 @@
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
</ItemGroup>
<ItemGroup>

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

@@ -44,6 +44,7 @@ public class EmailController(ILogger<VaultController> logger, IDbContextFactory<
{
Id = email!.Id,
Subject = email.Subject,
FromDisplay = email.From,
FromDomain = email.FromDomain,
FromLocal = email.FromLocal,
ToDomain = email.ToDomain,

View File

@@ -18,8 +18,9 @@ using Microsoft.AspNetCore.Mvc;
/// Controller for retrieving favicons from external websites.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
/// <param name="logger">Logger instance.</param>
[ApiVersion("1")]
public class FaviconController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
public class FaviconController(UserManager<AliasVaultUser> userManager, ILogger<FaviconController> logger) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Proxies the request to the identity generator to generate a random identity.
@@ -36,9 +37,22 @@ public class FaviconController(UserManager<AliasVaultUser> userManager) : Authen
}
// Get the favicon from the URL.
var image = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(url);
try
{
var image = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(url);
// Return the favicon as base64 string of image representation.
return Ok(new FaviconExtractModel { Image = image });
// Return the favicon as base64 string of image representation.
return Ok(new FaviconExtractModel { Image = image });
}
catch (Exception ex)
{
// Anonymize the URL by replacing all a-Z characters with 'x' before logging.
// This will still allow to see the host structure but not the actual domain.
var anonymizedUrl = new string(url.Select(c => char.IsLetter(c) ? 'x' : c).ToArray());
logger.LogInformation(ex, "Failed to extract favicon from {Url}", anonymizedUrl);
}
// Return null if favicon extraction failed.
return Ok(new FaviconExtractModel { Image = null });
}
}

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:8.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8081
EXPOSE 3001
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 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

@@ -1,23 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<RootNamespace>AliasVault.Client</RootNamespace>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<BuildVersion>$([System.DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))</BuildVersion>
<WasmBuildNative>true</WasmBuildNative>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Client.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.Client.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CacheBuster>dev</CacheBuster>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugSymbols>true</DebugSymbols>
<DocumentationFile>bin\Release\net8.0\AliasVault.Client.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.Client.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Optimize>True</Optimize>
<CacheBuster>$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss"))</CacheBuster>
@@ -48,15 +49,16 @@
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,7 +9,7 @@
</Found>
<NotFound>
<LayoutView Layout="@typeof(Auth.Layout.MainLayout)">
<p>Sorry, there's nothing at this address.</p>
<p class="text-gray-500 dark:text-gray-400">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

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

@@ -1,15 +1,14 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet: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
# 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

@@ -26,7 +26,7 @@
</button>
</div>
<div class="mt-4">
<p class="text-sm text-gray-500 dark:text-gray-400">From: @Email?.FromDisplay</p>
<p class="text-sm text-gray-500 dark:text-gray-400">From: @(Email?.FromLocal)@@@(Email?.FromDomain)</p>
<p class="text-sm text-gray-500 dark:text-gray-400">To: @(Email?.ToLocal)@@@(Email?.ToDomain)</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Date: @Email?.DateSystem</p>
</div>
@@ -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

@@ -11,12 +11,21 @@
</div>
@code {
/// <summary>
/// Title tag text of the loading indicator.
/// </summary>
[Parameter]
public string Title { get; set; } = string.Empty;
/// <summary>
/// Whether the loading indicator is spinning.
/// </summary>
[Parameter]
public bool Spinning { get; set; } = true;
/// <summary>
/// The content to display inside the loading indicator.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
}

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

@@ -93,7 +93,7 @@ public static class DbMergeUtility
// Record exists, compare UpdatedAt if it exists.
logger.LogDebug("Comparing UpdatedAt in {tableName}.", tableName);
logger.LogDebug("UpdatedAt: {existingRecord}", existingRecord);
var baseUpdatedAt = DateTime.Parse((string)existingRecord, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
var baseUpdatedAt = DateTime.Parse((string)existingRecord, CultureInfo.InvariantCulture);
if (updatedAt > baseUpdatedAt)
{
// Source record is newer, update the base record.

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

@@ -357,9 +357,9 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
"integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",

View File

@@ -638,18 +638,6 @@ video {
bottom: 0.25rem;
}
.-right-12 {
right: -3rem;
}
.-right-2 {
right: -0.5rem;
}
.-top-1 {
top: -0.25rem;
}
.-top-2 {
top: -0.5rem;
}
@@ -690,22 +678,6 @@ video {
top: 5rem;
}
.-right-10 {
right: -2.5rem;
}
.-right-8 {
right: -2rem;
}
.right-2 {
right: 0.5rem;
}
.right-4 {
right: 1rem;
}
.z-10 {
z-index: 10;
}
@@ -845,10 +817,6 @@ video {
margin-top: 2.5rem;
}
.mt-12 {
margin-top: 3rem;
}
.mt-2 {
margin-top: 0.5rem;
}
@@ -1040,10 +1008,6 @@ video {
min-width: 100%;
}
.min-w-\[3rem\] {
min-width: 3rem;
}
.max-w-7xl {
max-width: 80rem;
}
@@ -1068,11 +1032,6 @@ video {
flex-grow: 1;
}
.translate-x-2 {
--tw-translate-x: 0.5rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
@@ -1594,16 +1553,6 @@ video {
padding: 2rem;
}
.px-1 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px-1\.5 {
padding-left: 0.375rem;
padding-right: 0.375rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@@ -1718,10 +1667,6 @@ video {
padding-top: 2rem;
}
.pe-14 {
padding-inline-end: 3.5rem;
}
.text-left {
text-align: left;
}
@@ -1766,6 +1711,10 @@ video {
line-height: 1;
}
.text-\[10px\] {
font-size: 10px;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
@@ -1791,18 +1740,6 @@ video {
line-height: 1rem;
}
.text-\[10px\] {
font-size: 10px;
}
.text-\[11px\] {
font-size: 11px;
}
.text-\[9px\] {
font-size: 9px;
}
.font-bold {
font-weight: 700;
}

View File

@@ -1,32 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasClientDb.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasClientDb.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
<DocumentationFile>bin\Release\net8.0\AliasClientDb.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasClientDb.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -16,20 +17,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10">
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -248,6 +248,19 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
optionsBuilder
.UseSqlite(configuration.GetConnectionString("AliasServerDbContext"))
.UseLazyLoadingProxies();
// Set busy timeout using PRAGMA to avoid "The database file is locked" error.
var connection = Database.GetDbConnection();
if (connection.State != System.Data.ConnectionState.Open)
{
connection.Open();
}
using (var command = connection.CreateCommand())
{
command.CommandText = "PRAGMA busy_timeout = 5000;";
command.ExecuteNonQuery();
}
}
}
}

View File

@@ -1,18 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Generators.Identity.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.Generators.Identity.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.Generators.Identity.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.Generators.Identity.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

View File

@@ -1,18 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Generators.Password.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.Generators.Password.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.Generators.Password.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.Generators.Password.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

View File

@@ -1,22 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-AliasVault.SmtpService-eaac287e-32a7-4ff9-bbf9-1925c446ef73</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..\..</DockerfileContext>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Debug\net8.0\AliasVault.SmtpService.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.SmtpService.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Release\net8.0\AliasVault.SmtpService.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.SmtpService.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -24,7 +25,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="MimeKit" Version="4.8.0" />
<PackageReference Include="NUglify" Version="1.21.10" />

View File

@@ -1,7 +1,7 @@
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

View File

@@ -97,9 +97,8 @@ builder.Services.AddSingleton(
// If we don't do this, the certificate will be loaded without the private key and
// will throw error on Windows:
// "The TLS server credential's certificate does not have a private key information property attached to it"
cert = new X509Certificate2(cert.Export(X509ContentType.Pfx));
return cert;
var certBytes = cert.Export(X509ContentType.Pfx, "password");
return X509CertificateLoader.LoadPkcs12(certBytes, "password", X509KeyStorageFlags.DefaultKeySet);
}
});

View File

@@ -1,18 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.RazorComponents.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.RazorComponents.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.RazorComponents.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.RazorComponents.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
@@ -22,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -27,11 +27,13 @@
}
@code {
/// <inheritdoc/>
protected override void OnInitialized()
{
ModalService.OnChange += StateHasChanged;
}
/// <inheritdoc/>
public void Dispose()
{
ModalService.OnChange -= StateHasChanged;

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

@@ -1,18 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Shared.Core.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.Shared.Core.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.Shared.Core.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.Shared.Core.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

View File

@@ -25,7 +25,7 @@ public static class AppInfo
/// <summary>
/// Gets the minor version number.
/// </summary>
public const int VersionMinor = 6;
public const int VersionMinor = 7;
/// <summary>
/// Gets the patch version number.

View File

@@ -1,18 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.Shared.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.Shared.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.Shared.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.Shared.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
@@ -21,7 +22,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.70" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,22 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Debug\net8.0\AliasVault.E2ETests.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.E2ETests.xml</DocumentationFile>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Release\net8.0\AliasVault.E2ETests.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.E2ETests.xml</DocumentationFile>
<DefineConstants>TRACE</DefineConstants>
</PropertyGroup>
@@ -27,11 +28,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.3.0">
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -14,8 +14,8 @@ using AliasServerDb;
/// </summary>
public class AdminPlaywrightTest : PlaywrightTest
{
private static readonly int _basePort = 5700;
private static int _currentPort = _basePort;
private const int BasePort = 5700;
private static int _currentPort = BasePort;
/// <summary>
/// For starting the Admin project in-memory.
@@ -58,7 +58,7 @@ public class AdminPlaywrightTest : PlaywrightTest
AppBaseUrl = "http://localhost:" + appPort + "/";
// Start Admin project in-memory.
_webAppFactory.HostUrl = "http://localhost:" + appPort;
_webAppFactory.Port = appPort;
_webAppFactory.CreateDefaultClient();
await SetupPlaywrightBrowserAndContext();

View File

@@ -16,8 +16,8 @@ using Microsoft.Playwright;
/// </summary>
public class ClientPlaywrightTest : PlaywrightTest
{
private static readonly int _basePort = 5600;
private static int _currentPort = _basePort;
private const int BasePort = 5600;
private static int _currentPort = BasePort;
/// <summary>
/// For starting the WebAPI project in-memory.
@@ -85,11 +85,11 @@ public class ClientPlaywrightTest : PlaywrightTest
ApiBaseUrl = "http://localhost:" + apiPort + "/";
// Start WebAPI in-memory.
_apiFactory.HostUrl = "http://localhost:" + apiPort;
_apiFactory.Port = apiPort;
_apiFactory.CreateDefaultClient();
// Start Blazor WASM in-memory.
_clientFactory.HostUrl = "http://localhost:" + appPort;
_clientFactory.Port = appPort;
_clientFactory.CreateDefaultClient();
await SetupPlaywrightBrowserAndContext();

View File

@@ -0,0 +1,66 @@
//-----------------------------------------------------------------------
// <copyright file="KestrelTestServer.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.E2ETests.Infrastructure;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
/// <summary>
/// A <see cref="TestServer"/> that uses Kestrel as the server.
/// </summary>
public class KestrelTestServer : TestServer, IServer
{
private readonly KestrelServer _server;
/// <summary>
/// Initializes a new instance of the <see cref="KestrelTestServer"/> class.
/// </summary>
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> to use.</param>
public KestrelTestServer(IServiceProvider serviceProvider)
: base(serviceProvider)
{
// We get all the transport factories registered, and the first one is the correct one
// Getting the IConnectionListenerFactory directly from the service provider does not work
var transportFactory = serviceProvider.GetRequiredService<IEnumerable<IConnectionListenerFactory>>().First();
var kestrelOptions = serviceProvider.GetRequiredService<IOptions<KestrelServerOptions>>();
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
_server = new KestrelServer(kestrelOptions, transportFactory, loggerFactory);
}
/// <inheritdoc />
async Task IServer.StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
{
// We need to also invoke the TestServer's StartAsync method to ensure that the test server is started
// Because the TestServer's StartAsync method is implemented explicitly, we need to use reflection to invoke it
await InvokeExplicitInterfaceMethod(nameof(IServer.StartAsync), typeof(TContext), [application, cancellationToken]);
// We also start the Kestrel server in order for localhost to work
await _server.StartAsync(application, cancellationToken);
}
/// <inheritdoc />
async Task IServer.StopAsync(CancellationToken cancellationToken)
{
await InvokeExplicitInterfaceMethod(nameof(IServer.StopAsync), null, [cancellationToken]);
await _server.StopAsync(cancellationToken);
}
private Task InvokeExplicitInterfaceMethod(string methodName, Type? genericParameter, object[] args)
{
var baseMethod = typeof(TestServer).GetInterfaceMap(typeof(IServer)).TargetMethods.First(m => m.Name.EndsWith(methodName));
var method = genericParameter == null ? baseMethod : baseMethod.MakeGenericMethod(genericParameter);
var task = method.Invoke(this, args) as Task ?? throw new InvalidOperationException("Task not returned");
return task;
}
}

View File

@@ -11,6 +11,7 @@ using System.Data.Common;
using AliasServerDb;
using AliasVault.Admin.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
@@ -49,9 +50,9 @@ public class WebApplicationAdminFactoryFixture<TEntryPoint> : WebApplicationFact
}
/// <summary>
/// Gets or sets the URL the web application host will listen on.
/// Gets or sets the port the web application kestrel host will listen on.
/// </summary>
public string HostUrl { get; set; } = "https://localhost:5003";
public int Port { get; set; } = 5003;
/// <summary>
/// Returns the DbContext instance for the test. This can be used to seed the database with test data.
@@ -82,28 +83,23 @@ public class WebApplicationAdminFactoryFixture<TEntryPoint> : WebApplicationFact
/// <inheritdoc />
protected override IHost CreateHost(IHostBuilder builder)
{
var dummyHost = builder.Build();
builder.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder.UseKestrel(opt => opt.ListenLocalhost(Port));
webHostBuilder.ConfigureServices(s => s.AddSingleton<IServer, KestrelTestServer>());
});
builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());
var host = builder.Build();
host.Start();
var host = base.CreateHost(builder);
// Get the DbContextFactory instance and store it for later use during tests.
_dbContextFactory = host.Services.GetRequiredService<IDbContextFactory<AliasServerDbContext>>();
// This delay prevents "ERR_CONNECTION_REFUSED" errors
// which happened like 1 out of 10 times when running tests.
Thread.Sleep(100);
return dummyHost;
return host;
}
/// <inheritdoc />
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseUrls(HostUrl);
SetEnvironmentVariables();
builder.ConfigureServices(services =>
@@ -129,19 +125,13 @@ public class WebApplicationAdminFactoryFixture<TEntryPoint> : WebApplicationFact
/// <param name="services">The <see cref="IServiceCollection"/> to modify.</param>
private static void RemoveExistingRegistrations(IServiceCollection services)
{
var descriptorsToRemove = new[]
{
services.SingleOrDefault(d => d.ServiceType == typeof(IDbContextFactory<AliasServerDbContext>)),
services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AliasServerDbContext>)),
services.SingleOrDefault(d => d.ServiceType == typeof(VersionedContentService)),
};
var descriptorsToRemove = services.Where(d =>
d.ServiceType.ToString().Contains("AliasServerDbContext") ||
d.ServiceType == typeof(VersionedContentService)).ToList();
foreach (var descriptor in descriptorsToRemove)
{
if (descriptor != null)
{
services.Remove(descriptor);
}
services.Remove(descriptor);
}
}

View File

@@ -11,6 +11,7 @@ using System.Data.Common;
using AliasServerDb;
using AliasVault.Shared.Providers.Time;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
@@ -49,9 +50,9 @@ public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactor
}
/// <summary>
/// Gets or sets the URL the web application host will listen on.
/// Gets or sets the port the web application kestrel host will listen on.
/// </summary>
public string HostUrl { get; set; } = "https://localhost:5001";
public int Port { get; set; } = 5001;
/// <summary>
/// Gets the time provider instance for mutating the current time in tests.
@@ -87,28 +88,23 @@ public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactor
/// <inheritdoc />
protected override IHost CreateHost(IHostBuilder builder)
{
var dummyHost = builder.Build();
builder.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder.UseKestrel(opt => opt.ListenLocalhost(Port));
webHostBuilder.ConfigureServices(s => s.AddSingleton<IServer, KestrelTestServer>());
});
builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());
var host = builder.Build();
host.Start();
var host = base.CreateHost(builder);
// Get the DbContextFactory instance and store it for later use during tests.
_dbContextFactory = host.Services.GetRequiredService<IDbContextFactory<AliasServerDbContext>>();
// This delay prevents "ERR_CONNECTION_REFUSED" errors
// which happened like 1 out of 10 times when running tests.
Thread.Sleep(100);
return dummyHost;
return host;
}
/// <inheritdoc />
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseUrls(HostUrl);
SetEnvironmentVariables();
builder.ConfigureServices(services =>
@@ -133,19 +129,13 @@ public class WebApplicationApiFactoryFixture<TEntryPoint> : WebApplicationFactor
/// <param name="services">The <see cref="IServiceCollection"/> to modify.</param>
private static void RemoveExistingRegistrations(IServiceCollection services)
{
var descriptorsToRemove = new[]
{
services.SingleOrDefault(d => d.ServiceType == typeof(IDbContextFactory<AliasServerDbContext>)),
services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<AliasServerDbContext>)),
services.SingleOrDefault(d => d.ServiceType == typeof(ITimeProvider)),
};
var descriptorsToRemove = services.Where(d =>
d.ServiceType.ToString().Contains("AliasServerDbContext") ||
d.ServiceType == typeof(ITimeProvider)).ToList();
foreach (var descriptor in descriptorsToRemove)
{
if (descriptor != null)
{
services.Remove(descriptor);
}
services.Remove(descriptor);
}
}

View File

@@ -8,7 +8,9 @@
namespace AliasVault.E2ETests.Infrastructure;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
/// <summary>
@@ -19,30 +21,19 @@ public class WebApplicationClientFactoryFixture<TEntryPoint> : WebApplicationFac
where TEntryPoint : class
{
/// <summary>
/// Gets or sets the URL the web application host will listen on.
/// Gets or sets the port the web application kestrel host will listen on.
/// </summary>
public string HostUrl { get; set; } = "https://localhost:5002";
/// <inheritdoc />
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseUrls(HostUrl);
}
public int Port { get; set; } = 5002;
/// <inheritdoc />
protected override IHost CreateHost(IHostBuilder builder)
{
var dummyHost = builder.Build();
builder.ConfigureWebHost(webHostBuilder =>
{
webHostBuilder.UseKestrel(opt => opt.ListenLocalhost(Port));
webHostBuilder.ConfigureServices(s => s.AddSingleton<IServer, KestrelTestServer>());
});
builder.ConfigureWebHost(webHostBuilder => webHostBuilder.UseKestrel());
var host = builder.Build();
host.Start();
// This delay prevents "ERR_CONNECTION_REFUSED" errors
// which happened like 1 out of 10 times when running tests.
Thread.Sleep(100);
return dummyHost;
return base.CreateHost(builder);
}
}

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

@@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DocumentationFile>bin\Debug\net8.0\AliasVault.IntegrationTests.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.IntegrationTests.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\net8.0\AliasVault.IntegrationTests.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.IntegrationTests.xml</DocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
@@ -24,7 +24,7 @@
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
<PackageReference Include="NUnit" Version="4.2.2"/>
<PackageReference Include="NUnit.Analyzers" Version="4.3.0"/>
<PackageReference Include="NUnit.Analyzers" Version="4.4.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>

View File

@@ -1,23 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>AliasVault.Tests</RootNamespace>
<LangVersion>13</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Debug\net8.0\AliasVault.UnitTests.xml</DocumentationFile>
<DocumentationFile>bin\Debug\net9.0\AliasVault.UnitTests.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Release\net8.0\AliasVault.UnitTests.xml</DocumentationFile>
<DocumentationFile>bin\Release\net9.0\AliasVault.UnitTests.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -34,7 +34,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.3.0">
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

Some files were not shown because too many files have changed in this diff Show More