mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
11
.github/workflows/docker-compose-build.yml
vendored
11
.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=(
|
||||
|
||||
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:
|
||||
|
||||
@@ -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,17 @@ 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
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!release
|
||||
@@ -37,7 +37,7 @@ chmod +x install.sh
|
||||
```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.
|
||||
> **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`
|
||||
|
||||
@@ -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
|
||||
---
|
||||
146
install.sh
146
install.sh
@@ -1,11 +1,12 @@
|
||||
#!/bin/bash
|
||||
# @version 0.8.2
|
||||
# @version 0.9.0
|
||||
|
||||
# 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
|
||||
@@ -37,16 +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 " 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 " 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"
|
||||
@@ -115,6 +117,10 @@ parse_args() {
|
||||
COMMAND="update"
|
||||
shift
|
||||
;;
|
||||
update-installer|cs)
|
||||
COMMAND="update-installer"
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
@@ -192,6 +198,10 @@ main() {
|
||||
"update")
|
||||
handle_update
|
||||
;;
|
||||
"update-installer")
|
||||
check_install_script_update
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -224,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}/blob/${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}/blob/${version_tag}/docker-compose.letsencrypt.yml and place it in the root directory of AliasVault.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
@@ -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
|
||||
@@ -1059,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"
|
||||
@@ -1118,7 +1167,7 @@ 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}/install.sh" -o "install.sh.tmp"; then
|
||||
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
|
||||
@@ -1160,6 +1209,16 @@ check_install_script_update() {
|
||||
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
|
||||
@@ -1206,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; }
|
||||
@@ -1214,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
|
||||
@@ -1226,17 +1289,12 @@ 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}"
|
||||
)
|
||||
|
||||
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>
|
||||
|
||||
99
src/AliasVault.Admin/Main/Pages/Settings/Server.razor
Normal file
99
src/AliasVault.Admin/Main/Pages/Settings/Server.razor
Normal file
@@ -0,0 +1,99 @@
|
||||
@page "/settings/server"
|
||||
@inject ServerSettingsService SettingsService
|
||||
@using AliasVault.Shared.Server.Models
|
||||
@using AliasVault.Shared.Server.Services
|
||||
@inherits MainBase
|
||||
|
||||
<LayoutPageTitle>Server settings</LayoutPageTitle>
|
||||
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="Server settings"
|
||||
Description="Configure AliasVault server settings.">
|
||||
<CustomActions>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<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.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -126,6 +126,11 @@ 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>
|
||||
/// The OnModelCreating method.
|
||||
/// </summary>
|
||||
@@ -237,38 +242,25 @@ 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" +
|
||||
";Journal Mode=WAL" +
|
||||
";Synchronous=Normal" +
|
||||
";Busy Timeout=30000";
|
||||
|
||||
// Set additional PRAGMA settings
|
||||
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 temp_store = MEMORY;
|
||||
PRAGMA mmap_size = 1073741824;";
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
optionsBuilder
|
||||
.UseSqlite(connectionString, options => options.CommandTimeout(60))
|
||||
.UseLazyLoadingProxies();
|
||||
}
|
||||
}
|
||||
|
||||
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,36 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,26 @@ 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.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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
66
src/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs
Normal file
66
src/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <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 AliasVault.Shared.Server.Services;
|
||||
using AliasVault.TaskRunner.Tasks;
|
||||
|
||||
/// <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>
|
||||
public class TaskRunnerWorker(ILogger<TaskRunnerWorker> logger, IEnumerable<IMaintenanceTask> tasks, ServerSettingsService settingsService) : BackgroundService
|
||||
{
|
||||
private DateTime _nextRun = DateTime.MinValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogWarning("TaskRunnerWorker started at: {Time}", DateTimeOffset.Now);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var settings = await settingsService.GetAllSettingsAsync();
|
||||
var now = DateTime.Now;
|
||||
|
||||
// Calculate if we should run now or wait
|
||||
var scheduledTime = settings.MaintenanceTime;
|
||||
var currentTime = TimeOnly.FromDateTime(now);
|
||||
var shouldRunToday = settings.TaskRunnerDays.Contains((int)now.DayOfWeek);
|
||||
var hasPassedScheduledTime = currentTime >= scheduledTime;
|
||||
|
||||
// Run if:
|
||||
// 1. We haven't run yet today (nextRun is from previous day)
|
||||
// 2. It's a scheduled day
|
||||
// 3. The scheduled time has passed
|
||||
if (shouldRunToday && hasPassedScheduledTime && now.Date >= _nextRun.Date)
|
||||
{
|
||||
logger.LogWarning("Starting maintenance tasks at {Time}", now);
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task.ExecuteAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error executing task {TaskName}", task.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// Set next run to tomorrow at the scheduled time
|
||||
_nextRun = now.Date.AddDays(1);
|
||||
logger.LogInformation("Tasks completed. Next run scheduled for date: {NextRun}", _nextRun);
|
||||
}
|
||||
|
||||
// Calculate delay until next check
|
||||
// Check every minute for schedule changes, but not more often than that
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = 2;
|
||||
public const int VersionPatch = 1;
|
||||
|
||||
/// <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 (disabled).
|
||||
/// </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,131 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <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);
|
||||
|
||||
return new ServerSettingsModel
|
||||
{
|
||||
GeneralLogRetentionDays = int.TryParse(settings.GetValueOrDefault("GeneralLogRetentionDays"), out var generalDays) ? generalDays : 30,
|
||||
AuthLogRetentionDays = int.TryParse(settings.GetValueOrDefault("AuthLogRetentionDays"), out var authDays) ? authDays : 90,
|
||||
EmailRetentionDays = int.TryParse(settings.GetValueOrDefault("EmailRetentionDays"), out var emailDays) ? emailDays : 30,
|
||||
MaxEmailsPerUser = int.TryParse(settings.GetValueOrDefault("MaxEmailsPerUser"), out var maxEmails) ? maxEmails : 100,
|
||||
MaintenanceTime = TimeOnly.TryParse(
|
||||
settings.GetValueOrDefault("MaintenanceTime") ?? "00:00",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var time) ? time : new TimeOnly(0, 0),
|
||||
TaskRunnerDays = settings.GetValueOrDefault("TaskRunnerDays")?.Split(',').Select(int.Parse).ToList() ?? new List<int> { 1, 2, 3, 4, 5, 6, 7 },
|
||||
};
|
||||
}
|
||||
|
||||
/// <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));
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,195 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <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;
|
||||
if (currentDay == 0)
|
||||
{
|
||||
currentDay = 7; // Convert Sunday from 0 to 7
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user