mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a759091755 | ||
|
|
8dc99c09a8 | ||
|
|
b9ec4baf66 | ||
|
|
71ed62cdcb | ||
|
|
2bbad8c75c | ||
|
|
f02b841eea | ||
|
|
f6fc5af8ac | ||
|
|
1d1155bf0e | ||
|
|
2632211af6 | ||
|
|
05cca6998e | ||
|
|
c4a8a20a62 | ||
|
|
f2c6af9ccb | ||
|
|
e94201acda | ||
|
|
9e03473208 | ||
|
|
0c5b2fb1da | ||
|
|
a5c4a7618d | ||
|
|
70220cecbb | ||
|
|
c63faa352f | ||
|
|
7e261a05c9 | ||
|
|
545ec5576e | ||
|
|
73dcbe5860 | ||
|
|
13917444b9 | ||
|
|
119e13a9dd | ||
|
|
7d656e9a9a | ||
|
|
8bd05b5c2e | ||
|
|
1e65f14323 | ||
|
|
2c7543889d | ||
|
|
63c5483208 | ||
|
|
2586d61651 | ||
|
|
c7a32cf0e9 | ||
|
|
46cc6527aa | ||
|
|
ef291bffc1 | ||
|
|
94f6199e27 | ||
|
|
5ababf3bf3 | ||
|
|
b47e735e8f | ||
|
|
de17303085 | ||
|
|
635136d257 | ||
|
|
832e340b1b | ||
|
|
4e0b6b5adf | ||
|
|
18be105350 | ||
|
|
9bea01fbf8 | ||
|
|
a33fd08cb4 | ||
|
|
25f5660f81 | ||
|
|
0923936f7c | ||
|
|
3c0905d0b0 | ||
|
|
97fd3beeaa | ||
|
|
3195ad86ce | ||
|
|
d147639a83 | ||
|
|
9e0716d32e | ||
|
|
3a05b1e5c3 | ||
|
|
9628861186 | ||
|
|
2b541dc28d | ||
|
|
e655dcedb0 | ||
|
|
9b8bbebb44 | ||
|
|
bbc99ebf16 | ||
|
|
23690f4e9b | ||
|
|
6286034a9d | ||
|
|
2ea684061e | ||
|
|
973abc8917 | ||
|
|
65304b0f84 | ||
|
|
ca4dd89e89 | ||
|
|
fccf10dc82 | ||
|
|
b845245728 | ||
|
|
e46357d603 | ||
|
|
6568ed8059 | ||
|
|
236718c76e | ||
|
|
17ef816fa3 | ||
|
|
db33a0a1da | ||
|
|
7a97bbf716 | ||
|
|
0c4ab8c1b6 | ||
|
|
6ee19d57bf | ||
|
|
dcb92c8dad | ||
|
|
968d3cfcf1 | ||
|
|
8e9c12f6e7 | ||
|
|
3c8f32e67a | ||
|
|
86d7ee3e9b | ||
|
|
a39ed8c0a7 | ||
|
|
e772e722b5 | ||
|
|
b6bf431062 | ||
|
|
aa41cceff3 | ||
|
|
1baea180aa | ||
|
|
0d8143c62e | ||
|
|
4ae84052e8 | ||
|
|
c73c41ca06 | ||
|
|
5b58418e57 | ||
|
|
7c7f7549c5 | ||
|
|
38203fd767 | ||
|
|
a7b8484a84 | ||
|
|
a091a94737 | ||
|
|
2c299a82b8 | ||
|
|
5ee710750e | ||
|
|
ed5ea31ca8 | ||
|
|
ffdb427184 | ||
|
|
4cef3efa1f | ||
|
|
a5c8908c6b |
28
.github/workflows/docker-compose-build.yml
vendored
28
.github/workflows/docker-compose-build.yml
vendored
@@ -18,23 +18,22 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Create .env file with custom SMTP port as port 25 is not allowed in GitHub Actions
|
||||
run: |
|
||||
echo "SMTP_PORT=2525" > .env
|
||||
|
||||
- name: Set permissions and run install.sh
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
./install.sh build --verbose
|
||||
|
||||
- name: Set up Docker Compose
|
||||
run: |
|
||||
sed -i 's/25\:25/2525\:25/g' docker-compose.yml
|
||||
docker compose -f docker-compose.yml up -d
|
||||
|
||||
- name: Test if services are responding
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
max_attempts: 5
|
||||
command: |
|
||||
sleep 5
|
||||
sleep 15
|
||||
|
||||
# Array of endpoints to test
|
||||
declare -A endpoints=(
|
||||
@@ -77,8 +76,19 @@ jobs:
|
||||
# Exit with error if any service failed
|
||||
if [ "$failed" = true ]; then
|
||||
# Get container logs
|
||||
echo "Container Logs:"
|
||||
docker compose logs
|
||||
echo "Container Logs admin:"
|
||||
docker compose logs admin
|
||||
echo "Container Logs api:"
|
||||
docker compose logs api
|
||||
echo "Container Logs client:"
|
||||
docker compose logs client
|
||||
echo "Container Logs smtp:"
|
||||
docker compose logs smtp
|
||||
echo "Container Logs reverse-proxy:"
|
||||
docker compose logs reverse-proxy
|
||||
|
||||
# Restart containers for next test in case of failure
|
||||
docker compose restart
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -90,4 +100,4 @@ jobs:
|
||||
echo "Expected: 'New admin password: <at least 8 base64 chars>'"
|
||||
echo "Actual: $output"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
9
.github/workflows/docker-compose-pull.yml
vendored
9
.github/workflows/docker-compose-pull.yml
vendored
@@ -29,16 +29,17 @@ jobs:
|
||||
echo "Downloading install script from: $INSTALL_SCRIPT_URL"
|
||||
curl -f -o install.sh "$INSTALL_SCRIPT_URL"
|
||||
|
||||
- name: Create .env file with custom SMTP port as port 25 is not allowed in GitHub Actions
|
||||
run: |
|
||||
echo "SMTP_PORT=2525" > .env
|
||||
|
||||
- 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
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
|
||||
- name: Wait for services to be up
|
||||
run: |
|
||||
|
||||
8
.github/workflows/publish-docker-images.yml
vendored
8
.github/workflows/publish-docker-images.yml
vendored
@@ -70,6 +70,14 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-smtp:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push TaskRunner image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: src/Services/AliasVault.TaskRunner/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:${{ github.ref_name }}
|
||||
|
||||
- name: Build and push Reverse Proxy image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -390,6 +390,9 @@ src/Tests/AliasVault.E2ETests/appsettings.Development.json
|
||||
# .env is generated by install.sh and therefore should be ignored
|
||||
.env
|
||||
|
||||
# install.sh backup files are generated by install.sh self-update and therefore should be ignored
|
||||
install.sh.backup
|
||||
|
||||
# Draw.io diagram temp files
|
||||
*.drawio.*
|
||||
|
||||
|
||||
10
README.md
10
README.md
@@ -6,9 +6,9 @@
|
||||
<a href="https://app.aliasvault.net">Live demo 🔥</a> • <a href="https://aliasvault.net?utm_source=gh-readme">Website 🌐</a> • <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation 📚</a> • <a href="#installation">Installation ⚙️</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
Open-source password and alias manager
|
||||
</h3>
|
||||
<p align="center">
|
||||
<strong>Open-source password and alias manager</strong>
|
||||
</p>
|
||||
|
||||
[<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)
|
||||
@@ -25,7 +25,7 @@ Open-source password and alias manager
|
||||
|
||||
</div>
|
||||
|
||||
AliasVault is an open-source password and alias manager built with C# ASP.NET technology. AliasVault can be self-hosted on your own server with Docker, providing a secure and private solution for managing your online identities and passwords.
|
||||
AliasVault is an end-to-end encrypted password and alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. The core of AliasVault is built with C# ASP.NET Blazor WASM technology. AliasVault can be self-hosted on your own server with Docker.
|
||||
|
||||
### What makes AliasVault unique:
|
||||
- **Zero-knowledge architecture**: All data is end-to-end encrypted on the client and stored in encrypted state on the server. Your master password never leaves your device and the server never has access to your data.
|
||||
@@ -69,7 +69,7 @@ The install script will output the URL where the app is available. By default th
|
||||
- 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.
|
||||
> Note: If you want to change the default AliasVault ports you can do so in the `.env` file.
|
||||
|
||||
## Detailed documentation
|
||||
For more detailed information about the installation process and other topics, please see the official documentation website:
|
||||
|
||||
@@ -67,6 +67,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DD359F
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Shared.Core", "src\Shared\AliasVault.Shared.Core\AliasVault.Shared.Core.csproj", "{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.TaskRunner", "src\Services\AliasVault.TaskRunner\AliasVault.TaskRunner.csproj", "{D631A936-DD1C-40CC-B735-BD0A5D4F46A1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Shared.Server", "src\Shared\AliasVault.Shared.Server\AliasVault.Shared.Server.csproj", "{34FADEB6-4B56-463B-B359-F844B43D76D9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -169,6 +173,14 @@ Global
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D631A936-DD1C-40CC-B735-BD0A5D4F46A1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{34FADEB6-4B56-463B-B359-F844B43D76D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{34FADEB6-4B56-463B-B359-F844B43D76D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{34FADEB6-4B56-463B-B359-F844B43D76D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{34FADEB6-4B56-463B-B359-F844B43D76D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -197,6 +209,8 @@ Global
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{15EFE0D0-F41B-47D7-86B7-8F840335CB82} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{D631A936-DD1C-40CC-B735-BD0A5D4F46A1} = {8A477241-B96C-4174-968D-D40CB77F1ECD}
|
||||
{34FADEB6-4B56-463B-B359-F844B43D76D9} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FEE82475-C009-4762-8113-A6563D9DC49E}
|
||||
|
||||
@@ -27,4 +27,10 @@ services:
|
||||
image: aliasvault-smtp
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/Services/AliasVault.SmtpService/Dockerfile
|
||||
dockerfile: src/Services/AliasVault.SmtpService/Dockerfile
|
||||
|
||||
task-runner:
|
||||
image: aliasvault-task-runner
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/Services/AliasVault.TaskRunner/Dockerfile
|
||||
|
||||
@@ -2,8 +2,8 @@ services:
|
||||
reverse-proxy:
|
||||
image: ghcr.io/lanedirt/aliasvault-reverse-proxy:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "${HTTP_PORT:-80}:80"
|
||||
- "${HTTPS_PORT:-443}:443"
|
||||
volumes:
|
||||
- ./certificates/ssl:/etc/nginx/ssl:rw
|
||||
- ./certificates/letsencrypt:/etc/nginx/ssl-letsencrypt:rw
|
||||
@@ -35,9 +35,9 @@ services:
|
||||
- ./database:/database:rw
|
||||
- ./certificates/app:/certificates/app:rw
|
||||
- ./logs:/logs:rw
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
admin:
|
||||
image: ghcr.io/lanedirt/aliasvault-admin:latest
|
||||
@@ -54,11 +54,20 @@ services:
|
||||
smtp:
|
||||
image: ghcr.io/lanedirt/aliasvault-smtp:latest
|
||||
ports:
|
||||
- "25:25"
|
||||
- "587:587"
|
||||
- "${SMTP_PORT:-25}:25"
|
||||
- "${SMTP_TLS_PORT:-587}:587"
|
||||
volumes:
|
||||
- ./database:/database:rw
|
||||
- ./logs:/logs:rw
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
task-runner:
|
||||
image: ghcr.io/lanedirt/aliasvault-task-runner:latest
|
||||
volumes:
|
||||
- ./database:/database:rw
|
||||
- ./logs:/logs:rw
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!release
|
||||
@@ -12,7 +12,7 @@ permalink: /
|
||||
Open-source password and identity manager with email alias generation and zero-knowledge architecture.
|
||||
{: .fs-6 .fw-300 }
|
||||
|
||||
[Installation](./installation){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
|
||||
[Installation](./installation/install){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
|
||||
[View on GitHub](https://github.com/lanedirt/AliasVault){: .btn .fs-5 .mb-4 .mb-md-0 }
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Build from Source
|
||||
parent: Installation Guide
|
||||
parent: Advanced
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
9
docs/installation/advanced/index.md
Normal file
9
docs/installation/advanced/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Advanced
|
||||
parent: Installation Guide
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Advanced Installation
|
||||
The following guides provide more advanced installation options for AliasVault. These options are not required for the basic installation, but may be useful for advanced users.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Manual Setup
|
||||
parent: Installation Guide
|
||||
parent: Advanced
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
@@ -4,80 +4,5 @@ title: Installation Guide
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Installation
|
||||
Follow the steps below to install AliasVault on your own server. Minimum experience with Docker and Linux is required.
|
||||
|
||||
{: .toc }
|
||||
* TOC
|
||||
{:toc}
|
||||
|
||||
---
|
||||
|
||||
## 1. Basic Installation
|
||||
To get AliasVault up and running quickly, run the install script to pull pre-built Docker images. The install script will also configure the .env file and start the AliasVault containers. You can get up and running in less than 5 minutes.
|
||||
|
||||
### Hardware requirements
|
||||
- Linux VM with root access (Ubuntu or RHEL based distros recommended)
|
||||
- 1 vCPU
|
||||
- 512MB RAM
|
||||
- 16GB disk space
|
||||
- Docker installed
|
||||
|
||||
### Installation steps
|
||||
1. Download the install script to a directory of your choice. All AliasVault files and directories will be created in this directory.
|
||||
```bash
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/main/install.sh
|
||||
```
|
||||
2. Make the install script executable.
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
3. Run the install script. This will create the .env file, pull the Docker images, and start the AliasVault containers. Follow the on-screen prompts to configure AliasVault.
|
||||
```bash
|
||||
./install.sh install
|
||||
```
|
||||
> **Note**: AliasVault binds to ports 80 and 443 by default. If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file for the `reverse-proxy` (nginx) container. Afterwards re-run the `./install.sh install` command to restart the containers with the new port settings.
|
||||
|
||||
3. After the script completes, you can access AliasVault at:
|
||||
- Client: `https://localhost`
|
||||
- Admin: `https://localhost/admin`
|
||||
|
||||
---
|
||||
|
||||
## 2. SSL configuration
|
||||
The default installation will create a self-signed SSL certificate and configure Nginx to use it.
|
||||
|
||||
You can however also use Let's Encrypt to generate valid SSL certificates and configure Nginx to use it. In order to make this work you will need the following:
|
||||
|
||||
- A public IPv4 address assigned to your server
|
||||
- Port 80 and 443 on your server must be open and accessible from the internet
|
||||
- A registered domain name with an A record pointing to your server's public IP address (e.g. mydomain.com)
|
||||
|
||||
|
||||
### Steps
|
||||
|
||||
1. Run the install script with the `configure-ssl` option
|
||||
```bash
|
||||
./install.sh configure-ssl
|
||||
```
|
||||
2. Follow the prompts to configure Let's Encrypt.
|
||||
|
||||
### Reverting to self-signed SSL
|
||||
If at any point you would like to revert to the self-signed SSL certificate, run the install script again with the `configure-ssl` option
|
||||
and then in the prompt choose option 2.
|
||||
|
||||
---
|
||||
|
||||
## 3. Troubleshooting
|
||||
|
||||
### Resetting the admin password
|
||||
If you have lost your admin password, you can reset it by running the install script with the `reset-password` option. This will generate a new random password and update the .env file with it. After that it will restart the AliasVault containers to apply the changes.
|
||||
```bash
|
||||
./install.sh reset-password
|
||||
```
|
||||
|
||||
### Verbose output
|
||||
If you need more detailed output from the install script, you can run it with the `--verbose` option. This will print more information to the console.
|
||||
```bash
|
||||
./install.sh install --verbose
|
||||
```
|
||||
# Installation Guide
|
||||
The following guide will walk you through the steps to install AliasVault on your own server. Minimum experience with Docker and Linux is required.
|
||||
|
||||
150
docs/installation/install.md
Normal file
150
docs/installation/install.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
layout: default
|
||||
title: Basic Install
|
||||
parent: Installation Guide
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Basic Install
|
||||
The following guide will walk you through the steps to install AliasVault on your own server. Minimum experience with Docker and Linux is required.
|
||||
|
||||
{: .toc }
|
||||
* TOC
|
||||
{:toc}
|
||||
|
||||
---
|
||||
|
||||
## 1. Basic Installation
|
||||
To get AliasVault up and running quickly, run the install script to pull pre-built Docker images. The install script will also configure the .env file and start the AliasVault containers. You can get up and running in less than 5 minutes.
|
||||
|
||||
### Hardware requirements
|
||||
- Linux VM with root access (Ubuntu or RHEL based distros recommended)
|
||||
- 1 vCPU
|
||||
- 512MB RAM
|
||||
- 16GB disk space
|
||||
- Docker installed
|
||||
|
||||
### Installation steps
|
||||
1. Download the install script to a directory of your choice. All AliasVault files and directories will be created in this directory.
|
||||
```bash
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/main/install.sh
|
||||
```
|
||||
2. Make the install script executable.
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
3. Run the install script. This will create the .env file, pull the Docker images, and start the AliasVault containers. Follow the on-screen prompts to configure AliasVault.
|
||||
```bash
|
||||
./install.sh install
|
||||
```
|
||||
> **Note**: AliasVault binds to ports 80 and 443 by default. If you want to change the default AliasVault ports you can do so in the `.env` file. Afterwards re-run the `./install.sh install` command to restart the containers with the new port settings.
|
||||
|
||||
3. After the script completes, you can access AliasVault at:
|
||||
- Client: `https://localhost`
|
||||
- Admin: `https://localhost/admin`
|
||||
|
||||
---
|
||||
|
||||
## 2. SSL configuration
|
||||
The default installation will create a self-signed SSL certificate and configure Nginx to use it.
|
||||
|
||||
You can however also use Let's Encrypt to generate valid SSL certificates and configure Nginx to use it. In order to make this work you will need the following:
|
||||
|
||||
- A public IPv4 address assigned to your server
|
||||
- Port 80 and 443 on your server must be open and accessible from the internet
|
||||
- A registered domain name with an A record pointing to your server's public IP address (e.g. mydomain.com)
|
||||
|
||||
### Steps
|
||||
|
||||
1. Run the install script with the `configure-ssl` option
|
||||
```bash
|
||||
./install.sh configure-ssl
|
||||
```
|
||||
2. Follow the prompts to configure Let's Encrypt.
|
||||
|
||||
### Reverting to self-signed SSL
|
||||
If at any point you would like to revert to the self-signed SSL certificate, run the install script again with the `configure-ssl` option
|
||||
and then in the prompt choose option 2.
|
||||
|
||||
---
|
||||
|
||||
## 3. Email Server Setup
|
||||
|
||||
AliasVault includes a built-in email server that can handle multiple custom domains for your aliases.
|
||||
|
||||
To set up the email server, you need the following:
|
||||
- Public IPv4 address
|
||||
- Open ports (25 and 587) in server firewall for SMTP traffic
|
||||
- Access to DNS record management for your domain
|
||||
|
||||
### a) DNS Configuration
|
||||
Configure the following DNS records for your domain:
|
||||
|
||||
| Name | Type | Priority | Content | TTL |
|
||||
|------|------|----------|---------------------------|-----|
|
||||
| mail | A | | `<your-server-public-ip>` | 3600 |
|
||||
| @ | MX | 10 | `mail.<your-domain>` | 3600 |
|
||||
|
||||
> Note: Replace `<your-server-public-ip>` and `<your-domain>` with your actual values.
|
||||
|
||||
### b) Port Configuration
|
||||
The email server requires the following ports to be open:
|
||||
- Port 25: Standard SMTP (unencrypted)
|
||||
- Port 587: SMTP with STARTTLS (encrypted)
|
||||
|
||||
#### Verifying Port Access
|
||||
You can test if the SMTP ports are correctly configured using telnet:
|
||||
|
||||
```bash
|
||||
# Test standard SMTP port
|
||||
telnet <your-server-public-ip> 25
|
||||
|
||||
# Test secure SMTP port
|
||||
telnet <your-server-public-ip> 587
|
||||
```
|
||||
|
||||
If successful, you'll see a connection establishment message. Press Ctrl+C to exit the telnet session.
|
||||
|
||||
### c) Setting Up Email Domains
|
||||
|
||||
1. Run the email configuration script:
|
||||
```bash
|
||||
./install.sh configure-email
|
||||
````
|
||||
2. Follow the interactive prompts to:
|
||||
- Configure your domain(s)
|
||||
- Restart required services
|
||||
|
||||
3. Once configured, you can:
|
||||
- Create new aliases in the AliasVault client
|
||||
- Use your custom domain(s) for email addresses
|
||||
- Note: you can configure the default domain for new aliases in the AliasVault client in Menu > Settings > Email Settings > Default Email Domain
|
||||
- Start receiving emails on your aliases
|
||||
|
||||
{: .note }
|
||||
Important: DNS propagation can take up to 24-48 hours. During this time, email delivery might be inconsistent.
|
||||
|
||||
If you encounter any issues, feel free to open an issue on the [GitHub repository](https://github.com/lanedirt/AliasVault/issues).
|
||||
|
||||
---
|
||||
|
||||
## 4. Troubleshooting
|
||||
|
||||
### Resetting the admin password
|
||||
If you have lost your admin password, you can reset it by running the install script with the `reset-password` option. This will generate a new random password and update the .env file with it. After that it will restart the AliasVault containers to apply the changes.
|
||||
```bash
|
||||
./install.sh reset-password
|
||||
```
|
||||
|
||||
### Verbose output
|
||||
If you need more detailed output from the install script, you can run it with the `--verbose` option. This will print more information to the console.
|
||||
```bash
|
||||
./install.sh install --verbose
|
||||
```
|
||||
|
||||
### No emails being received
|
||||
If you are not receiving emails on your aliases, check the following:
|
||||
- Verify DNS records are correctly configured
|
||||
- Ensure ports 25 and 587 are accessible
|
||||
- Check your server's firewall settings
|
||||
- Verify that your ISP/hosting provider allows SMTP traffic
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Start/stop
|
||||
parent: Installation Guide
|
||||
nav_order: 3
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Starting and stopping AliasVault
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Uninstall
|
||||
parent: Installation Guide
|
||||
nav_order: 5
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
# Uninstall
|
||||
@@ -16,4 +16,4 @@ This will not delete any data stored in the database. If you wish to delete all
|
||||
1. Run the install script with the `uninstall` option
|
||||
```bash
|
||||
./install.sh uninstall
|
||||
```
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Update
|
||||
parent: Installation Guide
|
||||
nav_order: 4
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# Updating AliasVault
|
||||
@@ -21,6 +21,17 @@ To update to the latest version, run the install script with the `update` option
|
||||
./install.sh update
|
||||
```
|
||||
|
||||
> Tip: to skip the confirmation prompts and automatically proceed with the update, use the `-y` flag: `./install.sh update -y`
|
||||
|
||||
## Updating the installer script
|
||||
The installer script can check for and apply updates to itself. This is done as part of the `update` command. However you can also update the installer script separately with the `update-installer` command. This is useful if you want to update the installer script without updating AliasVault itself, e.g. as a separate step during CI/CD pipeline.
|
||||
|
||||
```bash
|
||||
./install.sh update-installer
|
||||
```
|
||||
|
||||
> Tip: to skip the confirmation prompts and automatically proceed with the update, use the `-y` flag: `./install.sh update-installer -y`
|
||||
|
||||
## Installing a specific version
|
||||
To install a specific version and skip the automatic version checks, run the install script with the `install` option and specify the version you want to install.
|
||||
|
||||
|
||||
19
docs/misc/release/create-new-release.md
Normal file
19
docs/misc/release/create-new-release.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
layout: default
|
||||
title: Create a new release
|
||||
parent: Release
|
||||
grand_parent: Miscellaneous
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Release Preparation Checklist
|
||||
|
||||
Follow the steps in the checklist below to prepare a new release.
|
||||
|
||||
- [ ] Update ./src/Shared/AliasVault.Shared.Core/AppInfo.cs and update major/minor/patch to the new version. This version will be shown in the client and admin app footer.
|
||||
- [ ] Update ./install.sh `@version` in header if the install script has changed. This allows the install script to self-update when running ./install.sh update command on default installations.
|
||||
- [ ] Update README screenshots if applicable
|
||||
- [ ] Update README current/upcoming features
|
||||
|
||||
Optional steps:
|
||||
- [ ] Update /docs instructions if any changes have been made to the setup process
|
||||
6
docs/misc/release/index.md
Normal file
6
docs/misc/release/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
layout: default
|
||||
title: Release
|
||||
parent: Miscellaneous
|
||||
nav_order: 1
|
||||
---
|
||||
433
install.sh
433
install.sh
@@ -1,10 +1,12 @@
|
||||
#!/bin/bash
|
||||
# @version 0.9.4
|
||||
|
||||
# Repository information used for downloading files and images from GitHub
|
||||
REPO_OWNER="lanedirt"
|
||||
REPO_NAME="AliasVault"
|
||||
REPO_BRANCH="main"
|
||||
GITHUB_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}"
|
||||
GITHUB_RAW_URL_REPO="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}"
|
||||
GITHUB_RAW_URL_REPO_BRANCH="$GITHUB_RAW_URL_REPO/$REPO_BRANCH"
|
||||
GITHUB_CONTAINER_REGISTRY="ghcr.io/$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]')/$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
# Required files and directories
|
||||
@@ -22,7 +24,6 @@ REQUIRED_DIRS=(
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
@@ -37,15 +38,17 @@ show_usage() {
|
||||
printf "Usage: $0 [COMMAND] [OPTIONS]\n"
|
||||
printf "\n"
|
||||
printf "Commands:\n"
|
||||
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (recommended)\n"
|
||||
printf " uninstall Uninstall AliasVault\n"
|
||||
printf " update Update AliasVault to the latest version\n"
|
||||
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
|
||||
printf " start Start AliasVault containers\n"
|
||||
printf " stop Stop AliasVault containers\n"
|
||||
printf " restart Restart AliasVault containers\n"
|
||||
printf " reset-password Reset admin password\n"
|
||||
printf " build Build AliasVault from source (takes longer and requires sufficient specs)\n"
|
||||
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (recommended)\n"
|
||||
printf " uninstall Uninstall AliasVault\n"
|
||||
printf " update Update AliasVault to the latest version\n"
|
||||
printf " update-installer Check and update install.sh script if newer version available\n"
|
||||
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
|
||||
printf " configure-email Configure email domains for receiving emails\n"
|
||||
printf " start Start AliasVault containers\n"
|
||||
printf " stop Stop AliasVault containers\n"
|
||||
printf " restart Restart AliasVault containers\n"
|
||||
printf " reset-password Reset admin password\n"
|
||||
printf " build Build AliasVault from source (takes longer and requires sufficient specs)\n"
|
||||
|
||||
printf "\n"
|
||||
printf "Options:\n"
|
||||
@@ -94,6 +97,10 @@ parse_args() {
|
||||
COMMAND="configure-ssl"
|
||||
shift
|
||||
;;
|
||||
configure-email|email)
|
||||
COMMAND="configure-email"
|
||||
shift
|
||||
;;
|
||||
start|s)
|
||||
COMMAND="start"
|
||||
shift
|
||||
@@ -110,6 +117,10 @@ parse_args() {
|
||||
COMMAND="update"
|
||||
shift
|
||||
;;
|
||||
update-installer|cs)
|
||||
COMMAND="update-installer"
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
@@ -172,6 +183,9 @@ main() {
|
||||
"configure-ssl")
|
||||
handle_ssl_configuration
|
||||
;;
|
||||
"configure-email")
|
||||
handle_email_configuration
|
||||
;;
|
||||
"start")
|
||||
handle_start
|
||||
;;
|
||||
@@ -184,6 +198,10 @@ main() {
|
||||
"update")
|
||||
handle_update
|
||||
;;
|
||||
"update-installer")
|
||||
check_install_script_update
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -216,37 +234,36 @@ create_directories() {
|
||||
# Function to initialize workspace
|
||||
initialize_workspace() {
|
||||
create_directories
|
||||
handle_docker_compose
|
||||
}
|
||||
|
||||
# Function to handle docker-compose.yml
|
||||
handle_docker_compose() {
|
||||
printf "${CYAN}> Checking docker-compose files...${NC}\n"
|
||||
local version_tag="$1"
|
||||
printf "${CYAN}> Downloading latest docker-compose files...${NC}\n"
|
||||
|
||||
# Check and download main docker-compose.yml
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
printf " ${CYAN}> Downloading docker-compose.yml...${NC}"
|
||||
if curl -sSf "${GITHUB_RAW_URL}/docker-compose.yml" -o "docker-compose.yml" > /dev/null 2>&1; then
|
||||
printf "\n ${GREEN}> docker-compose.yml downloaded successfully.${NC}\n"
|
||||
# Download and overwrite docker-compose.yml
|
||||
printf " ${GREEN}> Downloading docker-compose.yml for version ${version_tag}...${NC}"
|
||||
if curl -sSf "${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.yml" -o "docker-compose.yml.tmp" > /dev/null 2>&1; then
|
||||
# Replace the :latest tag with the specific version if provided
|
||||
if [ -n "$version_tag" ] && [ "$version_tag" != "latest" ]; then
|
||||
sed "s/:latest/:$version_tag/g" docker-compose.yml.tmp > docker-compose.yml
|
||||
rm docker-compose.yml.tmp
|
||||
else
|
||||
printf "\n ${YELLOW}> Failed to download docker-compose.yml, please check your internet connection and try again. Alternatively, you can download it manually from https://github.com/${REPO_OWNER}/${REPO_NAME}/blob/main/docker-compose.yml and place it in the root directory of AliasVault.${NC}\n"
|
||||
exit 1
|
||||
mv docker-compose.yml.tmp docker-compose.yml
|
||||
fi
|
||||
printf "\n ${CYAN}> docker-compose.yml downloaded successfully.${NC}\n"
|
||||
else
|
||||
printf " ${GREEN}> docker-compose.yml already exists.${NC}\n"
|
||||
printf "\n ${YELLOW}> Failed to download docker-compose.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.yml and place it in the root directory of AliasVault.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check and download docker-compose.letsencrypt.yml
|
||||
if [ ! -f "docker-compose.letsencrypt.yml" ]; then
|
||||
printf " ${CYAN}> Downloading docker-compose.letsencrypt.yml...${NC}"
|
||||
if curl -sSf "${GITHUB_RAW_URL}/docker-compose.letsencrypt.yml" -o "docker-compose.letsencrypt.yml" > /dev/null 2>&1; then
|
||||
printf "\n ${GREEN}> docker-compose.letsencrypt.yml downloaded successfully.${NC}\n"
|
||||
else
|
||||
printf "\n ${YELLOW}> Failed to download docker-compose.letsencrypt.yml, please check your internet connection and try again. Alternatively, you can download it manually from https://github.com/${REPO_OWNER}/${REPO_NAME}/blob/main/docker-compose.letsencrypt.yml and place it in the root directory of AliasVault.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
# Download and overwrite docker-compose.letsencrypt.yml
|
||||
printf " ${GREEN}> Downloading docker-compose.letsencrypt.yml for version ${version_tag}...${NC}"
|
||||
if curl -sSf "${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.letsencrypt.yml" -o "docker-compose.letsencrypt.yml" > /dev/null 2>&1; then
|
||||
printf "\n ${CYAN}> docker-compose.letsencrypt.yml downloaded successfully.${NC}\n"
|
||||
else
|
||||
printf " ${GREEN}> docker-compose.letsencrypt.yml already exists.${NC}\n"
|
||||
printf "\n ${YELLOW}> Failed to download docker-compose.letsencrypt.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.letsencrypt.yml and place it in the root directory of AliasVault.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
@@ -258,8 +275,8 @@ print_logo() {
|
||||
printf " _ _ _ __ __ _ _ \n"
|
||||
printf " / \ | (_) __ _ ___ \ \ / /_ _ _ _| | |_\n"
|
||||
printf " / _ \ | | |/ _\` / __| \ \/\/ / _\` | | | | | __|\n"
|
||||
printf " / ___ \| | | (_| \__ \ \ / (_| | |_| | | |_ \n"
|
||||
printf "/_/ \_\_|_|\__,_|___/ \/ \__,_|\__,_|_|\__|\n"
|
||||
printf " / ___ \| | | (_| \__ \ \ / / (_| | |_| | | |_ \n"
|
||||
printf "/_/ \_\_|_|\__,_|___/ \/ \__,__|\__,_|_|\__|\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
@@ -316,22 +333,14 @@ populate_data_protection_cert_pass() {
|
||||
set_private_email_domains() {
|
||||
printf "${CYAN}> Checking PRIVATE_EMAIL_DOMAINS...${NC}\n"
|
||||
if ! grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "Please enter the domains that should be allowed to receive email, separated by commas (press Enter to disable email support): "
|
||||
read -r private_email_domains
|
||||
update_env_var "PRIVATE_EMAIL_DOMAINS" "DISABLED.TLD"
|
||||
fi
|
||||
|
||||
private_email_domains=${private_email_domains:-"DISABLED.TLD"}
|
||||
update_env_var "PRIVATE_EMAIL_DOMAINS" "$private_email_domains"
|
||||
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${RED}SMTP is disabled.${NC}\n"
|
||||
fi
|
||||
private_email_domains=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${RED}Email server is disabled.${NC} To enable use ./install.sh configure-email command.\n"
|
||||
else
|
||||
private_email_domains=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists.${NC} ${RED}Private email domains are disabled.${NC}\n"
|
||||
else
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists.${NC}\n"
|
||||
fi
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists. Email server is enabled.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -400,6 +409,37 @@ generate_admin_password() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to set default ports
|
||||
set_default_ports() {
|
||||
printf "${CYAN}> Checking default ports...${NC}\n"
|
||||
|
||||
# Web ports
|
||||
if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
update_env_var "HTTP_PORT" "80"
|
||||
else
|
||||
printf " ${GREEN}> HTTP_PORT already exists.${NC}\n"
|
||||
fi
|
||||
|
||||
if ! grep -q "^HTTPS_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTPS_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
update_env_var "HTTPS_PORT" "443"
|
||||
else
|
||||
printf " ${GREEN}> HTTPS_PORT already exists.${NC}\n"
|
||||
fi
|
||||
|
||||
# SMTP ports
|
||||
if ! grep -q "^SMTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^SMTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
update_env_var "SMTP_PORT" "25"
|
||||
else
|
||||
printf " ${GREEN}> SMTP_PORT already exists.${NC}\n"
|
||||
fi
|
||||
|
||||
if ! grep -q "^SMTP_TLS_PORT=" "$ENV_FILE" || [ -z "$(grep "^SMTP_TLS_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
update_env_var "SMTP_TLS_PORT" "587"
|
||||
else
|
||||
printf " ${GREEN}> SMTP_TLS_PORT already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to update environment variables
|
||||
update_env_var() {
|
||||
local key=$1
|
||||
@@ -413,6 +453,7 @@ update_env_var() {
|
||||
printf " ${GREEN}> $key has been set in $ENV_FILE.${NC}\n"
|
||||
}
|
||||
|
||||
|
||||
# Helper function to delete environment variables
|
||||
delete_env_var() {
|
||||
local key=$1
|
||||
@@ -542,7 +583,7 @@ handle_build() {
|
||||
printf "Please clone the complete repository using:\n"
|
||||
printf "git clone https://github.com/${REPO_OWNER}/${REPO_NAME}.git\n"
|
||||
printf "\n"
|
||||
printf "Alternatively, you can use '/install' to pull pre-built images.\n"
|
||||
printf "Alternatively, you can use './install.sh install' to pull pre-built images.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -554,6 +595,7 @@ handle_build() {
|
||||
set_private_email_domains || { printf "${RED}> Failed to set email domains${NC}\n"; exit 1; }
|
||||
set_smtp_tls_enabled || { printf "${RED}> Failed to set SMTP TLS${NC}\n"; exit 1; }
|
||||
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
|
||||
set_default_ports || { printf "${RED}> Failed to set default ports${NC}\n"; exit 1; }
|
||||
|
||||
# Only generate admin password if not already set
|
||||
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
@@ -743,6 +785,147 @@ handle_ssl_configuration() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to handle email server configuration
|
||||
# Function to handle email server configuration
|
||||
handle_email_configuration() {
|
||||
# Setup trap for Ctrl+C and other interrupts
|
||||
trap 'printf "\n${YELLOW}Configuration cancelled by user.${NC}\n"; exit 1' INT TERM
|
||||
|
||||
printf "${YELLOW}+++ Email Server Configuration +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Check if AliasVault is installed
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current email domains from .env
|
||||
CURRENT_DOMAINS=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
|
||||
printf "${CYAN}About Email Server:${NC}\n"
|
||||
printf "AliasVault includes a built-in email server for handling virtual email addresses.\n"
|
||||
printf "When enabled, it can receive emails for one or more configured domains.\n"
|
||||
printf "Each domain must have an MX record in DNS configuration pointing to this server's hostname.\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}Current Configuration:${NC}\n"
|
||||
|
||||
if [ "$CURRENT_DOMAINS" = "DISABLED.TLD" ]; then
|
||||
printf "Email Server Status: ${RED}Disabled${NC}\n"
|
||||
else
|
||||
printf "Email Server Status: ${GREEN}Enabled${NC}\n"
|
||||
printf "Active Domains: ${CYAN}${CURRENT_DOMAINS}${NC}\n"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
printf "Email Server Options:\n"
|
||||
printf "1) Enable email server / Update domains\n"
|
||||
printf "2) Disable email server\n"
|
||||
printf "3) Cancel\n"
|
||||
printf "\n"
|
||||
|
||||
read -p "Select an option [1-3]: " email_option
|
||||
|
||||
case $email_option in
|
||||
1)
|
||||
while true; do
|
||||
printf "\n${CYAN}Enter domain(s) for email server${NC}\n"
|
||||
printf "For multiple domains, separate with commas (e.g. domain1.com,domain2.com)\n"
|
||||
printf "IMPORTANT: Each domain must have an MX record in DNS pointing to this server.\n"
|
||||
read -p "Domains: " new_domains
|
||||
|
||||
if [ -z "$new_domains" ]; then
|
||||
printf "${RED}Error: Domains cannot be empty${NC}\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
printf "\n${CYAN}You entered the following domains:${NC}\n"
|
||||
IFS=',' read -ra DOMAIN_ARRAY <<< "$new_domains"
|
||||
for domain in "${DOMAIN_ARRAY[@]}"; do
|
||||
printf " - ${GREEN}${domain}${NC}\n"
|
||||
done
|
||||
printf "\n"
|
||||
|
||||
read -p "Are these domains correct? (y/n): " confirm
|
||||
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
|
||||
read -p "Continue with restart? (y/n): " restart_confirm
|
||||
|
||||
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
|
||||
printf "${YELLOW}Configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update .env file and restart
|
||||
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "$new_domains"; then
|
||||
printf "${RED}Failed to update configuration.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${GREEN}Email server configuration updated${NC}\n"
|
||||
printf "Restarting AliasVault services...\n"
|
||||
|
||||
if ! handle_restart; then
|
||||
printf "${RED}Failed to restart services.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only show next steps if everything succeeded
|
||||
printf "\n${CYAN}The email server is now succesfully configured.${NC}\n"
|
||||
printf "\n"
|
||||
printf "To test the email server:\n"
|
||||
printf " a. Log in to your AliasVault account\n"
|
||||
printf " b. Create a new alias using one of your configured private domains\n"
|
||||
printf " c. Send a test email from an external email service (e.g., Gmail)\n"
|
||||
printf " d. Check if the email appears in your AliasVault inbox\n"
|
||||
printf "\n"
|
||||
printf "If emails don't arrive, please verify:\n"
|
||||
printf " > DNS MX records are correctly configured\n"
|
||||
printf " > Your server's firewall allows incoming traffic on port 25 and 587\n"
|
||||
printf " > Your ISP/hosting provider doesn't block SMTP traffic\n"
|
||||
printf "\n"
|
||||
;;
|
||||
2)
|
||||
printf "${YELLOW}Warning: Docker containers need to be restarted after disabling the email server.${NC}\n"
|
||||
read -p "Continue with disable and restart? (y/n): " disable_confirm
|
||||
|
||||
if [ "$disable_confirm" != "y" ] && [ "$disable_confirm" != "Y" ]; then
|
||||
printf "${YELLOW}Configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Disable email server
|
||||
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "DISABLED.TLD"; then
|
||||
printf "${RED}Failed to update configuration.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${YELLOW}Email server disabled${NC}\n"
|
||||
printf "Restarting AliasVault services...\n"
|
||||
|
||||
if ! handle_restart; then
|
||||
printf "${RED}Failed to restart services.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
printf "${YELLOW}Email configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf "${RED}Invalid option selected.${NC}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Remove the trap before normal exit
|
||||
trap - INT TERM
|
||||
}
|
||||
|
||||
# Function to configure Let's Encrypt
|
||||
configure_letsencrypt() {
|
||||
printf "${CYAN}> Configuring Let's Encrypt SSL certificate...${NC}\n"
|
||||
@@ -891,6 +1074,9 @@ handle_update() {
|
||||
printf "${YELLOW}+++ Checking for AliasVault updates +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# First check for install.sh updates
|
||||
check_install_script_update || true
|
||||
|
||||
# Check current version
|
||||
if ! grep -q "^ALIASVAULT_VERSION=" "$ENV_FILE"; then
|
||||
printf "${YELLOW}> No version information found. Running first-time update check...${NC}\n"
|
||||
@@ -906,8 +1092,8 @@ handle_update() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${CYAN}> Current version: ${current_version}${NC}\n"
|
||||
printf "${CYAN}> Latest version: ${latest_version}${NC}\n"
|
||||
printf "${CYAN}> Current AliasVault version: ${current_version}${NC}\n"
|
||||
printf "${CYAN}> Latest AliasVault version: ${latest_version}${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
if [ "$current_version" = "$latest_version" ]; then
|
||||
@@ -915,6 +1101,13 @@ handle_update() {
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$FORCE_YES" = true ]; then
|
||||
printf "${CYAN}> Updating AliasVault to the latest version...${NC}\n"
|
||||
handle_install_version "$latest_version"
|
||||
printf "${GREEN}> Update completed successfully!${NC}\n"
|
||||
return
|
||||
fi
|
||||
|
||||
printf "${YELLOW}> A new version of AliasVault is available!${NC}\n"
|
||||
printf "\n"
|
||||
printf "${MAGENTA}Important:${NC}\n"
|
||||
@@ -934,6 +1127,126 @@ handle_update() {
|
||||
printf "${GREEN}> Update completed successfully!${NC}\n"
|
||||
}
|
||||
|
||||
# Function to extract version
|
||||
extract_version() {
|
||||
local file="$1"
|
||||
local version=$(head -n 2 "$file" | grep '@version' | cut -d' ' -f3)
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
# Function to compare semantic versions
|
||||
compare_versions() {
|
||||
local version1="$1"
|
||||
local version2="$2"
|
||||
|
||||
# Split versions into arrays
|
||||
IFS='.' read -ra v1_parts <<< "$version1"
|
||||
IFS='.' read -ra v2_parts <<< "$version2"
|
||||
|
||||
# Compare each part numerically
|
||||
for i in {0..2}; do
|
||||
# Default to 0 if part doesn't exist
|
||||
local v1_part=${v1_parts[$i]:-0}
|
||||
local v2_part=${v2_parts[$i]:-0}
|
||||
|
||||
# Compare numerically
|
||||
if [ "$v1_part" -gt "$v2_part" ]; then
|
||||
echo "1" # version1 is greater
|
||||
return
|
||||
elif [ "$v1_part" -lt "$v2_part" ]; then
|
||||
echo "-1" # version1 is lesser
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
echo "0" # versions are equal
|
||||
}
|
||||
|
||||
# Function to check if install.sh needs updating
|
||||
check_install_script_update() {
|
||||
printf "${CYAN}> Checking for install script updates...${NC}\n"
|
||||
|
||||
# Download latest install.sh to temporary file
|
||||
if ! curl -sSf "${GITHUB_RAW_URL_REPO_BRANCH}/install.sh" -o "install.sh.tmp"; then
|
||||
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get versions
|
||||
local current_version=$(extract_version "install.sh")
|
||||
local new_version=$(extract_version "install.sh.tmp")
|
||||
|
||||
# Check if versions could be extracted
|
||||
if [ -z "$current_version" ] || [ -z "$new_version" ]; then
|
||||
printf "${YELLOW}> Could not determine script versions. Falling back to file comparison...${NC}\n"
|
||||
# Fall back to file comparison
|
||||
if ! cmp -s "install.sh" "install.sh.tmp"; then
|
||||
printf "${YELLOW}> Changes detected in install script.${NC}\n"
|
||||
else
|
||||
printf "${GREEN}> Install script is up to date.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
printf "${CYAN}> Current install script version: ${current_version}${NC}\n"
|
||||
printf "${CYAN}> Latest install script version: ${new_version}${NC}\n"
|
||||
|
||||
# Compare versions using semver comparison
|
||||
if [ "$current_version" = "$new_version" ]; then
|
||||
printf "${GREEN}> Install script is up to date.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
else
|
||||
local compare_result=$(compare_versions "$current_version" "$new_version")
|
||||
|
||||
if [ "$compare_result" -ge "0" ]; then
|
||||
printf "${GREEN}> Install script is up to date.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If we get here, an update is available
|
||||
if [ "$FORCE_YES" = true ]; then
|
||||
printf "${CYAN}> Updating install script...${NC}\n"
|
||||
cp "install.sh" "install.sh.backup"
|
||||
mv "install.sh.tmp" "install.sh"
|
||||
chmod +x "install.sh"
|
||||
printf "${GREEN}> Install script updated successfully.${NC}\n"
|
||||
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf "${YELLOW}> A new version of the install script is available.${NC}\n"
|
||||
printf "Would you like to update the install script before proceeding? [Y/n]: "
|
||||
read -r reply
|
||||
|
||||
if [[ ! $reply =~ ^[Nn]$ ]]; then
|
||||
# Create backup of current script
|
||||
cp "install.sh" "install.sh.backup"
|
||||
|
||||
if mv "install.sh.tmp" "install.sh"; then
|
||||
chmod +x "install.sh"
|
||||
printf "${GREEN}> Install script updated successfully.${NC}\n"
|
||||
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
|
||||
printf "${YELLOW}> Please run the update command again to continue with the update process.${NC}\n"
|
||||
exit 0
|
||||
else
|
||||
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
|
||||
# Restore from backup if update failed
|
||||
mv "install.sh.backup" "install.sh"
|
||||
rm -f install.sh.tmp
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
printf "${YELLOW}> Continuing with current install script version.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to perform the actual installation with specific version
|
||||
handle_install_version() {
|
||||
local target_version="$1"
|
||||
@@ -952,6 +1265,9 @@ handle_install_version() {
|
||||
# Initialize workspace which makes sure all required directories and files exist
|
||||
initialize_workspace
|
||||
|
||||
# Update docker-compose files with correct version so we pull the correct images
|
||||
handle_docker_compose "$target_version"
|
||||
|
||||
# Initialize environment
|
||||
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
|
||||
populate_hostname || { printf "${RED}> Failed to set hostname${NC}\n"; exit 1; }
|
||||
@@ -960,6 +1276,7 @@ handle_install_version() {
|
||||
set_private_email_domains || { printf "${RED}> Failed to set email domains${NC}\n"; exit 1; }
|
||||
set_smtp_tls_enabled || { printf "${RED}> Failed to set SMTP TLS${NC}\n"; exit 1; }
|
||||
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
|
||||
set_default_ports || { printf "${RED}> Failed to set default ports${NC}\n"; exit 1; }
|
||||
|
||||
# Only generate admin password if not already set
|
||||
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
@@ -972,17 +1289,13 @@ handle_install_version() {
|
||||
|
||||
printf "${CYAN}> Installing version: ${target_version}${NC}\n"
|
||||
|
||||
local tag="$target_version"
|
||||
if [ "$target_version" = "latest" ]; then
|
||||
tag="latest"
|
||||
fi
|
||||
|
||||
images=(
|
||||
"${GITHUB_CONTAINER_REGISTRY}-reverse-proxy:${tag}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-api:${tag}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-client:${tag}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-admin:${tag}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-smtp:${tag}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-reverse-proxy:${target_version}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-api:${target_version}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-client:${target_version}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-admin:${target_version}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-smtp:${target_version}"
|
||||
"${GITHUB_CONTAINER_REGISTRY}-task-runner:${target_version}"
|
||||
)
|
||||
|
||||
for image in "${images[@]}"; do
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.RazorComponents\AliasVault.RazorComponents.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared.Core\AliasVault.Shared.Core.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared.Server\AliasVault.Shared.Server.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.Auth\AliasVault.Auth.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.Logging\AliasVault.Logging.csproj" />
|
||||
<ProjectReference Include="..\Utilities\Cryptography\AliasVault.Cryptography.Server\AliasVault.Cryptography.Server.csproj" />
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
@using AliasVault.WorkerStatus.Database
|
||||
@inherits MainBase
|
||||
|
||||
@foreach (var service in Services)
|
||||
{
|
||||
<button @onclick="() => ServiceClick(service.Name)"
|
||||
class="@GetServiceButtonClasses(service) mx-3 inline-flex items-center justify-center rounded-xl px-8 py-2 text-white"
|
||||
disabled="@(!IsHeartbeatValid(service.LastHeartbeat))"
|
||||
title="@GetButtonTooltip(service.LastHeartbeat)">
|
||||
<span>@service.DisplayName</span>
|
||||
@if (service.IsPending)
|
||||
{
|
||||
<svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The names of the services to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public List<string> ServiceNames { get; set; } = ["AliasVault.SmtpService", "AliasVault.TaskRunner"];
|
||||
|
||||
/// <summary>
|
||||
/// The display names of the services to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Dictionary<string, string> ServiceDisplayNames { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The statuses of the services.
|
||||
/// </summary>
|
||||
private List<WorkerServiceStatus> ServiceStatus = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether the page is initializing.
|
||||
/// </summary>
|
||||
private bool InitInProgress;
|
||||
|
||||
/// <summary>
|
||||
/// The interval to refresh the page.
|
||||
/// </summary>
|
||||
private readonly int AutoRefreshInterval = 5000;
|
||||
private CancellationTokenSource? _timerCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The state of a service.
|
||||
/// </summary>
|
||||
private sealed class ServiceState
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string DisplayName { get; set; } = "";
|
||||
public bool Status { get; set; }
|
||||
public bool IsPending { get; set; }
|
||||
public DateTime LastHeartbeat { get; set; }
|
||||
}
|
||||
|
||||
private List<ServiceState> Services { get; set; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Services = ServiceNames.Select(name => new ServiceState
|
||||
{
|
||||
Name = name,
|
||||
DisplayName = ServiceDisplayNames.GetValueOrDefault(name, name)
|
||||
}).ToList();
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
_timerCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RunPeriodicRefreshAsync(_timerCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_timerCancellationTokenSource?.Cancel();
|
||||
_timerCancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the heartbeat is valid (within the last 5 minutes).
|
||||
/// </summary>
|
||||
private static bool IsHeartbeatValid(DateTime lastHeartbeat)
|
||||
{
|
||||
return DateTime.Now <= lastHeartbeat.AddMinutes(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CSS classes for a service button based on its current state.
|
||||
/// </summary>
|
||||
private static string GetServiceButtonClasses(ServiceState service)
|
||||
{
|
||||
string buttonClass = "cursor-pointer ";
|
||||
|
||||
if (!IsHeartbeatValid(service.LastHeartbeat))
|
||||
{
|
||||
buttonClass += "bg-gray-600";
|
||||
}
|
||||
else if (service.Status)
|
||||
{
|
||||
buttonClass += "bg-green-600";
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonClass += "bg-red-600";
|
||||
}
|
||||
|
||||
return buttonClass;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tooltip text for a service button based on its last heartbeat.
|
||||
/// </summary>
|
||||
private static string GetButtonTooltip(DateTime lastHeartbeat)
|
||||
{
|
||||
return IsHeartbeatValid(lastHeartbeat) ? "" : "Heartbeat offline";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a click on a service button.
|
||||
/// </summary>
|
||||
private async Task ServiceClick(string serviceName)
|
||||
{
|
||||
var service = Services.First(s => s.Name == serviceName);
|
||||
|
||||
if (!IsHeartbeatValid(service.LastHeartbeat))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
service.IsPending = true;
|
||||
StateHasChanged();
|
||||
|
||||
service.Status = !service.Status;
|
||||
await UpdateServiceStatus(serviceName, service.Status);
|
||||
|
||||
service.IsPending = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the page.
|
||||
/// </summary>
|
||||
private async Task InitPage()
|
||||
{
|
||||
if (InitInProgress || Services.Any(s => s.IsPending))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InitInProgress = true;
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
ServiceStatus = await dbContext.WorkerServiceStatuses.ToListAsync();
|
||||
|
||||
foreach (var service in Services)
|
||||
{
|
||||
var entry = ServiceStatus.Find(x => x.ServiceName == service.Name);
|
||||
if (entry != null)
|
||||
{
|
||||
service.LastHeartbeat = entry.Heartbeat;
|
||||
service.Status = IsHeartbeatValid(service.LastHeartbeat) && entry.CurrentStatus == "Started";
|
||||
}
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
finally
|
||||
{
|
||||
InitInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the status of a service.
|
||||
/// </summary>
|
||||
private async Task<bool> UpdateServiceStatus(string serviceName, bool newStatus)
|
||||
{
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var entry = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstOrDefaultAsync();
|
||||
if (entry != null)
|
||||
{
|
||||
string newDesiredStatus = newStatus ? "Started" : "Stopped";
|
||||
entry.DesiredStatus = newDesiredStatus;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
var timeout = DateTime.Now.AddSeconds(30);
|
||||
while (true)
|
||||
{
|
||||
if (DateTime.Now > timeout)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var check = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstAsync();
|
||||
if (check.CurrentStatus == newDesiredStatus)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the service status periodically.
|
||||
/// </summary>
|
||||
private async Task RunPeriodicRefreshAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await InitPage();
|
||||
await Task.Delay(AutoRefreshInterval, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
@using AliasVault.WorkerStatus.Database
|
||||
@inherits MainBase
|
||||
|
||||
<button @onclick="SmtpClick"
|
||||
class="@GetSmtpButtonClasses() mx-3 inline-flex items-center justify-center rounded-xl px-8 py-2 text-white"
|
||||
disabled="@(!IsHeartbeatValid())"
|
||||
title="@GetButtonTooltip()">
|
||||
<span>SmtpService</span>
|
||||
@if (SmtpPending)
|
||||
{
|
||||
<svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
|
||||
@code {
|
||||
private List<WorkerServiceStatus> ServiceStatus = [];
|
||||
private bool InitInProgress;
|
||||
private bool SmtpStatus;
|
||||
private bool SmtpPending;
|
||||
private DateTime LastHeartbeat;
|
||||
|
||||
/// <summary>
|
||||
/// The interval in milliseconds for refreshing the service status.
|
||||
/// </summary>
|
||||
private readonly int AutoRefreshInterval = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// CancellationTokenSource for the timer.
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _timerCancellationTokenSource;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
_timerCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RunPeriodicRefreshAsync(_timerCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the service status periodically while waiting for specified amount of ms in between.
|
||||
/// </summary>
|
||||
private async Task RunPeriodicRefreshAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await InitPage();
|
||||
await Task.Delay(AutoRefreshInterval, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_timerCancellationTokenSource?.Cancel();
|
||||
_timerCancellationTokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the CSS classes for the SMTP button based on its current state.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the CSS classes for the button.</returns>
|
||||
private string GetSmtpButtonClasses()
|
||||
{
|
||||
string buttonClass = "cursor-pointer ";
|
||||
|
||||
if (!IsHeartbeatValid())
|
||||
{
|
||||
buttonClass += "bg-gray-600";
|
||||
}
|
||||
else if (SmtpStatus)
|
||||
{
|
||||
buttonClass += "bg-green-600";
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonClass += "bg-red-600";
|
||||
}
|
||||
|
||||
return buttonClass;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tooltip text for the SMTP button.
|
||||
/// </summary>
|
||||
/// <returns>A string containing the tooltip text.</returns>
|
||||
private string GetButtonTooltip()
|
||||
{
|
||||
return IsHeartbeatValid() ? "" : "Heartbeat offline";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the heartbeat is valid (within the last 5 minutes).
|
||||
/// </summary>
|
||||
/// <returns>True if the heartbeat is valid, false otherwise.</returns>
|
||||
private bool IsHeartbeatValid()
|
||||
{
|
||||
return DateTime.Now <= LastHeartbeat.AddMinutes(5);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the click event for the SMTP button.
|
||||
/// </summary>
|
||||
private async void SmtpClick()
|
||||
{
|
||||
if (!IsHeartbeatValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SmtpPending = true;
|
||||
StateHasChanged();
|
||||
|
||||
SmtpStatus = !SmtpStatus;
|
||||
await UpdateSmtpStatus(SmtpStatus);
|
||||
|
||||
SmtpPending = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the page by fetching service statuses and updating the SMTP status.
|
||||
/// </summary>
|
||||
private async Task InitPage()
|
||||
{
|
||||
if (InitInProgress || SmtpPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InitInProgress = true;
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
ServiceStatus = await dbContext.WorkerServiceStatuses.ToListAsync();
|
||||
|
||||
var smtpEntry = ServiceStatus.Find(x => x.ServiceName == "AliasVault.SmtpService");
|
||||
if (smtpEntry != null)
|
||||
{
|
||||
LastHeartbeat = smtpEntry.Heartbeat;
|
||||
SmtpStatus = IsHeartbeatValid() && smtpEntry.CurrentStatus == "Started";
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
finally
|
||||
{
|
||||
InitInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the service statuses.
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateServiceStatus(string serviceName, bool newStatus)
|
||||
{
|
||||
// Refresh the DbContext to ensure we get the latest data.
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var entry = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstOrDefaultAsync();
|
||||
if (entry != null)
|
||||
{
|
||||
string newDesiredStatus = newStatus ? "Started" : "Stopped";
|
||||
entry.DesiredStatus = newDesiredStatus;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Wait for service to have updated its status.
|
||||
var timeout = DateTime.Now.AddSeconds(30);
|
||||
while (true)
|
||||
{
|
||||
if (DateTime.Now > timeout)
|
||||
{
|
||||
// Timeout
|
||||
return false;
|
||||
}
|
||||
|
||||
dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var check = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstAsync();
|
||||
if (check.CurrentStatus == newDesiredStatus)
|
||||
{
|
||||
// Done
|
||||
return true;
|
||||
}
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the SMTP service status.
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateSmtpStatus(bool newStatus)
|
||||
{
|
||||
return await UpdateServiceStatus("AliasVault.SmtpService", newStatus);
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,20 @@
|
||||
<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>
|
||||
<NavLink href="settings/server" 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">
|
||||
Server settings
|
||||
</NavLink>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end items-center lg:order-2">
|
||||
<Services />
|
||||
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
|
||||
<ServiceControl ServiceNames="@(new List<string> { "AliasVault.SmtpService", "AliasVault.TaskRunner" })"
|
||||
ServiceDisplayNames="@(new Dictionary<string, string>
|
||||
{
|
||||
{ "AliasVault.SmtpService", "Smtp" },
|
||||
{ "AliasVault.TaskRunner", "Tasks" }
|
||||
})" />
|
||||
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
@@ -52,7 +60,7 @@
|
||||
</div>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
|
||||
<li>
|
||||
<a href="account/manage" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Account settings</a>
|
||||
<a href="account/manage/change-password" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Account settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
@@ -99,6 +107,11 @@
|
||||
Auth logs
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="block border-b dark:border-gray-700">
|
||||
<NavLink href="settings/server" 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">
|
||||
Server settings
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
49
src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs
Normal file
49
src/AliasVault.Admin/Main/Models/UserEmailClaimWithCount.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UserEmailClaimWithCount.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.Admin.Main.Models;
|
||||
|
||||
/// <summary>
|
||||
/// User email claim view model with count.
|
||||
/// </summary>
|
||||
public class UserEmailClaimWithCount
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address.
|
||||
/// </summary>
|
||||
public string Address { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address local.
|
||||
/// </summary>
|
||||
public string AddressLocal { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address domain.
|
||||
/// </summary>
|
||||
public string AddressDomain { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the created at timestamp.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the updated at timestamp.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email count.
|
||||
/// </summary>
|
||||
public int EmailCount { get; set; }
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<LayoutPageTitle>Change password</LayoutPageTitle>
|
||||
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Change password</h3>
|
||||
<EditForm Model="Input" FormName="change-password" OnValidSubmit="OnValidSubmitAsync" method="post" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
@page "/account/manage"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
|
||||
<LayoutPageTitle>Profile</LayoutPageTitle>
|
||||
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Profile</h3>
|
||||
|
||||
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync" class="space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<ValidationSummary class="text-red-600 dark:text-red-400" role="alert"/>
|
||||
<div>
|
||||
<label for="username" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Username</label>
|
||||
<input type="text" value="@username" id="username" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-gray-100 cursor-not-allowed dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400" placeholder="Please choose your username." disabled/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="phone-number" class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-200">Phone number</label>
|
||||
<InputText @bind-Value="Input.PhoneNumber" id="phone-number" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="Please enter your phone number."/>
|
||||
<ValidationMessage For="() => Input.PhoneNumber" class="mt-1 text-sm text-red-600 dark:text-red-400"/>
|
||||
</div>
|
||||
<div>
|
||||
<SubmitButton>Save</SubmitButton>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? username;
|
||||
private string? phoneNumber;
|
||||
|
||||
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
username = await UserManager.GetUserNameAsync(UserService.User());
|
||||
phoneNumber = await UserManager.GetPhoneNumberAsync(UserService.User());
|
||||
|
||||
Input.PhoneNumber ??= phoneNumber;
|
||||
}
|
||||
|
||||
private async Task OnValidSubmitAsync()
|
||||
{
|
||||
if (Input.PhoneNumber != phoneNumber)
|
||||
{
|
||||
var setPhoneResult = await UserManager.SetPhoneNumberAsync(UserService.User(), Input.PhoneNumber);
|
||||
if (!setPhoneResult.Succeeded)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("Phone number could not be set", true);
|
||||
}
|
||||
}
|
||||
|
||||
GlobalNotificationService.AddSuccessMessage("Your profile has been updated", true);
|
||||
}
|
||||
|
||||
private sealed class InputModel
|
||||
{
|
||||
[Phone]
|
||||
[Display(Name = "Phone number")]
|
||||
public string? PhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
@page "/account/manage/2fa"
|
||||
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
|
||||
@inject UserManager<AdminUser> UserManager
|
||||
@inject SignInManager<AdminUser> SignInManager
|
||||
|
||||
<LayoutPageTitle>Two-factor authentication (2FA)</LayoutPageTitle>
|
||||
|
||||
@if (is2FaEnabled)
|
||||
{
|
||||
<div class="mx-auto mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Two-factor authentication (2FA)</h3>
|
||||
|
||||
@if (recoveryCodesLeft == 0)
|
||||
@@ -41,7 +39,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-100 dark:bg-gray-700 rounded-lg">
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Authenticator app</h4>
|
||||
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
|
||||
@if (!hasAuthenticator)
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="Manage account"
|
||||
Description="Manage your profile here.">
|
||||
Description="Manage security settings for the admin account here.">
|
||||
</PageHeader>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<hr class="mb-6 border-t border-gray-300"/>
|
||||
<div class="mx-auto px-4 py-8">
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<div class="w-full md:w-1/4 mb-6 md:mb-0">
|
||||
<ManageNavMenu/>
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
|
||||
<ul class="flex flex-col space-y-1">
|
||||
<li>
|
||||
<NavLink href="account/manage" Match="NavLinkMatch.All" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Profile</NavLink>
|
||||
<NavLink href="account/manage/change-password" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">Password</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink href="account/manage/change-password" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Password</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink href="account/manage/2fa" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150">Two-factor authentication</NavLink>
|
||||
<NavLink href="account/manage/2fa" class="block px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors duration-150" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">Two-factor authentication</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Active users</h3>
|
||||
<button
|
||||
@onclick="ToggleUserNames"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
@(ShowUserNames ? "Hide names" : "Show names")
|
||||
</button>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 24 hours</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last24Hours</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last24HourUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 7 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last7Days</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last7DayUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="bg-green-50 dark:bg-green-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last14Days</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last14DayUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private UserStatistics UserStats { get; set; } = new();
|
||||
private bool ShowUserNames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the card.
|
||||
/// </summary>
|
||||
public async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var last24Hours = now.AddHours(-24);
|
||||
var last7Days = now.AddDays(-7);
|
||||
var last14Days = now.AddDays(-14);
|
||||
|
||||
// Get user statistics
|
||||
var (count24h, users24h) = await GetActiveUserCount(last24Hours);
|
||||
var (count7d, users7d) = await GetActiveUserCount(last7Days);
|
||||
var (count14d, users14d) = await GetActiveUserCount(last14Days);
|
||||
|
||||
UserStats = new UserStatistics
|
||||
{
|
||||
Last24Hours = count24h,
|
||||
Last7Days = count7d,
|
||||
Last14Days = count14d,
|
||||
Last24HourUsers = users24h,
|
||||
Last7DayUsers = users7d,
|
||||
Last14DayUsers = users14d
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task<(int count, List<string> users)> GetActiveUserCount(DateTime since)
|
||||
{
|
||||
// Get unique users who either:
|
||||
// 1. Have successful auth logs
|
||||
// 2. Have updated their vault
|
||||
var activeUsers = await DbContext.AuthLogs
|
||||
.Where(l => l.Timestamp >= since && l.IsSuccess)
|
||||
.Select(l => l.Username)
|
||||
.Union(
|
||||
DbContext.Vaults
|
||||
.Where(v => v.UpdatedAt >= since)
|
||||
.Select(v => v.User.UserName!)
|
||||
)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
return (activeUsers.Count, activeUsers);
|
||||
}
|
||||
|
||||
private void ToggleUserNames()
|
||||
{
|
||||
ShowUserNames = !ShowUserNames;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private sealed class UserStatistics
|
||||
{
|
||||
public int Last24Hours { get; set; }
|
||||
public int Last7Days { get; set; }
|
||||
public int Last14Days { get; set; }
|
||||
public List<string> Last24HourUsers { get; set; } = new();
|
||||
public List<string> Last7DayUsers { get; set; } = new();
|
||||
public List<string> Last14DayUsers { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Recent emails received</h3>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="bg-primary-50 dark:bg-gray-700/50 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 24 hours</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Last24Hours</h4>
|
||||
</div>
|
||||
<div class="bg-primary-50 dark:bg-gray-700/50 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 7 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Last7Days</h4>
|
||||
</div>
|
||||
<div class="bg-primary-50 dark:bg-gray-700/50 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Last14Days</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private EmailStatistics EmailStats { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the card.
|
||||
/// </summary>
|
||||
public async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var last24Hours = now.AddHours(-24);
|
||||
var last7Days = now.AddDays(-7);
|
||||
var last14Days = now.AddDays(-14);
|
||||
|
||||
// Get email statistics
|
||||
var emailQuery = DbContext.Emails.AsQueryable();
|
||||
EmailStats = new EmailStatistics
|
||||
{
|
||||
Last24Hours = await emailQuery.CountAsync(e => e.DateSystem >= last24Hours),
|
||||
Last7Days = await emailQuery.CountAsync(e => e.DateSystem >= last7Days),
|
||||
Last14Days = await emailQuery.CountAsync(e => e.DateSystem >= last14Days)
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private sealed class EmailStatistics
|
||||
{
|
||||
public int Last24Hours { get; set; }
|
||||
public int Last7Days { get; set; }
|
||||
public int Last14Days { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">User registrations</h3>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 24 hours</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Last24Hours</h4>
|
||||
</div>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 7 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Last7Days</h4>
|
||||
</div>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 14 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Last14Days</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private RegistrationStatistics RegistrationStats { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the card.
|
||||
/// </summary>
|
||||
public async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var last24Hours = now.AddHours(-24);
|
||||
var last7Days = now.AddDays(-7);
|
||||
var last14Days = now.AddDays(-14);
|
||||
|
||||
// Get registration statistics
|
||||
var registrationQuery = DbContext.AliasVaultUsers.AsQueryable();
|
||||
RegistrationStats = new RegistrationStatistics
|
||||
{
|
||||
Last24Hours = await registrationQuery.CountAsync(u => u.CreatedAt >= last24Hours),
|
||||
Last7Days = await registrationQuery.CountAsync(u => u.CreatedAt >= last7Days),
|
||||
Last14Days = await registrationQuery.CountAsync(u => u.CreatedAt >= last14Days)
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private sealed class RegistrationStatistics
|
||||
{
|
||||
public int Last24Hours { get; set; }
|
||||
public int Last7Days { get; set; }
|
||||
public int Last14Days { get; set; }
|
||||
}
|
||||
}
|
||||
60
src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor
Normal file
60
src/AliasVault.Admin/Main/Pages/Dashboard/Index.razor
Normal file
@@ -0,0 +1,60 @@
|
||||
@page "/"
|
||||
@using AliasVault.Admin.Main.Pages.Dashboard.Components
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>Home</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="AliasVault Admin"
|
||||
Description="Welcome to the AliasVault admin portal. Below you can find statistics about recent email activity and active users.">
|
||||
<CustomActions>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
<div class="px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<ActiveUsersCard @ref="_activeUsersCard" />
|
||||
<RegistrationStatisticsCard @ref="_registrationStatisticsCard" />
|
||||
<EmailStatisticsCard @ref="_emailStatisticsCard" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private ActiveUsersCard? _activeUsersCard;
|
||||
private RegistrationStatisticsCard? _registrationStatisticsCard;
|
||||
private EmailStatisticsCard? _emailStatisticsCard;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// Check if 2FA is enabled. If not, show a one-time warning on the dashboard.
|
||||
if (!UserService.User().TwoFactorEnabled)
|
||||
{
|
||||
GlobalNotificationService.AddWarningMessage("Two-factor authentication is not enabled. It is recommended to enable it in Account Settings for better security.", true);
|
||||
}
|
||||
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the cards.
|
||||
/// </summary>
|
||||
private async Task RefreshData()
|
||||
{
|
||||
if (_activeUsersCard != null &&
|
||||
_registrationStatisticsCard != null &&
|
||||
_emailStatisticsCard != null)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
_activeUsersCard.RefreshData(),
|
||||
_registrationStatisticsCard.RefreshData(),
|
||||
_emailStatisticsCard.RefreshData()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
@page "/"
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>Home</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="AliasVault Admin"
|
||||
Description="Welcome to the AliasVault admin portal.">
|
||||
</PageHeader>
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
|
||||
// Redirect to users page.
|
||||
NavigationService.RedirectTo("users");
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ using Microsoft.JSInterop;
|
||||
/// Also, a default set of breadcrumbs is added in the parent OnInitialized method.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class MainBase : OwningComponentBase
|
||||
public abstract class MainBase : OwningComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the NavigationService instance responsible for handling navigation, replaces the default NavigationManager.
|
||||
@@ -102,18 +102,6 @@ public class MainBase : OwningComponentBase
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = NavigationService.BaseUri });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
// Check if 2FA is enabled. If not, show a persistent notification.
|
||||
if (!UserService.User().TwoFactorEnabled)
|
||||
{
|
||||
GlobalNotificationService.AddWarningMessage("Two-factor authentication is not enabled. Please enable it in Account Settings for better security.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the username from the authentication state asynchronously.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
@inherits MainBase
|
||||
|
||||
<div class="mb-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var job in JobList)
|
||||
{
|
||||
<SortableTableRow>
|
||||
<SortableTableColumn IsPrimary="true">@job.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@job.RunDate.ToString("yyyy-MM-dd")</SortableTableColumn>
|
||||
<SortableTableColumn>@job.StartTime.ToString("HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@(job.EndTime?.ToString("HH:mm") ?? "-")</SortableTableColumn>
|
||||
<SortableTableColumn>
|
||||
@{
|
||||
string bgColor = job.Status switch
|
||||
{
|
||||
TaskRunnerJobStatus.Pending => "bg-yellow-500",
|
||||
TaskRunnerJobStatus.Running => "bg-blue-500",
|
||||
TaskRunnerJobStatus.Finished => "bg-green-500",
|
||||
TaskRunnerJobStatus.Error => "bg-red-500",
|
||||
_ => "bg-gray-500"
|
||||
};
|
||||
}
|
||||
<span class="px-2 py-1 rounded-full text-white @bgColor">
|
||||
@job.Status
|
||||
</span>
|
||||
</SortableTableColumn>
|
||||
<SortableTableColumn>@(job.IsOnDemand ? "Yes" : "No")</SortableTableColumn>
|
||||
<SortableTableColumn Title="@job.ErrorMessage">
|
||||
@if (!string.IsNullOrEmpty(job.ErrorMessage))
|
||||
{
|
||||
<span class="text-red-600 dark:text-red-400">@(job.ErrorMessage.Length > 50 ? job.ErrorMessage[..50] + "..." : job.ErrorMessage)</span>
|
||||
}
|
||||
</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly List<TableColumn> _tableColumns =
|
||||
[
|
||||
new TableColumn { Title = "ID", PropertyName = "Id" },
|
||||
new TableColumn { Title = "Date", PropertyName = "RunDate" },
|
||||
new TableColumn { Title = "Start", PropertyName = "StartTime" },
|
||||
new TableColumn { Title = "End", PropertyName = "EndTime" },
|
||||
new TableColumn { Title = "Status", PropertyName = "Status" },
|
||||
new TableColumn { Title = "On-Demand", PropertyName = "IsOnDemand" },
|
||||
new TableColumn { Title = "Error", PropertyName = "ErrorMessage" },
|
||||
];
|
||||
|
||||
private List<TaskRunnerJob> JobList { get; set; } = [];
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 5;
|
||||
private int TotalRecords { get; set; }
|
||||
private string SortColumn { get; set; } = "Id";
|
||||
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed in the table.
|
||||
/// </summary>
|
||||
public async Task RefreshData()
|
||||
{
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var query = dbContext.TaskRunnerJobs.AsQueryable();
|
||||
|
||||
// Apply sorting
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => EF.Property<object>(x, SortColumn))
|
||||
: query.OrderByDescending(x => EF.Property<object>(x, SortColumn));
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
JobList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
private async Task HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
166
src/AliasVault.Admin/Main/Pages/Settings/Server.razor
Normal file
166
src/AliasVault.Admin/Main/Pages/Settings/Server.razor
Normal file
@@ -0,0 +1,166 @@
|
||||
@page "/settings/server"
|
||||
@inject ServerSettingsService SettingsService
|
||||
@inject ILogger<ServerSettingsService> Logger
|
||||
@using AliasVault.Shared.Models.Enums
|
||||
@using AliasVault.Shared.Server.Models
|
||||
@using AliasVault.Shared.Server.Services
|
||||
@using AliasVault.Admin.Main.Pages.Settings.Components
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>Server settings</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="Server settings"
|
||||
Description="Configure AliasVault server settings.">
|
||||
<CustomActions>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
<ConfirmButton OnClick="SaveSettings">Save changes</ConfirmButton>
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
<div class="px-4">
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Data Retention</h3>
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-2 sm:gap-6 sm:mb-5">
|
||||
<div>
|
||||
<label for="generalLogRetention" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">General Log Retention (days)</label>
|
||||
<input type="number" @bind="Settings.GeneralLogRetentionDays" id="generalLogRetention" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Set to 0 to disable automatic cleanup</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="authLogRetention" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Auth Log Retention (days)</label>
|
||||
<input type="number" @bind="Settings.AuthLogRetentionDays" id="authLogRetention" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Set to 0 to disable automatic cleanup</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="emailRetention" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Email Retention (days)</label>
|
||||
<input type="number" @bind="Settings.EmailRetentionDays" id="emailRetention" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Set to 0 to disable automatic cleanup</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="maxEmails" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Max Emails per User</label>
|
||||
<input type="number" @bind="Settings.MaxEmailsPerUser" id="maxEmails" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Set to 0 for unlimited emails</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Maintenance Schedule</h3>
|
||||
<div class="mb-4">
|
||||
<label for="schedule" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Time (24h format)</label>
|
||||
<input type="time" @bind="Settings.MaintenanceTime" id="schedule" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Time when maintenance tasks are run</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Run on Days</label>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
@foreach (var day in DaysOfWeek)
|
||||
{
|
||||
var isSelected = Settings.TaskRunnerDays.Contains(day.Key);
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" checked="@isSelected" @onchange="@(e => ToggleDay(day.Key))" id="@($"day_{day.Key}")" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="@($"day_{day.Key}")" class="ml-2 text-sm font-medium text-gray-900 dark:text-gray-300">@day.Value</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-2 text-md font-medium text-gray-900 dark:text-white">Manual Execution</h4>
|
||||
<ConfirmButton OnClick="RunMaintenanceTasksNow">Run Maintenance Tasks Now</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Maintenance History</h3>
|
||||
<TaskRunnerHistory @ref="_taskRunnerHistoryComponent" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private TaskRunnerHistory? _taskRunnerHistoryComponent;
|
||||
|
||||
private ServerSettingsModel Settings { get; set; } = new();
|
||||
private readonly Dictionary<int, string> DaysOfWeek = new()
|
||||
{
|
||||
{ 1, "Monday" },
|
||||
{ 2, "Tuesday" },
|
||||
{ 3, "Wednesday" },
|
||||
{ 4, "Thursday" },
|
||||
{ 5, "Friday" },
|
||||
{ 6, "Saturday" },
|
||||
{ 7, "Sunday" }
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Settings = await SettingsService.GetAllSettingsAsync();
|
||||
}
|
||||
|
||||
private void ToggleDay(int day)
|
||||
{
|
||||
if (Settings.TaskRunnerDays.Contains(day))
|
||||
{
|
||||
Settings.TaskRunnerDays.Remove(day);
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings.TaskRunnerDays.Add(day);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
await SettingsService.SaveSettingsAsync(Settings);
|
||||
GlobalNotificationService.AddSuccessMessage("Settings saved successfully", true);
|
||||
}
|
||||
|
||||
private async Task RunMaintenanceTasksNow()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var job = new TaskRunnerJob
|
||||
{
|
||||
Name = nameof(TaskRunnerJobType.Maintenance),
|
||||
RunDate = DateTime.Now.Date,
|
||||
StartTime = TimeOnly.FromDateTime(DateTime.Now),
|
||||
Status = TaskRunnerJobStatus.Pending,
|
||||
IsOnDemand = true
|
||||
};
|
||||
|
||||
dbContext.TaskRunnerJobs.Add(job);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Refresh the history component to show the new job
|
||||
if (_taskRunnerHistoryComponent != null)
|
||||
{
|
||||
await _taskRunnerHistoryComponent.RefreshData();
|
||||
}
|
||||
|
||||
Logger.LogWarning("Maintenance tasks manually queued.");
|
||||
GlobalNotificationService.AddSuccessMessage("Maintenance tasks queued. They will be executed on the next polling cycle (default every minute). Check the logs for details.", true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage($"Failed to start maintenance tasks: {ex.Message}", true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the page.
|
||||
/// </summary>
|
||||
private async Task RefreshData()
|
||||
{
|
||||
Settings = await SettingsService.GetAllSettingsAsync();
|
||||
|
||||
// Refresh the history component to show the new job
|
||||
if (_taskRunnerHistoryComponent != null)
|
||||
{
|
||||
await _taskRunnerHistoryComponent.RefreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
<SortableTableColumn IsPrimary="true">@entry.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.Address</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.EmailCount</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
@@ -16,7 +17,7 @@
|
||||
/// Gets or sets the list of email claims to display.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public List<UserEmailClaim> EmailClaimList { get; set; } = [];
|
||||
public List<UserEmailClaimWithCount> EmailClaimList { get; set; } = [];
|
||||
|
||||
private string SortColumn { get; set; } = "CreatedAt";
|
||||
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
||||
@@ -25,9 +26,10 @@
|
||||
new TableColumn { Title = "ID", PropertyName = "Id" },
|
||||
new TableColumn { Title = "Created", PropertyName = "CreatedAt" },
|
||||
new TableColumn { Title = "Email", PropertyName = "Address" },
|
||||
new TableColumn { Title = "Email Count", PropertyName = "EmailCount" },
|
||||
];
|
||||
|
||||
private IEnumerable<UserEmailClaim> SortedEmailClaimList => SortList(EmailClaimList, SortColumn, SortDirection);
|
||||
private IEnumerable<UserEmailClaimWithCount> SortedEmailClaimList => SortList(EmailClaimList, SortColumn, SortDirection);
|
||||
|
||||
private void HandleSortChanged((string column, SortDirection direction) sort)
|
||||
{
|
||||
@@ -36,13 +38,14 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private static IEnumerable<UserEmailClaim> SortList(List<UserEmailClaim> emailClaims, string sortColumn, SortDirection sortDirection)
|
||||
private static IEnumerable<UserEmailClaimWithCount> SortList(List<UserEmailClaimWithCount> emailClaims, string sortColumn, SortDirection sortDirection)
|
||||
{
|
||||
return sortColumn switch
|
||||
{
|
||||
"Id" => SortableTable.SortListByProperty(emailClaims, e => e.Id, sortDirection),
|
||||
"CreatedAt" => SortableTable.SortListByProperty(emailClaims, e => e.CreatedAt, sortDirection),
|
||||
"Address" => SortableTable.SortListByProperty(emailClaims, e => e.Address, sortDirection),
|
||||
"EmailCount" => SortableTable.SortListByProperty(emailClaims, e => e.EmailCount, sortDirection),
|
||||
_ => emailClaims
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ else
|
||||
private int TwoFactorKeysCount { get; set; }
|
||||
private List<AliasVaultUserRefreshToken> RefreshTokenList { get; set; } = [];
|
||||
private List<Vault> VaultList { get; set; } = [];
|
||||
private List<UserEmailClaim> EmailClaimList { get; set; } = [];
|
||||
private List<UserEmailClaimWithCount> EmailClaimList { get; set; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -171,7 +171,18 @@ else
|
||||
.ToListAsync();
|
||||
|
||||
// Load all email claims for this user.
|
||||
EmailClaimList = await DbContext.UserEmailClaims.Where(x => x.UserId == User.Id)
|
||||
EmailClaimList = await DbContext.UserEmailClaims
|
||||
.Where(x => x.UserId == User.Id)
|
||||
.Select(x => new UserEmailClaimWithCount
|
||||
{
|
||||
Id = x.Id,
|
||||
Address = x.Address,
|
||||
AddressLocal = x.AddressLocal,
|
||||
AddressDomain = x.AddressDomain,
|
||||
CreatedAt = x.CreatedAt,
|
||||
UpdatedAt = x.UpdatedAt,
|
||||
EmailCount = DbContext.Emails.Count(e => e.To == x.Address)
|
||||
})
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@using AliasVault.Admin
|
||||
@using AliasVault.Admin.Auth.Components
|
||||
@using AliasVault.Admin.Main
|
||||
@using AliasVault.Admin.Main.Layout
|
||||
@using AliasVault.Admin.Main.Components
|
||||
@using AliasVault.Admin.Main.Components.Alerts
|
||||
@using AliasVault.Admin.Main.Components.Layout
|
||||
@@ -27,4 +28,3 @@
|
||||
@using AliasVault.Admin.Services
|
||||
@using AliasServerDb
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using AliasVault.Auth;
|
||||
using AliasVault.Cryptography.Server;
|
||||
using AliasVault.Logging;
|
||||
using AliasVault.RazorComponents.Services;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -53,6 +54,7 @@ builder.Services.AddScoped<AuthenticationStateProvider, RevalidatingAuthenticati
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<AuthLoggingService>();
|
||||
builder.Services.AddScoped<ConfirmModalService>();
|
||||
builder.Services.AddScoped<ServerSettingsService>();
|
||||
builder.Services.AddSingleton(new VersionedContentService(Directory.GetCurrentDirectory() + "/wwwroot"));
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
|
||||
@@ -139,10 +139,7 @@ public class GlobalNotificationService
|
||||
messages.Add(new KeyValuePair<string, string>("error", message));
|
||||
}
|
||||
|
||||
// Clear messages
|
||||
SuccessMessages.Clear();
|
||||
InfoMessages.Clear();
|
||||
ErrorMessages.Clear();
|
||||
ClearMessages();
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
6
src/AliasVault.Admin/package-lock.json
generated
6
src/AliasVault.Admin/package-lock.json
generated
@@ -710,9 +710,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -988,6 +988,14 @@ video {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
|
||||
@@ -1258,6 +1266,11 @@ video {
|
||||
background-color: rgb(251 203 116 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 224 150 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-primary-500 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(244 149 65 / var(--tw-bg-opacity));
|
||||
@@ -1757,6 +1770,11 @@ video {
|
||||
color: rgb(154 93 38 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -1952,6 +1970,30 @@ video {
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-900\/30:is(.dark *) {
|
||||
background-color: rgb(30 58 138 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-gray-700\/50:is(.dark *) {
|
||||
background-color: rgb(55 65 81 / 0.5);
|
||||
}
|
||||
|
||||
.dark\:bg-green-900\/30:is(.dark *) {
|
||||
background-color: rgb(20 83 45 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-blue-950\/80:is(.dark *) {
|
||||
background-color: rgb(23 37 84 / 0.8);
|
||||
}
|
||||
|
||||
.dark\:bg-gray-800\/80:is(.dark *) {
|
||||
background-color: rgb(31 41 55 / 0.8);
|
||||
}
|
||||
|
||||
.dark\:bg-green-950\/80:is(.dark *) {
|
||||
background-color: rgb(5 46 22 / 0.8);
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
@@ -2085,6 +2127,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-gray-300:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(209 213 219 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-blue-500:focus:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
@@ -2227,6 +2274,10 @@ video {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.md\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2268,6 +2319,10 @@ video {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.lg\:mb-0 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.lg\:mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
<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="9.0.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.3.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="Swashbuckle.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -262,16 +262,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
return Unauthorized("User not found (name-2)");
|
||||
}
|
||||
|
||||
// Check if the refresh token is valid.
|
||||
var existingToken = await context.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(t => t.UserId == user.Id && t.Value == tokenModel.RefreshToken);
|
||||
if (existingToken == null || existingToken.ExpireDate < timeProvider.UtcNow)
|
||||
// Generate new tokens for the user.
|
||||
var token = await GenerateNewTokensForUser(user, tokenModel.RefreshToken);
|
||||
if (token == null)
|
||||
{
|
||||
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TokenRefresh, AuthFailureReason.InvalidRefreshToken);
|
||||
return Unauthorized("Refresh token expired");
|
||||
return Unauthorized("Invalid refresh token");
|
||||
}
|
||||
|
||||
// Generate new tokens for the user.
|
||||
var token = await GenerateNewTokensForUser(user, existingToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
await authLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.TokenRefresh);
|
||||
@@ -345,7 +343,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
UserName = model.Username,
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
PasswordChangedAt = DateTime.UtcNow,
|
||||
PasswordChangedAt = timeProvider.UtcNow,
|
||||
};
|
||||
|
||||
user.Vaults.Add(new AliasServerDb.Vault
|
||||
@@ -459,6 +457,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
private static (bool IsValid, string ErrorMessage) ValidateUsername(string username)
|
||||
{
|
||||
const int minimumUsernameLength = 3;
|
||||
const int maximumUsernameLength = 40;
|
||||
const string adminUsername = "admin";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
@@ -468,7 +467,12 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
|
||||
if (username.Length < minimumUsernameLength)
|
||||
{
|
||||
return (false, $"Username must be at least {minimumUsernameLength} characters long.");
|
||||
return (false, $"Username too short: must be at least {minimumUsernameLength} characters long.");
|
||||
}
|
||||
|
||||
if (username.Length > maximumUsernameLength)
|
||||
{
|
||||
return (false, $"Username too long: cannot be longer than {maximumUsernameLength} characters.");
|
||||
}
|
||||
|
||||
if (string.Equals(username, adminUsername, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -678,9 +682,9 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
/// to the database.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to generate the tokens for.</param>
|
||||
/// <param name="existingToken">The existing token that is being replaced (optional).</param>
|
||||
/// <returns>TokenModel which includes new access and refresh token.</returns>
|
||||
private async Task<TokenModel> GenerateNewTokensForUser(AliasVaultUser user, AliasVaultUserRefreshToken existingToken)
|
||||
/// <param name="existingTokenValue">The existing token value that is being replaced (optional).</param>
|
||||
/// <returns>TokenModel which includes new access and refresh token. Returns null if provided refresh token is invalid.</returns>
|
||||
private async Task<TokenModel?> GenerateNewTokensForUser(AliasVaultUser user, string existingTokenValue)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
await Semaphore.WaitAsync();
|
||||
@@ -693,7 +697,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
var existingTokenReuseWindow = timeProvider.UtcNow.AddSeconds(-30);
|
||||
var existingTokenReuse = await context.AliasVaultUserRefreshTokens
|
||||
.FirstOrDefaultAsync(t => t.UserId == user.Id &&
|
||||
t.PreviousTokenValue == existingToken.Value &&
|
||||
t.PreviousTokenValue == existingTokenValue &&
|
||||
t.CreatedAt > existingTokenReuseWindow);
|
||||
|
||||
if (existingTokenReuse is not null)
|
||||
@@ -704,15 +708,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
return new TokenModel { Token = accessToken, RefreshToken = existingTokenReuse.Value };
|
||||
}
|
||||
|
||||
// Remove the existing refresh token.
|
||||
var tokenToDelete = await context.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(t => t.Id == existingToken.Id);
|
||||
if (tokenToDelete is null)
|
||||
// Check if the refresh token still exists and is not expired.
|
||||
var existingToken = await context.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(t => t.UserId == user.Id && t.Value == existingTokenValue);
|
||||
if (existingToken == null || existingToken.ExpireDate < timeProvider.UtcNow)
|
||||
{
|
||||
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TokenRefresh, AuthFailureReason.InvalidRefreshToken);
|
||||
throw new InvalidOperationException("Refresh token does not exist (anymore).");
|
||||
return null;
|
||||
}
|
||||
|
||||
context.AliasVaultUserRefreshTokens.Remove(tokenToDelete);
|
||||
context.AliasVaultUserRefreshTokens.Remove(existingToken);
|
||||
|
||||
// New refresh token lifetime is the same as the existing one.
|
||||
var existingTokenLifetime = existingToken.ExpireDate - existingToken.CreatedAt;
|
||||
|
||||
@@ -89,11 +89,8 @@ public class EmailController(ILogger<VaultController> logger, IDbContextFactory<
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
// Delete associated attachments
|
||||
context.EmailAttachments.RemoveRange(email!.Attachments);
|
||||
|
||||
// Delete the email
|
||||
context.Emails.Remove(email);
|
||||
// Delete the email - attachments will be cascade deleted
|
||||
context.Emails.Remove(email!);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<a href="/">
|
||||
<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 class="relative">
|
||||
<span class="relative inline-flex flex-wrap items-center">
|
||||
AliasVault
|
||||
<span class="absolute -top-2 bg-primary-500 text-white text-xs px-2 py-0.5 rounded-full font-normal">BETA</span>
|
||||
<span class="ml-2 bg-primary-500 text-white text-xs px-2 py-0.5 rounded-full font-normal sm:absolute sm:-top-2 sm:ml-1">BETA</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Email</h3>
|
||||
</div>
|
||||
<div class="flex justify-end items-center space-x-2">
|
||||
@if (RefreshTimer is not null)
|
||||
@if (DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="Auto-refresh enabled"></div>
|
||||
}
|
||||
@@ -56,27 +56,27 @@
|
||||
<div class="overflow-hidden shadow sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Subject
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Date & Time
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Subject
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Date & Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
@foreach (var mail in MailboxEmails)
|
||||
{
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</span>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var mail in MailboxEmails)
|
||||
{
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</span>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -99,13 +99,56 @@
|
||||
private EmailApiModel Email { get; set; } = new();
|
||||
private bool EmailModalVisible { get; set; }
|
||||
private string Error { get; set; } = string.Empty;
|
||||
private Timer? RefreshTimer { get; set; }
|
||||
|
||||
private bool IsRefreshing { get; set; } = true;
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
private bool IsSpamOk { get; set; } = false;
|
||||
|
||||
private bool IsPageVisible { get; set; } = true;
|
||||
private CancellationTokenSource? PollingCancellationTokenSource { get; set; }
|
||||
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
|
||||
private readonly SemaphoreSlim RefreshSemaphore = new(1, 1);
|
||||
private DateTime LastRefreshTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by JavaScript when the page visibility changes.
|
||||
/// </summary>
|
||||
/// <param name="isVisible">Boolean whether the page is visible or not.</param>
|
||||
/// <returns>Task.</returns>
|
||||
[JSInvokable]
|
||||
public async Task OnVisibilityChange(bool isVisible)
|
||||
{
|
||||
IsPageVisible = isVisible;
|
||||
if (isVisible)
|
||||
{
|
||||
// Only enable auto-refresh if the setting is enabled.
|
||||
if (DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
await StartPolling();
|
||||
}
|
||||
|
||||
// Refresh immediately when tab becomes visible
|
||||
await ManualRefresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cancel polling.
|
||||
if (PollingCancellationTokenSource is not null)
|
||||
{
|
||||
await PollingCancellationTokenSource.CancelAsync();
|
||||
}
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
PollingCancellationTokenSource?.Cancel();
|
||||
PollingCancellationTokenSource?.Dispose();
|
||||
RefreshSemaphore.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -124,12 +167,29 @@
|
||||
}
|
||||
IsSpamOk = IsSpamOkDomain(EmailAddress);
|
||||
|
||||
// Set up visibility change detection
|
||||
await JsInteropService.RegisterVisibilityCallback(DotNetObjectReference.Create(this));
|
||||
|
||||
// Only enable auto-refresh if the setting is enabled.
|
||||
if (DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
RefreshTimer = new Timer(2000);
|
||||
RefreshTimer.Elapsed += async (sender, e) => await TimerRefresh();
|
||||
RefreshTimer.Start();
|
||||
await StartPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (!ShowComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await ManualRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,25 +206,62 @@
|
||||
IsSpamOk = IsSpamOkDomain(EmailAddress);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// Start the polling for new emails.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task StartPolling()
|
||||
{
|
||||
RefreshTimer?.Dispose();
|
||||
if (PollingCancellationTokenSource is not null)
|
||||
{
|
||||
await PollingCancellationTokenSource.CancelAsync();
|
||||
}
|
||||
|
||||
PollingCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
while (!PollingCancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
if (IsPageVisible)
|
||||
{
|
||||
// Only auto refresh when the tab is visible.
|
||||
await RefreshWithThrottling();
|
||||
await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, PollingCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal cancellation, ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
/// <summary>
|
||||
/// Refresh the emails with throttling to prevent multiple refreshes at the same time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task RefreshWithThrottling()
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (!ShowComponent)
|
||||
if (!await RefreshSemaphore.WaitAsync(0)) // Don't wait if a refresh is in progress
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender)
|
||||
try
|
||||
{
|
||||
await ManualRefresh();
|
||||
var timeSinceLastRefresh = DateTime.UtcNow - LastRefreshTime;
|
||||
if (timeSinceLastRefresh.TotalMilliseconds < ACTIVE_TAB_REFRESH_INTERVAL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadRecentEmailsAsync();
|
||||
LastRefreshTime = DateTime.UtcNow;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RefreshSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,15 +281,10 @@
|
||||
return Config.PrivateEmailDomains.Exists(x => email.EndsWith(x));
|
||||
}
|
||||
|
||||
private async Task TimerRefresh()
|
||||
{
|
||||
IsRefreshing = true;
|
||||
StateHasChanged();
|
||||
await LoadRecentEmailsAsync();
|
||||
IsRefreshing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually refresh the emails.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task ManualRefresh()
|
||||
{
|
||||
IsLoading = true;
|
||||
@@ -202,6 +294,10 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// (Re)load recent emails by making an API call to the server.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task LoadRecentEmailsAsync()
|
||||
{
|
||||
if (!ShowComponent || EmailAddress is null)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<input
|
||||
id="searchWidget"
|
||||
type="text"
|
||||
placeholder="Type here to search"
|
||||
placeholder="Search for a service..."
|
||||
autocomplete="off"
|
||||
class="w-full px-4 py-2 text-gray-700 bg-white border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:ring-primary-500"
|
||||
@bind-value="SearchTerm"
|
||||
|
||||
@@ -19,7 +19,7 @@ using Microsoft.AspNetCore.Components.Authorization;
|
||||
/// All pages that inherit from this class will receive default injected components that are used globally.
|
||||
/// Also, a default set of breadcrumbs is added in the parent OnInitialized method.
|
||||
/// </summary>
|
||||
public class MainBase : OwningComponentBase
|
||||
public abstract class MainBase : OwningComponentBase
|
||||
{
|
||||
private const string ReturnUrlKey = "returnUrl";
|
||||
private bool _parametersInitialSet;
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace AliasVault.Client.Services;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
@@ -237,6 +238,16 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
|
||||
await jsRuntime.InvokeVoidAsync("window.scrollTo", 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a visibility callback which is invoked when the visibility of component changes in client.
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">Component type.</typeparam>
|
||||
/// <param name="objRef">DotNetObjectReference.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task RegisterVisibilityCallback<TComponent>(DotNetObjectReference<TComponent> objRef)
|
||||
where TComponent : class =>
|
||||
await jsRuntime.InvokeVoidAsync("window.registerVisibilityCallback", objRef);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a WebAuthn get credential operation.
|
||||
/// </summary>
|
||||
|
||||
6
src/AliasVault.Client/package-lock.json
generated
6
src/AliasVault.Client/package-lock.json
generated
@@ -710,9 +710,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<div class="mt-4 text-center">
|
||||
<p id="security-quote" class="text-sm text-primary-600 italic"></p>
|
||||
</div>
|
||||
<div id="error-message" class="hidden text-red-600 dark:text-red-400 mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,7 +145,7 @@
|
||||
clearInterval(intervalId);
|
||||
} else if (elapsedTime % 1000 < checkInterval) {
|
||||
if (!('WebAssembly' in window)) {
|
||||
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
|
||||
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}
|
||||
@@ -157,7 +158,6 @@
|
||||
const errorMessageElement = document.getElementById('error-message');
|
||||
|
||||
const showError = (message) => {
|
||||
loadingScreen.querySelector('.inner').classList.add('hidden');
|
||||
errorMessageElement.textContent = message;
|
||||
errorMessageElement.classList.remove('hidden');
|
||||
document.querySelector('.loading-progress-text').classList.add('hidden');
|
||||
@@ -167,14 +167,14 @@
|
||||
// Listen for unhandled errors
|
||||
window.addEventListener('error', function(event) {
|
||||
if (event.error && event.error.message && event.error.message.includes('WebAssembly')) {
|
||||
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
|
||||
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
if (event.reason && event.reason.message && event.reason.message.includes('WebAssembly')) {
|
||||
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
|
||||
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -298,3 +298,9 @@ async function createWebAuthnCredentialAndDeriveKey(username) {
|
||||
return { Error: "WEBAUTHN_CREATE_ERROR", Message: createError.message };
|
||||
}
|
||||
}
|
||||
|
||||
window.registerVisibilityCallback = function (dotnetHelper) {
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
dotnetHelper.invokeMethodAsync('OnVisibilityChange', !document.hidden);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
|
||||
/// </summary>
|
||||
public AliasServerDbContext()
|
||||
{
|
||||
SetPragmaSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -34,6 +35,7 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
|
||||
public AliasServerDbContext(DbContextOptions<AliasServerDbContext> options)
|
||||
: base(options)
|
||||
{
|
||||
SetPragmaSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,6 +128,16 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
|
||||
/// </summary>
|
||||
public DbSet<AuthLog> AuthLogs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ServerSettings DbSet.
|
||||
/// </summary>
|
||||
public DbSet<ServerSetting> ServerSettings { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TaskRunnerJobs DbSet.
|
||||
/// </summary>
|
||||
public DbSet<TaskRunnerJob> TaskRunnerJobs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The OnModelCreating method.
|
||||
/// </summary>
|
||||
@@ -237,38 +249,46 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
|
||||
/// <param name="optionsBuilder">DbContextOptionsBuilder instance.</param>
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (!optionsBuilder.IsConfigured)
|
||||
if (optionsBuilder.IsConfigured)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
return;
|
||||
}
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
// Add SQLite connection with enhanced settings
|
||||
optionsBuilder
|
||||
.UseSqlite(
|
||||
configuration.GetConnectionString("AliasServerDbContext") + ";Mode=ReadWriteCreate;Cache=Shared",
|
||||
options => options.CommandTimeout(60))
|
||||
.UseLazyLoadingProxies();
|
||||
// Add SQLite connection with enhanced settings
|
||||
var connectionString = configuration.GetConnectionString("AliasServerDbContext") +
|
||||
";Mode=ReadWriteCreate;Cache=Shared";
|
||||
|
||||
// Set additional PRAGMA settings
|
||||
var connection = Database.GetDbConnection();
|
||||
if (connection.State != System.Data.ConnectionState.Open)
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
optionsBuilder
|
||||
.UseSqlite(connectionString, options => options.CommandTimeout(60))
|
||||
.UseLazyLoadingProxies();
|
||||
}
|
||||
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
// Increase busy timeout
|
||||
command.CommandText = @"
|
||||
/// <summary>
|
||||
/// Sets up the PRAGMA settings for SQLite.
|
||||
/// </summary>
|
||||
private void SetPragmaSettings()
|
||||
{
|
||||
var connection = Database.GetDbConnection();
|
||||
if (connection.State != System.Data.ConnectionState.Open)
|
||||
{
|
||||
connection.Open();
|
||||
}
|
||||
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
// Increase busy timeout
|
||||
command.CommandText = @"
|
||||
PRAGMA busy_timeout = 30000;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = FULL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA temp_store = MEMORY;
|
||||
PRAGMA mmap_size = 1073741824;";
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
848
src/Databases/AliasServerDb/Migrations/20241204121218_AddServerSettingsTable.Designer.cs
generated
Normal file
848
src/Databases/AliasServerDb/Migrations/20241204121218_AddServerSettingsTable.Designer.cs
generated
Normal file
@@ -0,0 +1,848 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContext))]
|
||||
[Migration("20241204121218_AddServerSettingsTable")]
|
||||
partial class AddServerSettingsTable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastPasswordChanged")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("PasswordChangedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PreviousTokenValue")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AliasVaultUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Browser")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int?>("FailureReason")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSuspiciousActivity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "EventType" }, "IX_EventType");
|
||||
|
||||
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
|
||||
|
||||
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
|
||||
|
||||
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
|
||||
.IsDescending(false, false, true);
|
||||
|
||||
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("AuthLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedSymmetricKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("From")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageHtml")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePlain")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePreview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageSource")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PushNotificationSent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("To")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserEncryptionKeyId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.HasIndex("DateSystem");
|
||||
|
||||
b.HasIndex("PushNotificationSent");
|
||||
|
||||
b.HasIndex("ToLocal");
|
||||
|
||||
b.HasIndex("UserEncryptionKeyId");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("Emails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EmailId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Filesize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmailId");
|
||||
|
||||
b.ToTable("EmailAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Log", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Application")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Exception")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Level")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LogEvent")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("LogEvent");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageTemplate")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceContext")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("TimeStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Application");
|
||||
|
||||
b.HasIndex("TimeStamp");
|
||||
|
||||
b.ToTable("Logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressDomain")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressLocal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEmailClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CredentialsCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EmailClaimsCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EncryptionSettings")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("FileSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RevisionNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CurrentStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DesiredStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Heartbeat")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("varchar");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkerServiceStatuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("FriendlyName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Xml")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DataProtectionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("RoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.ToTable("UserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.ToTable("UserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
|
||||
.WithMany("Emails")
|
||||
.HasForeignKey("UserEncryptionKeyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EncryptionKey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.Email", "Email")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("EmailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Email");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EmailClaims")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EncryptionKeys")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("Vaults")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Navigation("EmailClaims");
|
||||
|
||||
b.Navigation("EncryptionKeys");
|
||||
|
||||
b.Navigation("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Navigation("Emails");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddServerSettingsTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ServerSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Key = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ServerSettings", x => x.Key);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ServerSettings");
|
||||
}
|
||||
}
|
||||
}
|
||||
881
src/Databases/AliasServerDb/Migrations/20241215131807_AddTaskRunnerJobTable.Designer.cs
generated
Normal file
881
src/Databases/AliasServerDb/Migrations/20241215131807_AddTaskRunnerJobTable.Designer.cs
generated
Normal file
@@ -0,0 +1,881 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContext))]
|
||||
[Migration("20241215131807_AddTaskRunnerJobTable")]
|
||||
partial class AddTaskRunnerJobTable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastPasswordChanged")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AdminUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("PasswordChangedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PreviousTokenValue")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AliasVaultUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Browser")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DeviceType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int?>("FailureReason")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSuspiciousActivity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex(new[] { "EventType" }, "IX_EventType");
|
||||
|
||||
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
|
||||
|
||||
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
|
||||
|
||||
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
|
||||
.IsDescending(false, false, true);
|
||||
|
||||
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
|
||||
.IsDescending(false, true);
|
||||
|
||||
b.ToTable("AuthLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedSymmetricKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("From")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FromLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageHtml")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePlain")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessagePreview")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageSource")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("PushNotificationSent")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Subject")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("To")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToDomain")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ToLocal")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserEncryptionKeyId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Date");
|
||||
|
||||
b.HasIndex("DateSystem");
|
||||
|
||||
b.HasIndex("PushNotificationSent");
|
||||
|
||||
b.HasIndex("ToLocal");
|
||||
|
||||
b.HasIndex("UserEncryptionKeyId");
|
||||
|
||||
b.HasIndex("Visible");
|
||||
|
||||
b.ToTable("Emails");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("EmailId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Filesize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmailId");
|
||||
|
||||
b.ToTable("EmailAttachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Log", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Application")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Exception")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Level")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LogEvent")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("LogEvent");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("MessageTemplate")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Properties")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceContext")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("TimeStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Application");
|
||||
|
||||
b.HasIndex("TimeStamp");
|
||||
|
||||
b.ToTable("Logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsOnDemand")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TaskRunnerJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressDomain")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressLocal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEmailClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CredentialsCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EmailClaimsCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("EncryptionSettings")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptionType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("FileSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<long>("RevisionNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("CurrentStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DesiredStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("Heartbeat")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("varchar");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkerServiceStatuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("FriendlyName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Xml")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DataProtectionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("RoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.ToTable("UserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.ToTable("UserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("UserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
|
||||
.WithMany("Emails")
|
||||
.HasForeignKey("UserEncryptionKeyId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EncryptionKey");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.Email", "Email")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("EmailId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Email");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EmailClaims")
|
||||
.HasForeignKey("UserId");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("EncryptionKeys")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.HasOne("AliasServerDb.AliasVaultUser", "User")
|
||||
.WithMany("Vaults")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
|
||||
{
|
||||
b.Navigation("EmailClaims");
|
||||
|
||||
b.Navigation("EncryptionKeys");
|
||||
|
||||
b.Navigation("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Email", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Navigation("Emails");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTaskRunnerJobTable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TaskRunnerJobs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
RunDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
StartTime = table.Column<TimeOnly>(type: "TEXT", nullable: false),
|
||||
EndTime = table.Column<TimeOnly>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ErrorMessage = table.Column<string>(type: "TEXT", nullable: true),
|
||||
IsOnDemand = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TaskRunnerJobs", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TaskRunnerJobs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace AliasServerDb.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.10")
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
@@ -464,6 +464,59 @@ namespace AliasServerDb.Migrations
|
||||
b.ToTable("Logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsOnDemand")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TaskRunnerJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
39
src/Databases/AliasServerDb/ServerSetting.cs
Normal file
39
src/Databases/AliasServerDb/ServerSetting.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ServerSetting.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 AliasServerDb;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a server setting in the AliasServerDb.
|
||||
/// </summary>
|
||||
public class ServerSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key of the server setting.
|
||||
/// </summary>
|
||||
[Key]
|
||||
[MaxLength(255)]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the server setting.
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the creation date of the server setting.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the update date of the server setting.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
62
src/Databases/AliasServerDb/TaskRunnerJob.cs
Normal file
62
src/Databases/AliasServerDb/TaskRunnerJob.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TaskRunnerJob.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 AliasServerDb;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using AliasVault.Shared.Models.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a task runner job entry in the AliasServerDb.
|
||||
/// </summary>
|
||||
public class TaskRunnerJob
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the task runner job.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the task runner job.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Column(TypeName = "nvarchar(50)")]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date the job was run.
|
||||
/// </summary>
|
||||
public DateTime RunDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start time of the job.
|
||||
/// </summary>
|
||||
public TimeOnly StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the end time of the job.
|
||||
/// </summary>
|
||||
public TimeOnly? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status of the job.
|
||||
/// </summary>
|
||||
public TaskRunnerJobStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the error message of the job.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this is an on-demand run.
|
||||
/// </summary>
|
||||
public bool IsOnDemand { get; set; }
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SpamOK.PasswordGenerator" Version="1.0.1" />
|
||||
<PackageReference Include="SpamOK.PasswordGenerator" Version="1.1.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<ItemGroup>
|
||||
<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" />
|
||||
<PackageReference Include="MimeKit" Version="4.9.0" />
|
||||
<PackageReference Include="NUglify" Version="1.21.11" />
|
||||
<PackageReference Include="SmtpServer" Version="10.0.1" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -12,7 +12,7 @@ COPY . .
|
||||
|
||||
# Build and publish the application
|
||||
WORKDIR "/src/src/Services/AliasVault.SmtpService"
|
||||
RUN dotnet publish "./AliasVault.SmtpService.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "./AliasVault.SmtpService.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
@@ -298,7 +298,7 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
}
|
||||
|
||||
// Check if the local part of the toAddress is a known alias (claimed by a user)
|
||||
var dbContext = await dbContextFactory.CreateDbContextAsync(CancellationToken.None);
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(CancellationToken.None);
|
||||
var toAddressLocal = toAddress.User.ToLowerInvariant();
|
||||
var toAddressDomain = toAddress.Host.ToLowerInvariant();
|
||||
var userEmailClaim = await dbContext.UserEmailClaims
|
||||
@@ -348,7 +348,7 @@ public class DatabaseMessageStore(ILogger<DatabaseMessageStore> logger, Config c
|
||||
/// <param name="userEncryptionKey">The public key of the user to encrypt the mail contents with.</param>
|
||||
private async Task<int> InsertEmailIntoDatabase(MimeMessage message, MailAddress toAddress, UserEncryptionKey userEncryptionKey)
|
||||
{
|
||||
var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var newEmail = ConvertMimeMessageToEmail(message, toAddress);
|
||||
newEmail = EmailEncryption.EncryptEmail(newEmail, userEncryptionKey);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-AliasVault.TaskRunner-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\net9.0\AliasVault.TaskRunner.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<DocumentationFile>bin\Release\net9.0\AliasVault.TaskRunner.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\AliasVault.Shared.Server\AliasVault.Shared.Server.csproj" />
|
||||
<ProjectReference Include="..\..\Utilities\AliasVault.Logging\AliasVault.Logging.csproj" />
|
||||
<ProjectReference Include="..\..\Utilities\Cryptography\AliasVault.Cryptography.Server\AliasVault.Cryptography.Server.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
20
src/Services/AliasVault.TaskRunner/Dockerfile
Normal file
20
src/Services/AliasVault.TaskRunner/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
||||
WORKDIR /app
|
||||
|
||||
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/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj", "src/Services/AliasVault.TaskRunner/"]
|
||||
RUN dotnet restore "./src/Services/AliasVault.TaskRunner/AliasVault.TaskRunner.csproj"
|
||||
COPY . .
|
||||
|
||||
# Build and publish the application
|
||||
WORKDIR "/src/src/Services/AliasVault.TaskRunner"
|
||||
RUN dotnet publish "./AliasVault.TaskRunner.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "AliasVault.TaskRunner.dll"]
|
||||
48
src/Services/AliasVault.TaskRunner/Program.cs
Normal file
48
src/Services/AliasVault.TaskRunner/Program.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Program.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>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
using System.Reflection;
|
||||
using AliasServerDb;
|
||||
using AliasServerDb.Configuration;
|
||||
using AliasVault.Logging;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using AliasVault.TaskRunner.Tasks;
|
||||
using AliasVault.TaskRunner.Workers;
|
||||
using AliasVault.WorkerStatus.ServiceExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
|
||||
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
||||
builder.Services.ConfigureLogging(builder.Configuration, Assembly.GetExecutingAssembly().GetName().Name!, "../../../logs");
|
||||
|
||||
builder.Services.AddAliasVaultSqliteConfiguration();
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Register hosted services via Status library wrapper in order to monitor and control (start/stop) them via the database.
|
||||
// -----------------------------------------------------------------------
|
||||
builder.Services.AddSingleton<ServerSettingsService>();
|
||||
|
||||
// Define the tasks that will be executed by the TaskRunner.
|
||||
builder.Services.AddTransient<IMaintenanceTask, LogCleanupTask>();
|
||||
builder.Services.AddTransient<IMaintenanceTask, RefreshTokenCleanupTask>();
|
||||
builder.Services.AddTransient<IMaintenanceTask, EmailCleanupTask>();
|
||||
builder.Services.AddTransient<IMaintenanceTask, EmailQuotaCleanupTask>();
|
||||
|
||||
builder.Services.AddStatusHostedService<TaskRunnerWorker, AliasServerDbContext>(Assembly.GetExecutingAssembly().GetName().Name!);
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
using (var scope = host.Services.CreateScope())
|
||||
{
|
||||
var container = scope.ServiceProvider;
|
||||
var factory = container.GetRequiredService<IDbContextFactory<AliasServerDbContext>>();
|
||||
await using var context = await factory.CreateDbContextAsync();
|
||||
await context.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
await host.RunAsync();
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"profiles": {
|
||||
"AliasVault.TaskRunner": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"PRIVATE_EMAIL_DOMAINS": "example.tld",
|
||||
"SMTP_TLS_ENABLED": "false"
|
||||
},
|
||||
"dotnetRunMessages": true
|
||||
},
|
||||
"Container (Dockerfile)": {
|
||||
"commandName": "Docker"
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json"
|
||||
}
|
||||
64
src/Services/AliasVault.TaskRunner/Tasks/EmailCleanupTask.cs
Normal file
64
src/Services/AliasVault.TaskRunner/Tasks/EmailCleanupTask.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailCleanupTask.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.TaskRunner.Tasks;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// A maintenance task that deletes old emails based on server settings.
|
||||
/// </summary>
|
||||
public class EmailCleanupTask : IMaintenanceTask
|
||||
{
|
||||
private readonly ILogger<EmailCleanupTask> _logger;
|
||||
private readonly IDbContextFactory<AliasServerDbContext> _dbContextFactory;
|
||||
private readonly ServerSettingsService _settingsService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailCleanupTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="dbContextFactory">The database context factory.</param>
|
||||
/// <param name="settingsService">The settings service.</param>
|
||||
public EmailCleanupTask(
|
||||
ILogger<EmailCleanupTask> logger,
|
||||
IDbContextFactory<AliasServerDbContext> dbContextFactory,
|
||||
ServerSettingsService settingsService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Email Cleanup";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetAllSettingsAsync();
|
||||
if (settings.EmailRetentionDays <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-settings.EmailRetentionDays);
|
||||
|
||||
// Delete the emails
|
||||
var emailsDeleted = await dbContext.Emails
|
||||
.Where(x => x.DateSystem < cutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails older than {Days} days",
|
||||
emailsDeleted,
|
||||
settings.EmailRetentionDays);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailQuotaCleanupTask.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.TaskRunner.Tasks;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// A maintenance task that enforces email quotas by deleting oldest emails when users exceed their limit.
|
||||
/// </summary>
|
||||
public class EmailQuotaCleanupTask : IMaintenanceTask
|
||||
{
|
||||
private readonly ILogger<EmailQuotaCleanupTask> _logger;
|
||||
private readonly IDbContextFactory<AliasServerDbContext> _dbContextFactory;
|
||||
private readonly ServerSettingsService _settingsService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailQuotaCleanupTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="dbContextFactory">The database context factory.</param>
|
||||
/// <param name="settingsService">The settings service.</param>
|
||||
public EmailQuotaCleanupTask(
|
||||
ILogger<EmailQuotaCleanupTask> logger,
|
||||
IDbContextFactory<AliasServerDbContext> dbContextFactory,
|
||||
ServerSettingsService settingsService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Email Quota Cleanup";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetAllSettingsAsync();
|
||||
if (settings.MaxEmailsPerUser <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
// Get all users with their email claims
|
||||
var userEmailClaims = await dbContext.UserEmailClaims
|
||||
.Select(c => new { c.UserId, c.Address })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var totalEmailsDeleted = 0;
|
||||
var usersProcessed = 0;
|
||||
|
||||
// Group email claims by user
|
||||
foreach (var userGroup in userEmailClaims.GroupBy(c => c.UserId))
|
||||
{
|
||||
var userAddresses = userGroup.Select(c => c.Address).ToList();
|
||||
|
||||
// Get total email count for this user
|
||||
var emailCount = await dbContext.Emails
|
||||
.Where(e => userAddresses.Contains(e.To))
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
if (emailCount > settings.MaxEmailsPerUser)
|
||||
{
|
||||
// Calculate how many emails need to be deleted
|
||||
var deleteCount = emailCount - settings.MaxEmailsPerUser;
|
||||
|
||||
// Delete the oldest emails - attachments will be cascade deleted
|
||||
var emailsDeleted = await dbContext.Emails
|
||||
.Where(e => userAddresses.Contains(e.To))
|
||||
.OrderBy(e => e.DateSystem)
|
||||
.Take(deleteCount)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (emailsDeleted > 0)
|
||||
{
|
||||
totalEmailsDeleted += emailsDeleted;
|
||||
usersProcessed++;
|
||||
_logger.LogWarning(
|
||||
"Deleted {EmailCount} emails for user {UserId} to maintain quota of {MaxEmails}",
|
||||
emailsDeleted,
|
||||
userGroup.Key,
|
||||
settings.MaxEmailsPerUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Deleted {TotalEmails} emails across {UserCount} users to maintain quota of {MaxEmails} max emails per user",
|
||||
totalEmailsDeleted,
|
||||
usersProcessed,
|
||||
settings.MaxEmailsPerUser);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IMaintenanceTask.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.TaskRunner.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for maintenance tasks that can be executed by the TaskRunner.
|
||||
/// </summary>
|
||||
public interface IMaintenanceTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the task.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Executes the maintenance task.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
Task ExecuteAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
73
src/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs
Normal file
73
src/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="LogCleanupTask.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.TaskRunner.Tasks;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// A maintenance task that deletes old log entries.
|
||||
/// </summary>
|
||||
public class LogCleanupTask : IMaintenanceTask
|
||||
{
|
||||
private readonly ILogger<LogCleanupTask> _logger;
|
||||
private readonly IDbContextFactory<AliasServerDbContext> _dbContextFactory;
|
||||
private readonly ServerSettingsService _settingsService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LogCleanupTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="dbContextFactory">The database context factory.</param>
|
||||
/// <param name="settingsService">The settings service.</param>
|
||||
public LogCleanupTask(
|
||||
ILogger<LogCleanupTask> logger,
|
||||
IDbContextFactory<AliasServerDbContext> dbContextFactory,
|
||||
ServerSettingsService settingsService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Log Cleanup";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _settingsService.GetAllSettingsAsync();
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (settings.GeneralLogRetentionDays > 0)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-settings.GeneralLogRetentionDays);
|
||||
var deletedCount = await dbContext.Logs
|
||||
.Where(x => x.TimeStamp < cutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
_logger.LogWarning("Deleted {Count} general log entries older than {Days} days", deletedCount, settings.GeneralLogRetentionDays);
|
||||
|
||||
// Delete old task runner jobs
|
||||
var jobCutoffDate = DateTime.UtcNow.AddDays(-settings.GeneralLogRetentionDays);
|
||||
var deletedJobCount = await dbContext.TaskRunnerJobs
|
||||
.Where(x => x.RunDate < jobCutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
_logger.LogWarning("Deleted {Count} task runner job entries older than {Days} days", deletedJobCount, settings.GeneralLogRetentionDays);
|
||||
}
|
||||
|
||||
if (settings.AuthLogRetentionDays > 0)
|
||||
{
|
||||
var cutoffDate = DateTime.UtcNow.AddDays(-settings.AuthLogRetentionDays);
|
||||
var deletedCount = await dbContext.AuthLogs
|
||||
.Where(x => x.Timestamp < cutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
_logger.LogWarning("Deleted {Count} auth log entries older than {Days} days", deletedCount, settings.AuthLogRetentionDays);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="RefreshTokenCleanupTask.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.TaskRunner.Tasks;
|
||||
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// A maintenance task that deletes expired refresh tokens.
|
||||
/// </summary>
|
||||
public class RefreshTokenCleanupTask : IMaintenanceTask
|
||||
{
|
||||
private readonly ILogger<RefreshTokenCleanupTask> _logger;
|
||||
private readonly IDbContextFactory<AliasServerDbContext> _dbContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RefreshTokenCleanupTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="dbContextFactory">The database context factory.</param>
|
||||
public RefreshTokenCleanupTask(
|
||||
ILogger<RefreshTokenCleanupTask> logger,
|
||||
IDbContextFactory<AliasServerDbContext> dbContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Refresh Token Cleanup";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var cutoffDate = DateTime.UtcNow;
|
||||
var deletedCount = await dbContext.AliasVaultUserRefreshTokens
|
||||
.Where(x => x.ExpireDate < cutoffDate)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (deletedCount > 0)
|
||||
{
|
||||
_logger.LogWarning("Deleted {Count} expired refresh tokens", deletedCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/Services/AliasVault.TaskRunner/Workers/TaskRunnerWorker.cs
Normal file
133
src/Services/AliasVault.TaskRunner/Workers/TaskRunnerWorker.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TaskRunnerWorker.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.TaskRunner.Workers;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Models.Enums;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using AliasVault.TaskRunner.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// A worker for the TaskRunner.
|
||||
/// </summary>
|
||||
/// <param name="logger">ILogger instance.</param>
|
||||
/// <param name="tasks">List of maintenance tasks.</param>
|
||||
/// <param name="settingsService">Server settings service.</param>
|
||||
/// <param name="dbContextFactory">Database context factory.</param>
|
||||
public class TaskRunnerWorker(
|
||||
ILogger<TaskRunnerWorker> logger,
|
||||
IEnumerable<IMaintenanceTask> tasks,
|
||||
ServerSettingsService settingsService,
|
||||
IDbContextFactory<AliasServerDbContext> dbContextFactory) : BackgroundService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogWarning("TaskRunnerWorker started at: {Time}", DateTimeOffset.Now);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(stoppingToken);
|
||||
var settings = await settingsService.GetAllSettingsAsync();
|
||||
var now = DateTime.Now;
|
||||
var today = now.Date;
|
||||
|
||||
// Check for on-demand run request
|
||||
var onDemandJob = await dbContext.TaskRunnerJobs
|
||||
.Where(j => j.IsOnDemand && j.Status == TaskRunnerJobStatus.Pending)
|
||||
.OrderByDescending(j => j.StartTime)
|
||||
.FirstOrDefaultAsync(stoppingToken);
|
||||
|
||||
if (onDemandJob != null)
|
||||
{
|
||||
await ExecuteMaintenanceTasks(onDemandJob, dbContext, stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Regular scheduled run logic
|
||||
var scheduledTime = settings.MaintenanceTime;
|
||||
var currentTime = TimeOnly.FromDateTime(now);
|
||||
var shouldRunToday = settings.TaskRunnerDays.Contains((int)now.DayOfWeek + 1);
|
||||
var hasPassedScheduledTime = currentTime >= scheduledTime;
|
||||
|
||||
if (shouldRunToday && hasPassedScheduledTime)
|
||||
{
|
||||
var existingJob = await dbContext.TaskRunnerJobs
|
||||
.Where(j => j.Name == nameof(TaskRunnerJobType.Maintenance) && !j.IsOnDemand && j.RunDate.Date == today)
|
||||
.OrderByDescending(j => j.StartTime)
|
||||
.FirstOrDefaultAsync(stoppingToken);
|
||||
|
||||
if (existingJob == null)
|
||||
{
|
||||
var job = new TaskRunnerJob
|
||||
{
|
||||
Name = nameof(TaskRunnerJobType.Maintenance),
|
||||
RunDate = today,
|
||||
StartTime = TimeOnly.FromDateTime(now),
|
||||
Status = TaskRunnerJobStatus.Running,
|
||||
IsOnDemand = false,
|
||||
};
|
||||
|
||||
dbContext.TaskRunnerJobs.Add(job);
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
|
||||
await ExecuteMaintenanceTasks(job, dbContext, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check every minute for schedule changes or on-demand requests
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the maintenance tasks.
|
||||
/// </summary>
|
||||
/// <param name="job">The job to execute.</param>
|
||||
/// <param name="dbContext">The database context.</param>
|
||||
/// <param name="stoppingToken">The cancellation token.</param>
|
||||
private async Task ExecuteMaintenanceTasks(TaskRunnerJob job, AliasServerDbContext dbContext, CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogWarning("Starting maintenance tasks at {Time} (On-demand: {IsOnDemand})", DateTime.Now, job.IsOnDemand);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
try
|
||||
{
|
||||
job.Status = TaskRunnerJobStatus.Running;
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
await task.ExecuteAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error executing task {TaskName}", task.Name);
|
||||
job.ErrorMessage = $"Task {task.Name} failed: {ex.Message}";
|
||||
job.Status = TaskRunnerJobStatus.Error;
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (job.Status != TaskRunnerJobStatus.Error)
|
||||
{
|
||||
job.Status = TaskRunnerJobStatus.Finished;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
job.EndTime = TimeOnly.FromDateTime(DateTime.Now);
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
}
|
||||
|
||||
logger.LogInformation("Tasks completed with status: {Status}", job.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/Services/AliasVault.TaskRunner/appsettings.json
Normal file
12
src/Services/AliasVault.TaskRunner/appsettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"AliasServerDbContext": "Data Source=../../../database/AliasServerDb.sqlite"
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ public static class AppInfo
|
||||
/// <summary>
|
||||
/// Gets the minor version number.
|
||||
/// </summary>
|
||||
public const int VersionMinor = 8;
|
||||
public const int VersionMinor = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the patch version number.
|
||||
/// </summary>
|
||||
public const int VersionPatch = 1;
|
||||
public const int VersionPatch = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the build number, typically used in CI/CD pipelines.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DocumentationFile>bin\Debug\net9.0\AliasVault.Shared.Server.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\Release\net9.0\AliasVault.Shared.Server.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\stylecop.json">
|
||||
<Link>stylecop.json</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ServerSettingsModel.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Server.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Server settings model.
|
||||
/// </summary>
|
||||
public class ServerSettingsModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the general log retention days. Defaults to 30.
|
||||
/// </summary>
|
||||
public int GeneralLogRetentionDays { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the auth log retention days. Defaults to 30.
|
||||
/// </summary>
|
||||
public int AuthLogRetentionDays { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email retention days. Defaults to 0 (unlimited).
|
||||
/// </summary>
|
||||
public int EmailRetentionDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the max emails per user. Defaults to 0 (unlimited).
|
||||
/// </summary>
|
||||
public int MaxEmailsPerUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time when maintenance tasks are run (24h format). Defaults to 00:00.
|
||||
/// </summary>
|
||||
public TimeOnly MaintenanceTime { get; set; } = new(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the task runner days. Defaults to all days of the week.
|
||||
/// </summary>
|
||||
public List<int> TaskRunnerDays { get; set; } = [1, 2, 3, 4, 5, 6, 7];
|
||||
}
|
||||
5
src/Shared/AliasVault.Shared.Server/README.md
Normal file
5
src/Shared/AliasVault.Shared.Server/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# AliasVault.Shared.Server
|
||||
|
||||
This project contains shared functionality used only by the server applications and not required by the client applications.
|
||||
|
||||
This is to reduce the number of client dependencies and keep the client applications as lightweight as possible.
|
||||
@@ -0,0 +1,165 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ServerSettingsService.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Server.Services;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Server.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Server settings service.
|
||||
/// </summary>
|
||||
/// <param name="dbContextFactory">IDbContextFactory instance.</param>
|
||||
public class ServerSettingsService(IDbContextFactory<AliasServerDbContext> dbContextFactory)
|
||||
{
|
||||
private readonly Dictionary<string, string?> _cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the setting async.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The setting.</returns>
|
||||
public async Task<string?> GetSettingAsync(string key)
|
||||
{
|
||||
if (_cache.TryGetValue(key, out var cachedValue))
|
||||
{
|
||||
return cachedValue;
|
||||
}
|
||||
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(CancellationToken.None);
|
||||
var setting = await dbContext.ServerSettings.FirstOrDefaultAsync(x => x.Key == key);
|
||||
|
||||
_cache[key] = setting?.Value;
|
||||
return setting?.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the setting async.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public async Task SetSettingAsync(string key, string? value)
|
||||
{
|
||||
// First check if the value is already cached and matches
|
||||
if (_cache.TryGetValue(key, out var cachedValue) && cachedValue == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(CancellationToken.None);
|
||||
var setting = await dbContext.ServerSettings.FirstOrDefaultAsync(x => x.Key == key);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// If setting exists and value hasn't changed, return early
|
||||
if (setting?.Value == value)
|
||||
{
|
||||
// Update cache to match database
|
||||
_cache[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (setting == null)
|
||||
{
|
||||
setting = new ServerSetting
|
||||
{
|
||||
Key = key,
|
||||
Value = value,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
dbContext.ServerSettings.Add(setting);
|
||||
}
|
||||
else
|
||||
{
|
||||
setting.Value = value;
|
||||
setting.UpdatedAt = now;
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
_cache[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all settings async.
|
||||
/// </summary>
|
||||
/// <returns>The settings.</returns>
|
||||
public async Task<ServerSettingsModel> GetAllSettingsAsync()
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(CancellationToken.None);
|
||||
var settings = await dbContext.ServerSettings.ToDictionaryAsync(x => x.Key, x => x.Value);
|
||||
|
||||
// Create model with defaults
|
||||
var model = new ServerSettingsModel();
|
||||
|
||||
// Only override if parsing succeeds
|
||||
if (int.TryParse(settings.GetValueOrDefault("GeneralLogRetentionDays"), out var generalDays))
|
||||
{
|
||||
model.GeneralLogRetentionDays = generalDays;
|
||||
}
|
||||
|
||||
if (int.TryParse(settings.GetValueOrDefault("AuthLogRetentionDays"), out var authDays))
|
||||
{
|
||||
model.AuthLogRetentionDays = authDays;
|
||||
}
|
||||
|
||||
if (int.TryParse(settings.GetValueOrDefault("EmailRetentionDays"), out var emailDays))
|
||||
{
|
||||
model.EmailRetentionDays = emailDays;
|
||||
}
|
||||
|
||||
if (int.TryParse(settings.GetValueOrDefault("MaxEmailsPerUser"), out var maxEmails))
|
||||
{
|
||||
model.MaxEmailsPerUser = maxEmails;
|
||||
}
|
||||
|
||||
if (TimeOnly.TryParse(
|
||||
settings.GetValueOrDefault("MaintenanceTime") ?? "00:00",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var time))
|
||||
{
|
||||
model.MaintenanceTime = time;
|
||||
}
|
||||
|
||||
var taskRunnerDaysStr = settings.GetValueOrDefault("TaskRunnerDays");
|
||||
if (!string.IsNullOrEmpty(taskRunnerDaysStr))
|
||||
{
|
||||
try
|
||||
{
|
||||
model.TaskRunnerDays = taskRunnerDaysStr.Split(',').Select(int.Parse).ToList();
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// Keep default if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the settings async.
|
||||
/// </summary>
|
||||
/// <param name="model">The model.</param>
|
||||
/// <returns>A task.</returns>
|
||||
public async Task SaveSettingsAsync(ServerSettingsModel model)
|
||||
{
|
||||
await SetSettingAsync("GeneralLogRetentionDays", model.GeneralLogRetentionDays.ToString());
|
||||
await SetSettingAsync("AuthLogRetentionDays", model.AuthLogRetentionDays.ToString());
|
||||
await SetSettingAsync("EmailRetentionDays", model.EmailRetentionDays.ToString());
|
||||
await SetSettingAsync("MaxEmailsPerUser", model.MaxEmailsPerUser.ToString());
|
||||
await SetSettingAsync("MaintenanceTime", model.MaintenanceTime.ToString("HH:mm", CultureInfo.InvariantCulture));
|
||||
await SetSettingAsync("TaskRunnerDays", string.Join(",", model.TaskRunnerDays));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TaskRunnerJobStatus.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// The status of a task runner job.
|
||||
/// </summary>
|
||||
public enum TaskRunnerJobStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The job is pending.
|
||||
/// </summary>
|
||||
Pending = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The job is running.
|
||||
/// </summary>
|
||||
Running = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The job has finished.
|
||||
/// </summary>
|
||||
Finished = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The job has failed.
|
||||
/// </summary>
|
||||
Error = 9,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TaskRunnerJobType.cs" company="lanedirt">
|
||||
// Copyright (c) lanedirt. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// The type of a task runner job.
|
||||
/// </summary>
|
||||
public enum TaskRunnerJobType
|
||||
{
|
||||
/// <summary>
|
||||
/// The job is pending.
|
||||
/// </summary>
|
||||
Maintenance,
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2" />
|
||||
<PackageReference Include="NUnit" Version="4.3.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -109,6 +109,6 @@ public class AdminPlaywrightTest : PlaywrightTest
|
||||
await WaitForUrlAsync("**", "Users");
|
||||
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("This page gives an overview of all registered users and the associated vaults"), "No entry page content visible after logging in to admin app.");
|
||||
Assert.That(pageContent, Does.Contain("Welcome to the AliasVault admin portal"), "No entry page content visible after logging in to admin app.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public abstract class PlaywrightTest
|
||||
/// <summary>
|
||||
/// Gets or sets random unique account email that is used for the test.
|
||||
/// </summary>
|
||||
protected virtual string TestUserUsername { get; set; } = $"{Guid.NewGuid()}@test.com";
|
||||
protected virtual string TestUserUsername { get; set; } = $"{Guid.NewGuid().ToString()[..10]}@test.com";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets random unique account password that is used for the test.
|
||||
@@ -201,7 +201,7 @@ public abstract class PlaywrightTest
|
||||
/// </summary>
|
||||
protected void SetRandomTestUserCredentials()
|
||||
{
|
||||
TestUserUsername = $"{Guid.NewGuid()}@test.com";
|
||||
TestUserUsername = $"{Guid.NewGuid().ToString()[..10]}@test.com";
|
||||
TestUserPassword = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ServerSettingsTests.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.Tests.Admin;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end tests for server settings feature.
|
||||
/// </summary>
|
||||
[Parallelizable(ParallelScope.Self)]
|
||||
[Category("AdminTests")]
|
||||
[TestFixture]
|
||||
public class ServerSettingsTests : AdminPlaywrightTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test if mutating server settings works correctly.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task ServerSettingsMutationTest()
|
||||
{
|
||||
// Navigate to server settings page
|
||||
await NavigateBrowser("settings/server");
|
||||
await WaitForUrlAsync("settings/server", "Server settings");
|
||||
|
||||
// Set new values for retention settings
|
||||
await Page.Locator("input[id='generalLogRetention']").FillAsync("45");
|
||||
await Page.Locator("input[id='authLogRetention']").FillAsync("120");
|
||||
await Page.Locator("input[id='emailRetention']").FillAsync("60");
|
||||
await Page.Locator("input[id='maxEmails']").FillAsync("200");
|
||||
|
||||
// Set maintenance time
|
||||
await Page.Locator("input[id='schedule']").FillAsync("03:30");
|
||||
|
||||
// Uncheck Sunday and Saturday from maintenance days
|
||||
await Page.Locator("input[id='day_7']").UncheckAsync(); // Sunday
|
||||
await Page.Locator("input[id='day_6']").UncheckAsync(); // Saturday
|
||||
|
||||
// Save changes
|
||||
var saveButton = Page.Locator("text=Save changes");
|
||||
await saveButton.ClickAsync();
|
||||
|
||||
// Wait for success message
|
||||
await WaitForUrlAsync("settings/server", "Settings saved successfully");
|
||||
|
||||
// Verify settings in database
|
||||
var settings = await DbContext.ServerSettings.ToListAsync();
|
||||
|
||||
// Check retention settings
|
||||
var generalLogRetention = settings.Find(s => s.Key == "GeneralLogRetentionDays");
|
||||
Assert.That(generalLogRetention?.Value, Is.EqualTo("45"), "General log retention days not saved correctly");
|
||||
|
||||
var authLogRetention = settings.Find(s => s.Key == "AuthLogRetentionDays");
|
||||
Assert.That(authLogRetention?.Value, Is.EqualTo("120"), "Auth log retention days not saved correctly");
|
||||
|
||||
var emailRetention = settings.Find(s => s.Key == "EmailRetentionDays");
|
||||
Assert.That(emailRetention?.Value, Is.EqualTo("60"), "Email retention days not saved correctly");
|
||||
|
||||
var maxEmails = settings.Find(s => s.Key == "MaxEmailsPerUser");
|
||||
Assert.That(maxEmails?.Value, Is.EqualTo("200"), "Max emails per user not saved correctly");
|
||||
|
||||
// Check maintenance schedule
|
||||
var maintenanceTime = settings.Find(s => s.Key == "MaintenanceTime");
|
||||
Assert.That(maintenanceTime?.Value, Is.EqualTo("03:30"), "Maintenance time not saved correctly");
|
||||
|
||||
var taskRunnerDays = settings.Find(s => s.Key == "TaskRunnerDays");
|
||||
Assert.That(taskRunnerDays?.Value, Is.EqualTo("1,2,3,4,5"), "Task runner days not saved correctly");
|
||||
|
||||
// Refresh page and verify values persist
|
||||
await Page.ReloadAsync();
|
||||
await WaitForUrlAsync("settings/server", "Server settings");
|
||||
|
||||
var generalLogRetentionValue = await Page.Locator("input[id='generalLogRetention']").InputValueAsync();
|
||||
Assert.That(generalLogRetentionValue, Is.EqualTo("45"), "General log retention value not persisted after refresh");
|
||||
|
||||
var maintenanceTimeValue = await Page.Locator("input[id='schedule']").InputValueAsync();
|
||||
Assert.That(maintenanceTimeValue, Does.Contain("03:30"), "Maintenance time value not persisted after refresh");
|
||||
|
||||
// Verify weekend days are still unchecked
|
||||
var sundayChecked = await Page.Locator("input[id='day_7']").IsCheckedAsync();
|
||||
var saturdayChecked = await Page.Locator("input[id='day_6']").IsCheckedAsync();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(sundayChecked, Is.False, "Sunday checkbox should be unchecked");
|
||||
Assert.That(saturdayChecked, Is.False, "Saturday checkbox should be unchecked");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="BrowserWasmTests.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.Tests.Client.Shard3;
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end tests for user two-factor authentication.
|
||||
/// </summary>
|
||||
[Parallelizable(ParallelScope.Self)]
|
||||
[Category("ClientTests")]
|
||||
[TestFixture]
|
||||
public class BrowserWasmTests : ClientPlaywrightTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test if setting up two-factor authentication and then logging in works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
public async Task ShowsWarningWhenWebAssemblyNotSupported()
|
||||
{
|
||||
// Store current browser context and page.
|
||||
var originalContext = Context;
|
||||
var originalPage = Page;
|
||||
|
||||
try
|
||||
{
|
||||
// Create a new browser context and page with WebAssembly disabled to test the error message.
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.Development.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
bool headless = configuration.GetValue("PlaywrightSettings:Headless", true);
|
||||
var playwright = await Playwright.CreateAsync();
|
||||
Browser = await playwright.Chromium.LaunchAsync(new()
|
||||
{
|
||||
Args = ["--js-flags=--noexpose-wasm"],
|
||||
Headless = headless,
|
||||
});
|
||||
Context = await Browser.NewContextAsync();
|
||||
Page = await Context.NewPageAsync();
|
||||
|
||||
// Navigate to the app.
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
|
||||
// Wait for error message to appear.
|
||||
var errorMessage = Page.Locator("#error-message");
|
||||
await errorMessage.WaitForAsync(new LocatorWaitForOptions
|
||||
{
|
||||
State = WaitForSelectorState.Visible,
|
||||
Timeout = 5000,
|
||||
});
|
||||
|
||||
// Verify the error message.
|
||||
var message = await errorMessage.TextContentAsync();
|
||||
Assert.That(message, Does.Contain("AliasVault requires WebAssembly"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clean up the test context and page.
|
||||
await Page.CloseAsync();
|
||||
await Context.CloseAsync();
|
||||
|
||||
// Restore original context and page for further tests.
|
||||
Context = originalContext;
|
||||
Page = originalPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,11 +68,49 @@ public class AuthTests : ClientPlaywrightTest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if logging out and logging in works.
|
||||
/// Test if logging in with different case variations of username works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(3)]
|
||||
public async Task CapitalizedUsernameTest()
|
||||
{
|
||||
// Logout current user
|
||||
await Logout();
|
||||
|
||||
// Create a new user with capital letters in username
|
||||
var capitalUsername = "TestUser@Example.com";
|
||||
await Register(checkForSuccess: true, username: capitalUsername);
|
||||
await Logout();
|
||||
|
||||
// Test Case 1: Try to login with lowercase version of the username
|
||||
var lowercaseUsername = capitalUsername.ToLower();
|
||||
await LoginWithUsername(lowercaseUsername);
|
||||
await VerifySuccessfulLogin();
|
||||
|
||||
// Test Case 2: Try to login with exact capitalized username
|
||||
await Logout();
|
||||
await LoginWithUsername(capitalUsername);
|
||||
await VerifySuccessfulLogin();
|
||||
|
||||
// Test Case 3: Create new user with lowercase
|
||||
await Logout();
|
||||
var lowercaseUser = "testuser2@example.com";
|
||||
await Register(checkForSuccess: true, username: lowercaseUser);
|
||||
await Logout();
|
||||
|
||||
// Try logging in with uppercase version
|
||||
var uppercaseVersion = lowercaseUser.ToUpper();
|
||||
await LoginWithUsername(uppercaseVersion);
|
||||
await VerifySuccessfulLogin();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if logging out and logging in works.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(4)]
|
||||
public async Task LogoutAndLoginRememberMeTest()
|
||||
{
|
||||
await Logout();
|
||||
@@ -101,7 +139,7 @@ public class AuthTests : ClientPlaywrightTest
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(4)]
|
||||
[Order(5)]
|
||||
public async Task RegisterFormWarningTest()
|
||||
{
|
||||
await Logout();
|
||||
@@ -116,7 +154,7 @@ public class AuthTests : ClientPlaywrightTest
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(5)]
|
||||
[Order(6)]
|
||||
public async Task PasswordAuthLockoutTest()
|
||||
{
|
||||
await Logout();
|
||||
@@ -152,4 +190,41 @@ public class AuthTests : ClientPlaywrightTest
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain("locked out"), "No account lockout message.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Login with a given username.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to login with.</param>
|
||||
/// <returns>Async task.</returns>
|
||||
private async Task LoginWithUsername(string username)
|
||||
{
|
||||
await NavigateToLogin();
|
||||
|
||||
var emailField = await WaitForAndGetElement("input[id='email']");
|
||||
var passwordField = await WaitForAndGetElement("input[id='password']");
|
||||
await emailField.FillAsync(username);
|
||||
await passwordField.FillAsync(TestUserPassword);
|
||||
|
||||
var loginButton = await WaitForAndGetElement("button[type='submit']");
|
||||
await loginButton.ClickAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that a login was successful.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
private async Task VerifySuccessfulLogin()
|
||||
{
|
||||
// Wait for the index page to load which should show "Credentials" in the top menu.
|
||||
await WaitForUrlAsync("**", "Credentials");
|
||||
|
||||
// Check if the login was successful by verifying content.
|
||||
var pageContent = await Page.TextContentAsync("body");
|
||||
Assert.That(pageContent, Does.Contain(WelcomeMessage), "No index content after logging in.");
|
||||
|
||||
// Check if login has created an auth log entry.
|
||||
var authLogEntry = await ApiDbContext.AuthLogs.FirstOrDefaultAsync(x =>
|
||||
x.EventType == AuthEventType.Login);
|
||||
Assert.That(authLogEntry, Is.Not.Null, "Auth log entry not found in database after login.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,4 +116,55 @@ public class UserSetupTests : ClientPlaywrightTest
|
||||
var errorMessage = await WaitForAndGetElement("text='Username is already in use.'");
|
||||
Assert.That(errorMessage, Is.Not.Null, "The 'Username is already in use' error message should appear.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the "Username too short" and "Username too long" error appears when trying to register with an invalid username.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(3)]
|
||||
public async Task UserSetupUsernameLengthTest()
|
||||
{
|
||||
// Logout.
|
||||
await Logout();
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
await WaitForUrlAsync("user/start", "Create new vault");
|
||||
|
||||
// Click the "Create new vault" anchor tag.
|
||||
var createVaultButton = await WaitForAndGetElement("a:has-text('Create new vault')");
|
||||
await createVaultButton.ClickAsync();
|
||||
|
||||
// Wait for the terms and conditions to load.
|
||||
await WaitForUrlAsync("user/setup", "Terms and Conditions");
|
||||
|
||||
// Accept the terms and conditions.
|
||||
var acceptTermsCheckbox = await WaitForAndGetElement("input[id='agreeTerms']");
|
||||
await acceptTermsCheckbox.CheckAsync();
|
||||
|
||||
// Wait for the continue button to be enabled.
|
||||
await Task.Delay(100);
|
||||
|
||||
// Press the continue button.
|
||||
var continueButton = await WaitForAndGetElement("button:has-text('Continue')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Wait for the username step to load.
|
||||
await WaitForUrlAsync("user/setup", "Username");
|
||||
var usernameField = await WaitForAndGetElement("input[id='username']");
|
||||
await usernameField.FillAsync("ts"); // Too short username (2 chars)
|
||||
|
||||
// Check if the "Username is too short" error message appears
|
||||
var errorMessage = await WaitForAndGetElement("text='Username too short: must be at least 3 characters long.'");
|
||||
Assert.That(errorMessage, Is.Not.Null, "The 'Username too short' error message should appear.");
|
||||
|
||||
// Clear the username field.
|
||||
await usernameField.FillAsync(string.Empty);
|
||||
|
||||
// Fill in a too long username (41 chars).
|
||||
await usernameField.FillAsync("asdasdasdasdasdasdasdasdasdaaaasasddsdasd"); // Too long username (41 chars)
|
||||
|
||||
// Check if the "Username is too short" error message appears
|
||||
errorMessage = await WaitForAndGetElement("text='Username too long: cannot be longer than 40 characters.'");
|
||||
Assert.That(errorMessage, Is.Not.Null, "The 'Username too long' error message should appear.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||
<PackageReference Include="NUnit" Version="4.2.2"/>
|
||||
<PackageReference Include="NUnit" 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">
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Services\AliasVault.SmtpService\AliasVault.SmtpService.csproj" />
|
||||
<ProjectReference Include="..\..\Services\AliasVault.TaskRunner\AliasVault.TaskRunner.csproj" />
|
||||
<ProjectReference Include="..\..\Utilities\Cryptography\AliasVault.Cryptography.Server\AliasVault.Cryptography.Server.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
213
src/Tests/AliasVault.IntegrationTests/TaskRunner/SeedData.cs
Normal file
213
src/Tests/AliasVault.IntegrationTests/TaskRunner/SeedData.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SeedData.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.IntegrationTests.TaskRunner;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Models.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for seeding the database with test data.
|
||||
/// </summary>
|
||||
public static class SeedData
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds the database with test data.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public static async Task SeedDatabase(AliasServerDbContext dbContext)
|
||||
{
|
||||
// Seed the database with settings
|
||||
var settings = new List<ServerSetting>
|
||||
{
|
||||
new() { Key = "EmailRetentionDays", Value = "30" },
|
||||
new() { Key = "GeneralLogRetentionDays", Value = "45" },
|
||||
new() { Key = "AuthLogRetentionDays", Value = "60" },
|
||||
new() { Key = "MaxEmailsPerUser", Value = "100" },
|
||||
new() { Key = "MaintenanceTime", Value = "00:00" },
|
||||
new() { Key = "TaskRunnerDays", Value = "1,2,3,4,5,6,7" },
|
||||
};
|
||||
|
||||
await dbContext.ServerSettings.AddRangeAsync(settings);
|
||||
|
||||
// Create test user
|
||||
var user = new AliasVaultUser
|
||||
{
|
||||
UserName = "testuser",
|
||||
Email = "testuser@example.tld",
|
||||
};
|
||||
dbContext.AliasVaultUsers.Add(user);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Create encryption key for the user
|
||||
var encryptionKey = new UserEncryptionKey
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = user.Id,
|
||||
PublicKey = "test-encryption-key",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
dbContext.UserEncryptionKeys.Add(encryptionKey);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
await SeedEmails(dbContext, encryptionKey.Id);
|
||||
await SeedLogs(dbContext);
|
||||
await SeedAuthLogs(dbContext);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the database with test emails.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context.</param>
|
||||
/// <param name="encryptionKeyId">The encryption key ID.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private static async Task SeedEmails(AliasServerDbContext dbContext, Guid encryptionKeyId)
|
||||
{
|
||||
// Seed old emails (older than 30 days)
|
||||
var oldEmails = new List<Email>();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
oldEmails.Add(CreateTestEmail(i, -45, encryptionKeyId, "Old Email"));
|
||||
}
|
||||
|
||||
await dbContext.Emails.AddRangeAsync(oldEmails);
|
||||
|
||||
// Seed recent emails (within 30 days)
|
||||
var recentEmails = new List<Email>();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
recentEmails.Add(CreateTestEmail(i, -1, encryptionKeyId, "Recent Email"));
|
||||
}
|
||||
|
||||
await dbContext.Emails.AddRangeAsync(recentEmails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the database with test logs.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private static async Task SeedLogs(AliasServerDbContext dbContext)
|
||||
{
|
||||
// Add old general logs (older than 45 days)
|
||||
var oldLogs = new List<Log>();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
oldLogs.Add(CreateTestLog(i, -60, "Old Log"));
|
||||
}
|
||||
|
||||
await dbContext.Logs.AddRangeAsync(oldLogs);
|
||||
|
||||
// Add recent logs (within 45 days)
|
||||
var recentLogs = new List<Log>();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
recentLogs.Add(CreateTestLog(i, -1, "Recent Log"));
|
||||
}
|
||||
|
||||
await dbContext.Logs.AddRangeAsync(recentLogs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the database with test auth logs.
|
||||
/// </summary>
|
||||
/// <param name="dbContext">The database context.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private static async Task SeedAuthLogs(AliasServerDbContext dbContext)
|
||||
{
|
||||
// Add old auth logs (older than 60 days)
|
||||
var oldAuthLogs = new List<AuthLog>();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
oldAuthLogs.Add(CreateTestAuthLog(i, -70));
|
||||
}
|
||||
|
||||
await dbContext.AuthLogs.AddRangeAsync(oldAuthLogs);
|
||||
|
||||
// Add recent auth logs (within 60 days)
|
||||
var recentAuthLogs = new List<AuthLog>();
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
recentAuthLogs.Add(CreateTestAuthLog(i, -1));
|
||||
}
|
||||
|
||||
await dbContext.AuthLogs.AddRangeAsync(recentAuthLogs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test email.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <param name="daysOffset">The days offset.</param>
|
||||
/// <param name="encryptionKeyId">The encryption key ID.</param>
|
||||
/// <param name="prefix">The prefix.</param>
|
||||
/// <returns>Email.</returns>
|
||||
private static Email CreateTestEmail(int index, int daysOffset, Guid encryptionKeyId, string prefix)
|
||||
{
|
||||
return new Email
|
||||
{
|
||||
Subject = $"{prefix} {index}",
|
||||
From = "sender@example.com",
|
||||
FromLocal = "sender",
|
||||
FromDomain = "example.com",
|
||||
To = "testuser@example.tld",
|
||||
ToLocal = "testuser",
|
||||
ToDomain = "example.tld",
|
||||
Date = DateTime.UtcNow.AddDays(daysOffset),
|
||||
DateSystem = DateTime.UtcNow.AddDays(daysOffset),
|
||||
MessagePlain = "Test message",
|
||||
MessagePreview = "Test message",
|
||||
MessageSource = "Test source",
|
||||
EncryptedSymmetricKey = "dummy-key",
|
||||
UserEncryptionKeyId = encryptionKeyId,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test log.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <param name="daysOffset">The days offset.</param>
|
||||
/// <param name="prefix">The prefix.</param>
|
||||
/// <returns>Log.</returns>
|
||||
private static Log CreateTestLog(int index, int daysOffset, string prefix)
|
||||
{
|
||||
return new Log
|
||||
{
|
||||
Application = "TestApp",
|
||||
SourceContext = "TestContext",
|
||||
Message = $"{prefix} {index}",
|
||||
MessageTemplate = $"{prefix} {index}",
|
||||
Level = "Information",
|
||||
TimeStamp = DateTime.UtcNow.AddDays(daysOffset),
|
||||
Exception = string.Empty,
|
||||
Properties = "{}",
|
||||
LogEvent = "{}",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a test auth log.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <param name="daysOffset">The days offset.</param>
|
||||
/// <returns>AuthLog.</returns>
|
||||
private static AuthLog CreateTestAuthLog(int index, int daysOffset)
|
||||
{
|
||||
return new AuthLog
|
||||
{
|
||||
Username = "testuser",
|
||||
EventType = AuthEventType.Login,
|
||||
IsSuccess = true,
|
||||
Timestamp = DateTime.UtcNow.AddDays(daysOffset),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TaskRunnerTests.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.IntegrationTests.TaskRunner;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for TaskRunner service.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class TaskRunnerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// The test host instance.
|
||||
/// </summary>
|
||||
private IHost _testHost;
|
||||
|
||||
/// <summary>
|
||||
/// The test host builder instance.
|
||||
/// </summary>
|
||||
private TestHostBuilder _testHostBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// Setup logic for every test.
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_testHostBuilder = new TestHostBuilder();
|
||||
_testHost = _testHostBuilder.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tear down logic for every test.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[TearDown]
|
||||
public async Task TearDown()
|
||||
{
|
||||
await _testHost.StopAsync();
|
||||
_testHost.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the EmailCleanup task.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task EmailCleanup()
|
||||
{
|
||||
// Arrange
|
||||
await InitializeWithTestData();
|
||||
|
||||
// Assert
|
||||
var dbContext = _testHostBuilder.GetDbContext();
|
||||
var emails = await dbContext.Emails.ToListAsync();
|
||||
Assert.That(emails, Has.Count.EqualTo(50));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the LogCleanup task.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task LogCleanup()
|
||||
{
|
||||
// Arrange
|
||||
await InitializeWithTestData();
|
||||
|
||||
// Assert
|
||||
var dbContext = _testHostBuilder.GetDbContext();
|
||||
var generalLogs = await dbContext.Logs.ToListAsync();
|
||||
Assert.That(generalLogs, Has.Count.EqualTo(50), "Only recent general logs should remain");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the LogCleanup task.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task AuthLogCleanup()
|
||||
{
|
||||
// Arrange
|
||||
await InitializeWithTestData();
|
||||
|
||||
// Assert
|
||||
var dbContext = _testHostBuilder.GetDbContext();
|
||||
|
||||
// Check auth logs
|
||||
var authLogs = await dbContext.AuthLogs.ToListAsync();
|
||||
Assert.That(authLogs, Has.Count.EqualTo(50), "Only recent auth logs should remain");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the TaskRunner does not run tasks before the maintenance time.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task MaintenanceTimeInFutureDoesNotRun()
|
||||
{
|
||||
// Seed database with generic test data.
|
||||
await SeedData.SeedDatabase(_testHostBuilder.GetDbContext());
|
||||
|
||||
// Update maintenance time in database to future to ensure the task runner doesn't execute yet.
|
||||
|
||||
// Get current time and set maintenance time to 2 hours in the future
|
||||
var now = DateTime.Now;
|
||||
var futureTime = now.AddHours(2);
|
||||
|
||||
// Make sure we don't exceed midnight
|
||||
if (futureTime.Day != now.Day)
|
||||
{
|
||||
futureTime = new DateTime(now.Year, now.Month, now.Day, 23, 59, 5, DateTimeKind.Local);
|
||||
}
|
||||
|
||||
// Update maintenance time in database
|
||||
var dbContext = _testHostBuilder.GetDbContext();
|
||||
var maintenanceTimeSetting = await dbContext.ServerSettings
|
||||
.FirstAsync(s => s.Key == "MaintenanceTime");
|
||||
maintenanceTimeSetting.Value = futureTime.ToString("HH:mm");
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Get initial email count
|
||||
var initialEmailCount = await dbContext.Emails.CountAsync();
|
||||
|
||||
// Start the service.
|
||||
await _testHost.StartAsync();
|
||||
|
||||
// Verify email count hasn't changed (tasks haven't run)
|
||||
var currentEmailCount = await dbContext.Emails.CountAsync();
|
||||
Assert.That(currentEmailCount, Is.EqualTo(initialEmailCount), "Email count changed despite maintenance time being in the future. Check if TaskRunner is respecting the maintenance time setting.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that the TaskRunner does not run tasks when the current day is excluded.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task MaintenanceTimeExcludedDayDoesNotRun()
|
||||
{
|
||||
// Seed database with generic test data.
|
||||
await SeedData.SeedDatabase(_testHostBuilder.GetDbContext());
|
||||
|
||||
// Get current day of week (1-7, Monday = 1, Sunday = 7)
|
||||
var currentDay = (int)DateTime.Now.DayOfWeek + 1;
|
||||
|
||||
// Update maintenance settings in database to exclude current day
|
||||
var dbContext = _testHostBuilder.GetDbContext();
|
||||
|
||||
// Set maintenance time to midnight
|
||||
var maintenanceTimeSetting = await dbContext.ServerSettings
|
||||
.FirstAsync(s => s.Key == "MaintenanceTime");
|
||||
maintenanceTimeSetting.Value = "00:00";
|
||||
|
||||
// Set task runner days to all days except current day
|
||||
var taskRunnerDays = Enumerable.Range(1, 7)
|
||||
.Where(d => d != currentDay)
|
||||
.ToList();
|
||||
var taskRunnerDaysSetting = await dbContext.ServerSettings
|
||||
.FirstAsync(s => s.Key == "TaskRunnerDays");
|
||||
taskRunnerDaysSetting.Value = string.Join(",", taskRunnerDays);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Get initial email count
|
||||
var initialEmailCount = await dbContext.Emails.CountAsync();
|
||||
|
||||
// Start the service
|
||||
await _testHost.StartAsync();
|
||||
|
||||
// Verify email count hasn't changed (tasks haven't run)
|
||||
var currentEmailCount = await dbContext.Emails.CountAsync();
|
||||
Assert.That(currentEmailCount, Is.EqualTo(initialEmailCount), "Email count changed despite current day being excluded from maintenance days. Check if TaskRunner is respecting the task runner days setting.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test with test data.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
protected async Task InitializeWithTestData()
|
||||
{
|
||||
await SeedData.SeedDatabase(_testHostBuilder.GetDbContext());
|
||||
await _testHost.StartAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TestHostBuilder.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.IntegrationTests.TaskRunner;
|
||||
|
||||
using System.Data.Common;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using AliasVault.TaskRunner;
|
||||
using AliasVault.TaskRunner.Tasks;
|
||||
using AliasVault.TaskRunner.Workers;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Builder class for creating a test host for the TaskRunner in order to run integration tests against it.
|
||||
/// </summary>
|
||||
public class TestHostBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The DbConnection instance that is created for the test.
|
||||
/// </summary>
|
||||
private DbConnection? _dbConnection;
|
||||
|
||||
private AliasServerDbContext? _dbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the DbContext instance for the test.
|
||||
/// </summary>
|
||||
/// <returns>AliasServerDbContext instance.</returns>
|
||||
public AliasServerDbContext GetDbContext()
|
||||
{
|
||||
if (_dbContext == null)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AliasServerDbContext>()
|
||||
.UseSqlite(_dbConnection!)
|
||||
.Options;
|
||||
|
||||
_dbContext = new AliasServerDbContext(options);
|
||||
}
|
||||
|
||||
return _dbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the TaskRunner test host.
|
||||
/// </summary>
|
||||
/// <returns>IHost.</returns>
|
||||
public IHost Build()
|
||||
{
|
||||
// Create a persistent in-memory database for the duration of the test
|
||||
var dbConnection = new SqliteConnection("DataSource=:memory:");
|
||||
dbConnection.Open();
|
||||
_dbConnection = dbConnection;
|
||||
|
||||
var builder = Host.CreateDefaultBuilder()
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton(_dbConnection);
|
||||
|
||||
services.AddDbContextFactory<AliasServerDbContext>((sp, options) =>
|
||||
{
|
||||
var connection = sp.GetRequiredService<DbConnection>();
|
||||
options.UseSqlite(connection);
|
||||
});
|
||||
|
||||
// Add server settings service
|
||||
services.AddSingleton<ServerSettingsService>();
|
||||
|
||||
// Add maintenance tasks
|
||||
services.AddTransient<IMaintenanceTask, LogCleanupTask>();
|
||||
services.AddTransient<IMaintenanceTask, EmailCleanupTask>();
|
||||
services.AddTransient<IMaintenanceTask, EmailQuotaCleanupTask>();
|
||||
|
||||
// Add the TaskRunner worker
|
||||
services.AddHostedService<TaskRunnerWorker>();
|
||||
|
||||
// Ensure the in-memory database is populated with tables
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
{
|
||||
var dbContextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AliasServerDbContext>>();
|
||||
var dbContext = dbContextFactory.CreateDbContext();
|
||||
dbContext.Database.Migrate();
|
||||
}
|
||||
});
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2" />
|
||||
<PackageReference Include="NUnit" Version="4.3.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="4.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user