mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86d7ee3e9b | ||
|
|
a39ed8c0a7 | ||
|
|
e772e722b5 | ||
|
|
b6bf431062 | ||
|
|
aa41cceff3 | ||
|
|
1baea180aa | ||
|
|
0d8143c62e | ||
|
|
4ae84052e8 | ||
|
|
c73c41ca06 | ||
|
|
5b58418e57 | ||
|
|
7c7f7549c5 | ||
|
|
38203fd767 | ||
|
|
a7b8484a84 | ||
|
|
a091a94737 | ||
|
|
2c299a82b8 | ||
|
|
5ee710750e | ||
|
|
ed5ea31ca8 | ||
|
|
ffdb427184 | ||
|
|
4cef3efa1f | ||
|
|
a5c8908c6b |
17
.github/workflows/docker-compose-build.yml
vendored
17
.github/workflows/docker-compose-build.yml
vendored
@@ -77,8 +77,19 @@ jobs:
|
||||
# Exit with error if any service failed
|
||||
if [ "$failed" = true ]; then
|
||||
# Get container logs
|
||||
echo "Container Logs:"
|
||||
docker compose logs
|
||||
echo "Container Logs admin:"
|
||||
docker compose logs admin
|
||||
echo "Container Logs api:"
|
||||
docker compose logs api
|
||||
echo "Container Logs client:"
|
||||
docker compose logs client
|
||||
echo "Container Logs smtp:"
|
||||
docker compose logs smtp
|
||||
echo "Container Logs reverse-proxy:"
|
||||
docker compose logs reverse-proxy
|
||||
|
||||
# Restart containers for next test in case of failure
|
||||
docker compose restart
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -90,4 +101,4 @@ jobs:
|
||||
echo "Expected: 'New admin password: <at least 8 base64 chars>'"
|
||||
echo "Actual: $output"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -390,6 +390,9 @@ src/Tests/AliasVault.E2ETests/appsettings.Development.json
|
||||
# .env is generated by install.sh and therefore should be ignored
|
||||
.env
|
||||
|
||||
# install.sh backup files are generated by install.sh self-update and therefore should be ignored
|
||||
install.sh.backup
|
||||
|
||||
# Draw.io diagram temp files
|
||||
*.drawio.*
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ permalink: /
|
||||
Open-source password and identity manager with email alias generation and zero-knowledge architecture.
|
||||
{: .fs-6 .fw-300 }
|
||||
|
||||
[Installation](./installation){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
|
||||
[Installation](./installation/install){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 }
|
||||
[View on GitHub](https://github.com/lanedirt/AliasVault){: .btn .fs-5 .mb-4 .mb-md-0 }
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Build from Source
|
||||
parent: Installation Guide
|
||||
parent: Advanced
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
9
docs/installation/advanced/index.md
Normal file
9
docs/installation/advanced/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: default
|
||||
title: Advanced
|
||||
parent: Installation Guide
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Advanced Installation
|
||||
The following guides provide more advanced installation options for AliasVault. These options are not required for the basic installation, but may be useful for advanced users.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: default
|
||||
title: Manual Setup
|
||||
parent: Installation Guide
|
||||
parent: Advanced
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
@@ -4,80 +4,5 @@ title: Installation Guide
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Installation
|
||||
Follow the steps below to install AliasVault on your own server. Minimum experience with Docker and Linux is required.
|
||||
|
||||
{: .toc }
|
||||
* TOC
|
||||
{:toc}
|
||||
|
||||
---
|
||||
|
||||
## 1. Basic Installation
|
||||
To get AliasVault up and running quickly, run the install script to pull pre-built Docker images. The install script will also configure the .env file and start the AliasVault containers. You can get up and running in less than 5 minutes.
|
||||
|
||||
### Hardware requirements
|
||||
- Linux VM with root access (Ubuntu or RHEL based distros recommended)
|
||||
- 1 vCPU
|
||||
- 512MB RAM
|
||||
- 16GB disk space
|
||||
- Docker installed
|
||||
|
||||
### Installation steps
|
||||
1. Download the install script to a directory of your choice. All AliasVault files and directories will be created in this directory.
|
||||
```bash
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/main/install.sh
|
||||
```
|
||||
2. Make the install script executable.
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
3. Run the install script. This will create the .env file, pull the Docker images, and start the AliasVault containers. Follow the on-screen prompts to configure AliasVault.
|
||||
```bash
|
||||
./install.sh install
|
||||
```
|
||||
> **Note**: AliasVault binds to ports 80 and 443 by default. If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file for the `reverse-proxy` (nginx) container. Afterwards re-run the `./install.sh install` command to restart the containers with the new port settings.
|
||||
|
||||
3. After the script completes, you can access AliasVault at:
|
||||
- Client: `https://localhost`
|
||||
- Admin: `https://localhost/admin`
|
||||
|
||||
---
|
||||
|
||||
## 2. SSL configuration
|
||||
The default installation will create a self-signed SSL certificate and configure Nginx to use it.
|
||||
|
||||
You can however also use Let's Encrypt to generate valid SSL certificates and configure Nginx to use it. In order to make this work you will need the following:
|
||||
|
||||
- A public IPv4 address assigned to your server
|
||||
- Port 80 and 443 on your server must be open and accessible from the internet
|
||||
- A registered domain name with an A record pointing to your server's public IP address (e.g. mydomain.com)
|
||||
|
||||
|
||||
### Steps
|
||||
|
||||
1. Run the install script with the `configure-ssl` option
|
||||
```bash
|
||||
./install.sh configure-ssl
|
||||
```
|
||||
2. Follow the prompts to configure Let's Encrypt.
|
||||
|
||||
### Reverting to self-signed SSL
|
||||
If at any point you would like to revert to the self-signed SSL certificate, run the install script again with the `configure-ssl` option
|
||||
and then in the prompt choose option 2.
|
||||
|
||||
---
|
||||
|
||||
## 3. Troubleshooting
|
||||
|
||||
### Resetting the admin password
|
||||
If you have lost your admin password, you can reset it by running the install script with the `reset-password` option. This will generate a new random password and update the .env file with it. After that it will restart the AliasVault containers to apply the changes.
|
||||
```bash
|
||||
./install.sh reset-password
|
||||
```
|
||||
|
||||
### Verbose output
|
||||
If you need more detailed output from the install script, you can run it with the `--verbose` option. This will print more information to the console.
|
||||
```bash
|
||||
./install.sh install --verbose
|
||||
```
|
||||
# Installation Guide
|
||||
The following guide will walk you through the steps to install AliasVault on your own server. Minimum experience with Docker and Linux is required.
|
||||
|
||||
150
docs/installation/install.md
Normal file
150
docs/installation/install.md
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
layout: default
|
||||
title: Basic Install
|
||||
parent: Installation Guide
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Basic Install
|
||||
The following guide will walk you through the steps to install AliasVault on your own server. Minimum experience with Docker and Linux is required.
|
||||
|
||||
{: .toc }
|
||||
* TOC
|
||||
{:toc}
|
||||
|
||||
---
|
||||
|
||||
## 1. Basic Installation
|
||||
To get AliasVault up and running quickly, run the install script to pull pre-built Docker images. The install script will also configure the .env file and start the AliasVault containers. You can get up and running in less than 5 minutes.
|
||||
|
||||
### Hardware requirements
|
||||
- Linux VM with root access (Ubuntu or RHEL based distros recommended)
|
||||
- 1 vCPU
|
||||
- 512MB RAM
|
||||
- 16GB disk space
|
||||
- Docker installed
|
||||
|
||||
### Installation steps
|
||||
1. Download the install script to a directory of your choice. All AliasVault files and directories will be created in this directory.
|
||||
```bash
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/main/install.sh
|
||||
```
|
||||
2. Make the install script executable.
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
3. Run the install script. This will create the .env file, pull the Docker images, and start the AliasVault containers. Follow the on-screen prompts to configure AliasVault.
|
||||
```bash
|
||||
./install.sh install
|
||||
```
|
||||
> **Note**: AliasVault binds to ports 80 and 443 by default. If you want to change the default AliasVault ports you can do so in the `docker-compose.yml` file for the `reverse-proxy` (nginx) container. Afterwards re-run the `./install.sh install` command to restart the containers with the new port settings.
|
||||
|
||||
3. After the script completes, you can access AliasVault at:
|
||||
- Client: `https://localhost`
|
||||
- Admin: `https://localhost/admin`
|
||||
|
||||
---
|
||||
|
||||
## 2. SSL configuration
|
||||
The default installation will create a self-signed SSL certificate and configure Nginx to use it.
|
||||
|
||||
You can however also use Let's Encrypt to generate valid SSL certificates and configure Nginx to use it. In order to make this work you will need the following:
|
||||
|
||||
- A public IPv4 address assigned to your server
|
||||
- Port 80 and 443 on your server must be open and accessible from the internet
|
||||
- A registered domain name with an A record pointing to your server's public IP address (e.g. mydomain.com)
|
||||
|
||||
### Steps
|
||||
|
||||
1. Run the install script with the `configure-ssl` option
|
||||
```bash
|
||||
./install.sh configure-ssl
|
||||
```
|
||||
2. Follow the prompts to configure Let's Encrypt.
|
||||
|
||||
### Reverting to self-signed SSL
|
||||
If at any point you would like to revert to the self-signed SSL certificate, run the install script again with the `configure-ssl` option
|
||||
and then in the prompt choose option 2.
|
||||
|
||||
---
|
||||
|
||||
## 3. Email Server Setup
|
||||
|
||||
AliasVault includes a built-in email server that can handle multiple custom domains for your aliases.
|
||||
|
||||
To set up the email server, you need the following:
|
||||
- Public IPv4 address
|
||||
- Open ports (25 and 587) in server firewall for SMTP traffic
|
||||
- Access to DNS record management for your domain
|
||||
|
||||
### a) DNS Configuration
|
||||
Configure the following DNS records for your domain:
|
||||
|
||||
| Name | Type | Priority | Content | TTL |
|
||||
|------|------|----------|---------------------------|-----|
|
||||
| mail | A | | `<your-server-public-ip>` | 3600 |
|
||||
| @ | MX | 10 | `mail.<your-domain>` | 3600 |
|
||||
|
||||
> Note: Replace `<your-server-public-ip>` and `<your-domain>` with your actual values.
|
||||
|
||||
### b) Port Configuration
|
||||
The email server requires the following ports to be open:
|
||||
- Port 25: Standard SMTP (unencrypted)
|
||||
- Port 587: SMTP with STARTTLS (encrypted)
|
||||
|
||||
#### Verifying Port Access
|
||||
You can test if the SMTP ports are correctly configured using telnet:
|
||||
|
||||
```bash
|
||||
# Test standard SMTP port
|
||||
telnet <your-server-public-ip> 25
|
||||
|
||||
# Test secure SMTP port
|
||||
telnet <your-server-public-ip> 587
|
||||
```
|
||||
|
||||
If successful, you'll see a connection establishment message. Press Ctrl+C to exit the telnet session.
|
||||
|
||||
### c) Setting Up Email Domains
|
||||
|
||||
1. Run the email configuration script:
|
||||
```bash
|
||||
./install.sh configure-email
|
||||
````
|
||||
2. Follow the interactive prompts to:
|
||||
- Configure your domain(s)
|
||||
- Restart required services
|
||||
|
||||
3. Once configured, you can:
|
||||
- Create new aliases in the AliasVault client
|
||||
- Use your custom domain(s) for email addresses
|
||||
- Note: you can configure the default domain for new aliases in the AliasVault client in Menu > Settings > Email Settings > Default Email Domain
|
||||
- Start receiving emails on your aliases
|
||||
|
||||
{: .note }
|
||||
Important: DNS propagation can take up to 24-48 hours. During this time, email delivery might be inconsistent.
|
||||
|
||||
If you encounter any issues, feel free to open an issue on the [GitHub repository](https://github.com/lanedirt/AliasVault/issues).
|
||||
|
||||
---
|
||||
|
||||
## 4. Troubleshooting
|
||||
|
||||
### Resetting the admin password
|
||||
If you have lost your admin password, you can reset it by running the install script with the `reset-password` option. This will generate a new random password and update the .env file with it. After that it will restart the AliasVault containers to apply the changes.
|
||||
```bash
|
||||
./install.sh reset-password
|
||||
```
|
||||
|
||||
### Verbose output
|
||||
If you need more detailed output from the install script, you can run it with the `--verbose` option. This will print more information to the console.
|
||||
```bash
|
||||
./install.sh install --verbose
|
||||
```
|
||||
|
||||
### No emails being received
|
||||
If you are not receiving emails on your aliases, check the following:
|
||||
- Verify DNS records are correctly configured
|
||||
- Ensure ports 25 and 587 are accessible
|
||||
- Check your server's firewall settings
|
||||
- Verify that your ISP/hosting provider allows SMTP traffic
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Start/stop
|
||||
parent: Installation Guide
|
||||
nav_order: 3
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Starting and stopping AliasVault
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Uninstall
|
||||
parent: Installation Guide
|
||||
nav_order: 5
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
# Uninstall
|
||||
@@ -16,4 +16,4 @@ This will not delete any data stored in the database. If you wish to delete all
|
||||
1. Run the install script with the `uninstall` option
|
||||
```bash
|
||||
./install.sh uninstall
|
||||
```
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Update
|
||||
parent: Installation Guide
|
||||
nav_order: 4
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# Updating AliasVault
|
||||
|
||||
292
install.sh
292
install.sh
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# @version 0.8.2
|
||||
|
||||
# Repository information used for downloading files and images from GitHub
|
||||
REPO_OWNER="lanedirt"
|
||||
@@ -22,7 +23,6 @@ REQUIRED_DIRS=(
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
@@ -41,6 +41,7 @@ show_usage() {
|
||||
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"
|
||||
@@ -94,6 +95,10 @@ parse_args() {
|
||||
COMMAND="configure-ssl"
|
||||
shift
|
||||
;;
|
||||
configure-email|email)
|
||||
COMMAND="configure-email"
|
||||
shift
|
||||
;;
|
||||
start|s)
|
||||
COMMAND="start"
|
||||
shift
|
||||
@@ -172,6 +177,9 @@ main() {
|
||||
"configure-ssl")
|
||||
handle_ssl_configuration
|
||||
;;
|
||||
"configure-email")
|
||||
handle_email_configuration
|
||||
;;
|
||||
"start")
|
||||
handle_start
|
||||
;;
|
||||
@@ -258,8 +266,8 @@ print_logo() {
|
||||
printf " _ _ _ __ __ _ _ \n"
|
||||
printf " / \ | (_) __ _ ___ \ \ / /_ _ _ _| | |_\n"
|
||||
printf " / _ \ | | |/ _\` / __| \ \/\/ / _\` | | | | | __|\n"
|
||||
printf " / ___ \| | | (_| \__ \ \ / (_| | |_| | | |_ \n"
|
||||
printf "/_/ \_\_|_|\__,_|___/ \/ \__,_|\__,_|_|\__|\n"
|
||||
printf " / ___ \| | | (_| \__ \ \ / / (_| | |_| | | |_ \n"
|
||||
printf "/_/ \_\_|_|\__,_|___/ \/ \__,__|\__,_|_|\__|\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
@@ -316,22 +324,14 @@ populate_data_protection_cert_pass() {
|
||||
set_private_email_domains() {
|
||||
printf "${CYAN}> Checking PRIVATE_EMAIL_DOMAINS...${NC}\n"
|
||||
if ! grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "Please enter the domains that should be allowed to receive email, separated by commas (press Enter to disable email support): "
|
||||
read -r private_email_domains
|
||||
update_env_var "PRIVATE_EMAIL_DOMAINS" "DISABLED.TLD"
|
||||
fi
|
||||
|
||||
private_email_domains=${private_email_domains:-"DISABLED.TLD"}
|
||||
update_env_var "PRIVATE_EMAIL_DOMAINS" "$private_email_domains"
|
||||
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${RED}SMTP is disabled.${NC}\n"
|
||||
fi
|
||||
private_email_domains=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${RED}Email server is disabled.${NC} To enable use ./install.sh configure-email command.\n"
|
||||
else
|
||||
private_email_domains=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists.${NC} ${RED}Private email domains are disabled.${NC}\n"
|
||||
else
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists.${NC}\n"
|
||||
fi
|
||||
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists. Email server is enabled.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -743,6 +743,147 @@ handle_ssl_configuration() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to handle email server configuration
|
||||
# Function to handle email server configuration
|
||||
handle_email_configuration() {
|
||||
# Setup trap for Ctrl+C and other interrupts
|
||||
trap 'printf "\n${YELLOW}Configuration cancelled by user.${NC}\n"; exit 1' INT TERM
|
||||
|
||||
printf "${YELLOW}+++ Email Server Configuration +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Check if AliasVault is installed
|
||||
if [ ! -f "docker-compose.yml" ]; then
|
||||
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current email domains from .env
|
||||
CURRENT_DOMAINS=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
|
||||
printf "${CYAN}About Email Server:${NC}\n"
|
||||
printf "AliasVault includes a built-in email server for handling virtual email addresses.\n"
|
||||
printf "When enabled, it can receive emails for one or more configured domains.\n"
|
||||
printf "Each domain must have an MX record in DNS configuration pointing to this server's hostname.\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}Current Configuration:${NC}\n"
|
||||
|
||||
if [ "$CURRENT_DOMAINS" = "DISABLED.TLD" ]; then
|
||||
printf "Email Server Status: ${RED}Disabled${NC}\n"
|
||||
else
|
||||
printf "Email Server Status: ${GREEN}Enabled${NC}\n"
|
||||
printf "Active Domains: ${CYAN}${CURRENT_DOMAINS}${NC}\n"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
printf "Email Server Options:\n"
|
||||
printf "1) Enable email server / Update domains\n"
|
||||
printf "2) Disable email server\n"
|
||||
printf "3) Cancel\n"
|
||||
printf "\n"
|
||||
|
||||
read -p "Select an option [1-3]: " email_option
|
||||
|
||||
case $email_option in
|
||||
1)
|
||||
while true; do
|
||||
printf "\n${CYAN}Enter domain(s) for email server${NC}\n"
|
||||
printf "For multiple domains, separate with commas (e.g. domain1.com,domain2.com)\n"
|
||||
printf "IMPORTANT: Each domain must have an MX record in DNS pointing to this server.\n"
|
||||
read -p "Domains: " new_domains
|
||||
|
||||
if [ -z "$new_domains" ]; then
|
||||
printf "${RED}Error: Domains cannot be empty${NC}\n"
|
||||
continue
|
||||
fi
|
||||
|
||||
printf "\n${CYAN}You entered the following domains:${NC}\n"
|
||||
IFS=',' read -ra DOMAIN_ARRAY <<< "$new_domains"
|
||||
for domain in "${DOMAIN_ARRAY[@]}"; do
|
||||
printf " - ${GREEN}${domain}${NC}\n"
|
||||
done
|
||||
printf "\n"
|
||||
|
||||
read -p "Are these domains correct? (y/n): " confirm
|
||||
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
|
||||
read -p "Continue with restart? (y/n): " restart_confirm
|
||||
|
||||
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
|
||||
printf "${YELLOW}Configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Update .env file and restart
|
||||
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "$new_domains"; then
|
||||
printf "${RED}Failed to update configuration.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${GREEN}Email server configuration updated${NC}\n"
|
||||
printf "Restarting AliasVault services...\n"
|
||||
|
||||
if ! handle_restart; then
|
||||
printf "${RED}Failed to restart services.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only show next steps if everything succeeded
|
||||
printf "\n${CYAN}The email server is now succesfully configured.${NC}\n"
|
||||
printf "\n"
|
||||
printf "To test the email server:\n"
|
||||
printf " a. Log in to your AliasVault account\n"
|
||||
printf " b. Create a new alias using one of your configured private domains\n"
|
||||
printf " c. Send a test email from an external email service (e.g., Gmail)\n"
|
||||
printf " d. Check if the email appears in your AliasVault inbox\n"
|
||||
printf "\n"
|
||||
printf "If emails don't arrive, please verify:\n"
|
||||
printf " > DNS MX records are correctly configured\n"
|
||||
printf " > Your server's firewall allows incoming traffic on port 25 and 587\n"
|
||||
printf " > Your ISP/hosting provider doesn't block SMTP traffic\n"
|
||||
printf "\n"
|
||||
;;
|
||||
2)
|
||||
printf "${YELLOW}Warning: Docker containers need to be restarted after disabling the email server.${NC}\n"
|
||||
read -p "Continue with disable and restart? (y/n): " disable_confirm
|
||||
|
||||
if [ "$disable_confirm" != "y" ] && [ "$disable_confirm" != "Y" ]; then
|
||||
printf "${YELLOW}Configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Disable email server
|
||||
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "DISABLED.TLD"; then
|
||||
printf "${RED}Failed to update configuration.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${YELLOW}Email server disabled${NC}\n"
|
||||
printf "Restarting AliasVault services...\n"
|
||||
|
||||
if ! handle_restart; then
|
||||
printf "${RED}Failed to restart services.${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
printf "${YELLOW}Email configuration cancelled.${NC}\n"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf "${RED}Invalid option selected.${NC}\n"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Remove the trap before normal exit
|
||||
trap - INT TERM
|
||||
}
|
||||
|
||||
# Function to configure Let's Encrypt
|
||||
configure_letsencrypt() {
|
||||
printf "${CYAN}> Configuring Let's Encrypt SSL certificate...${NC}\n"
|
||||
@@ -891,6 +1032,9 @@ handle_update() {
|
||||
printf "${YELLOW}+++ Checking for AliasVault updates +++${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# First check for install.sh updates
|
||||
check_install_script_update || true
|
||||
|
||||
# Check current version
|
||||
if ! grep -q "^ALIASVAULT_VERSION=" "$ENV_FILE"; then
|
||||
printf "${YELLOW}> No version information found. Running first-time update check...${NC}\n"
|
||||
@@ -906,8 +1050,8 @@ handle_update() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "${CYAN}> Current version: ${current_version}${NC}\n"
|
||||
printf "${CYAN}> Latest version: ${latest_version}${NC}\n"
|
||||
printf "${CYAN}> Current AliasVault version: ${current_version}${NC}\n"
|
||||
printf "${CYAN}> Latest AliasVault version: ${latest_version}${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
if [ "$current_version" = "$latest_version" ]; then
|
||||
@@ -934,6 +1078,116 @@ handle_update() {
|
||||
printf "${GREEN}> Update completed successfully!${NC}\n"
|
||||
}
|
||||
|
||||
# Function to extract version
|
||||
extract_version() {
|
||||
local file="$1"
|
||||
local version=$(head -n 2 "$file" | grep '@version' | cut -d' ' -f3)
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
# Function to compare semantic versions
|
||||
compare_versions() {
|
||||
local version1="$1"
|
||||
local version2="$2"
|
||||
|
||||
# Split versions into arrays
|
||||
IFS='.' read -ra v1_parts <<< "$version1"
|
||||
IFS='.' read -ra v2_parts <<< "$version2"
|
||||
|
||||
# Compare each part numerically
|
||||
for i in {0..2}; do
|
||||
# Default to 0 if part doesn't exist
|
||||
local v1_part=${v1_parts[$i]:-0}
|
||||
local v2_part=${v2_parts[$i]:-0}
|
||||
|
||||
# Compare numerically
|
||||
if [ "$v1_part" -gt "$v2_part" ]; then
|
||||
echo "1" # version1 is greater
|
||||
return
|
||||
elif [ "$v1_part" -lt "$v2_part" ]; then
|
||||
echo "-1" # version1 is lesser
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
echo "0" # versions are equal
|
||||
}
|
||||
|
||||
# Function to check if install.sh needs updating
|
||||
check_install_script_update() {
|
||||
printf "${CYAN}> Checking for install script updates...${NC}\n"
|
||||
|
||||
# Download latest install.sh to temporary file
|
||||
if ! curl -sSf "${GITHUB_RAW_URL}/install.sh" -o "install.sh.tmp"; then
|
||||
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get versions
|
||||
local current_version=$(extract_version "install.sh")
|
||||
local new_version=$(extract_version "install.sh.tmp")
|
||||
|
||||
# Check if versions could be extracted
|
||||
if [ -z "$current_version" ] || [ -z "$new_version" ]; then
|
||||
printf "${YELLOW}> Could not determine script versions. Falling back to file comparison...${NC}\n"
|
||||
# Fall back to file comparison
|
||||
if ! cmp -s "install.sh" "install.sh.tmp"; then
|
||||
printf "${YELLOW}> Changes detected in install script.${NC}\n"
|
||||
else
|
||||
printf "${GREEN}> Install script is up to date.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
printf "${CYAN}> Current install script version: ${current_version}${NC}\n"
|
||||
printf "${CYAN}> Latest install script version: ${new_version}${NC}\n"
|
||||
|
||||
# Compare versions using semver comparison
|
||||
if [ "$current_version" = "$new_version" ]; then
|
||||
printf "${GREEN}> Install script is up to date.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
else
|
||||
local compare_result=$(compare_versions "$current_version" "$new_version")
|
||||
|
||||
if [ "$compare_result" -ge "0" ]; then
|
||||
printf "${GREEN}> Install script is up to date.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If we get here, an update is available
|
||||
printf "${YELLOW}> A new version of the install script is available.${NC}\n"
|
||||
printf "Would you like to update the install script before proceeding? [Y/n]: "
|
||||
read -r reply
|
||||
|
||||
if [[ ! $reply =~ ^[Nn]$ ]]; then
|
||||
# Create backup of current script
|
||||
cp "install.sh" "install.sh.backup"
|
||||
|
||||
if mv "install.sh.tmp" "install.sh"; then
|
||||
chmod +x "install.sh"
|
||||
printf "${GREEN}> Install script updated successfully.${NC}\n"
|
||||
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
|
||||
printf "${YELLOW}> Please run the update command again to continue with the update process.${NC}\n"
|
||||
exit 0
|
||||
else
|
||||
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
|
||||
# Restore from backup if update failed
|
||||
mv "install.sh.backup" "install.sh"
|
||||
rm -f install.sh.tmp
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
printf "${YELLOW}> Continuing with current install script version.${NC}\n"
|
||||
rm -f install.sh.tmp
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to perform the actual installation with specific version
|
||||
handle_install_version() {
|
||||
local target_version="$1"
|
||||
|
||||
@@ -262,16 +262,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
return Unauthorized("User not found (name-2)");
|
||||
}
|
||||
|
||||
// Check if the refresh token is valid.
|
||||
var existingToken = await context.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(t => t.UserId == user.Id && t.Value == tokenModel.RefreshToken);
|
||||
if (existingToken == null || existingToken.ExpireDate < timeProvider.UtcNow)
|
||||
// Generate new tokens for the user.
|
||||
var token = await GenerateNewTokensForUser(user, tokenModel.RefreshToken);
|
||||
if (token == null)
|
||||
{
|
||||
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TokenRefresh, AuthFailureReason.InvalidRefreshToken);
|
||||
return Unauthorized("Refresh token expired");
|
||||
return Unauthorized("Invalid refresh token");
|
||||
}
|
||||
|
||||
// Generate new tokens for the user.
|
||||
var token = await GenerateNewTokensForUser(user, existingToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
await authLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.TokenRefresh);
|
||||
@@ -345,7 +343,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
UserName = model.Username,
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
PasswordChangedAt = DateTime.UtcNow,
|
||||
PasswordChangedAt = timeProvider.UtcNow,
|
||||
};
|
||||
|
||||
user.Vaults.Add(new AliasServerDb.Vault
|
||||
@@ -459,6 +457,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
private static (bool IsValid, string ErrorMessage) ValidateUsername(string username)
|
||||
{
|
||||
const int minimumUsernameLength = 3;
|
||||
const int maximumUsernameLength = 40;
|
||||
const string adminUsername = "admin";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
@@ -468,7 +467,12 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
|
||||
if (username.Length < minimumUsernameLength)
|
||||
{
|
||||
return (false, $"Username must be at least {minimumUsernameLength} characters long.");
|
||||
return (false, $"Username too short: must be at least {minimumUsernameLength} characters long.");
|
||||
}
|
||||
|
||||
if (username.Length > maximumUsernameLength)
|
||||
{
|
||||
return (false, $"Username too long: cannot be longer than {maximumUsernameLength} characters.");
|
||||
}
|
||||
|
||||
if (string.Equals(username, adminUsername, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -678,9 +682,9 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
/// to the database.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to generate the tokens for.</param>
|
||||
/// <param name="existingToken">The existing token that is being replaced (optional).</param>
|
||||
/// <returns>TokenModel which includes new access and refresh token.</returns>
|
||||
private async Task<TokenModel> GenerateNewTokensForUser(AliasVaultUser user, AliasVaultUserRefreshToken existingToken)
|
||||
/// <param name="existingTokenValue">The existing token value that is being replaced (optional).</param>
|
||||
/// <returns>TokenModel which includes new access and refresh token. Returns null if provided refresh token is invalid.</returns>
|
||||
private async Task<TokenModel?> GenerateNewTokensForUser(AliasVaultUser user, string existingTokenValue)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
await Semaphore.WaitAsync();
|
||||
@@ -693,7 +697,7 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
var existingTokenReuseWindow = timeProvider.UtcNow.AddSeconds(-30);
|
||||
var existingTokenReuse = await context.AliasVaultUserRefreshTokens
|
||||
.FirstOrDefaultAsync(t => t.UserId == user.Id &&
|
||||
t.PreviousTokenValue == existingToken.Value &&
|
||||
t.PreviousTokenValue == existingTokenValue &&
|
||||
t.CreatedAt > existingTokenReuseWindow);
|
||||
|
||||
if (existingTokenReuse is not null)
|
||||
@@ -704,15 +708,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
|
||||
return new TokenModel { Token = accessToken, RefreshToken = existingTokenReuse.Value };
|
||||
}
|
||||
|
||||
// Remove the existing refresh token.
|
||||
var tokenToDelete = await context.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(t => t.Id == existingToken.Id);
|
||||
if (tokenToDelete is null)
|
||||
// Check if the refresh token still exists and is not expired.
|
||||
var existingToken = await context.AliasVaultUserRefreshTokens.FirstOrDefaultAsync(t => t.UserId == user.Id && t.Value == existingTokenValue);
|
||||
if (existingToken == null || existingToken.ExpireDate < timeProvider.UtcNow)
|
||||
{
|
||||
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TokenRefresh, AuthFailureReason.InvalidRefreshToken);
|
||||
throw new InvalidOperationException("Refresh token does not exist (anymore).");
|
||||
return null;
|
||||
}
|
||||
|
||||
context.AliasVaultUserRefreshTokens.Remove(tokenToDelete);
|
||||
context.AliasVaultUserRefreshTokens.Remove(existingToken);
|
||||
|
||||
// New refresh token lifetime is the same as the existing one.
|
||||
var existingTokenLifetime = existingToken.ExpireDate - existingToken.CreatedAt;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Email</h3>
|
||||
</div>
|
||||
<div class="flex justify-end items-center space-x-2">
|
||||
@if (RefreshTimer is not null)
|
||||
@if (DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
<div class="w-3 h-3 mr-2 rounded-full bg-primary-300 border-2 border-primary-100 animate-pulse" title="Auto-refresh enabled"></div>
|
||||
}
|
||||
@@ -56,27 +56,27 @@
|
||||
<div class="overflow-hidden shadow sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Subject
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Date & Time
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Subject
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Date & Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
@foreach (var mail in MailboxEmails)
|
||||
{
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</span>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var mail in MailboxEmails)
|
||||
{
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</span>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
<span class="cursor-pointer" @onclick="() => OpenEmail(mail.Id)">@mail.DateSystem</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -99,13 +99,56 @@
|
||||
private EmailApiModel Email { get; set; } = new();
|
||||
private bool EmailModalVisible { get; set; }
|
||||
private string Error { get; set; } = string.Empty;
|
||||
private Timer? RefreshTimer { get; set; }
|
||||
|
||||
private bool IsRefreshing { get; set; } = true;
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
private bool IsSpamOk { get; set; } = false;
|
||||
|
||||
private bool IsPageVisible { get; set; } = true;
|
||||
private CancellationTokenSource? PollingCancellationTokenSource { get; set; }
|
||||
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
|
||||
private readonly SemaphoreSlim RefreshSemaphore = new(1, 1);
|
||||
private DateTime LastRefreshTime = DateTime.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by JavaScript when the page visibility changes.
|
||||
/// </summary>
|
||||
/// <param name="isVisible">Boolean whether the page is visible or not.</param>
|
||||
/// <returns>Task.</returns>
|
||||
[JSInvokable]
|
||||
public async Task OnVisibilityChange(bool isVisible)
|
||||
{
|
||||
IsPageVisible = isVisible;
|
||||
if (isVisible)
|
||||
{
|
||||
// Only enable auto-refresh if the setting is enabled.
|
||||
if (DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
await StartPolling();
|
||||
}
|
||||
|
||||
// Refresh immediately when tab becomes visible
|
||||
await ManualRefresh();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cancel polling.
|
||||
if (PollingCancellationTokenSource is not null)
|
||||
{
|
||||
await PollingCancellationTokenSource.CancelAsync();
|
||||
}
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
PollingCancellationTokenSource?.Cancel();
|
||||
PollingCancellationTokenSource?.Dispose();
|
||||
RefreshSemaphore.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -124,12 +167,29 @@
|
||||
}
|
||||
IsSpamOk = IsSpamOkDomain(EmailAddress);
|
||||
|
||||
// Set up visibility change detection
|
||||
await JsInteropService.RegisterVisibilityCallback(DotNetObjectReference.Create(this));
|
||||
|
||||
// Only enable auto-refresh if the setting is enabled.
|
||||
if (DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
RefreshTimer = new Timer(2000);
|
||||
RefreshTimer.Elapsed += async (sender, e) => await TimerRefresh();
|
||||
RefreshTimer.Start();
|
||||
await StartPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (!ShowComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await ManualRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,25 +206,62 @@
|
||||
IsSpamOk = IsSpamOkDomain(EmailAddress);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// Start the polling for new emails.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task StartPolling()
|
||||
{
|
||||
RefreshTimer?.Dispose();
|
||||
if (PollingCancellationTokenSource is not null)
|
||||
{
|
||||
await PollingCancellationTokenSource.CancelAsync();
|
||||
}
|
||||
|
||||
PollingCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
while (!PollingCancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
if (IsPageVisible)
|
||||
{
|
||||
// Only auto refresh when the tab is visible.
|
||||
await RefreshWithThrottling();
|
||||
await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, PollingCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal cancellation, ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
/// <summary>
|
||||
/// Refresh the emails with throttling to prevent multiple refreshes at the same time.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task RefreshWithThrottling()
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (!ShowComponent)
|
||||
if (!await RefreshSemaphore.WaitAsync(0)) // Don't wait if a refresh is in progress
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender)
|
||||
try
|
||||
{
|
||||
await ManualRefresh();
|
||||
var timeSinceLastRefresh = DateTime.UtcNow - LastRefreshTime;
|
||||
if (timeSinceLastRefresh.TotalMilliseconds < ACTIVE_TAB_REFRESH_INTERVAL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadRecentEmailsAsync();
|
||||
LastRefreshTime = DateTime.UtcNow;
|
||||
}
|
||||
finally
|
||||
{
|
||||
RefreshSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,15 +281,10 @@
|
||||
return Config.PrivateEmailDomains.Exists(x => email.EndsWith(x));
|
||||
}
|
||||
|
||||
private async Task TimerRefresh()
|
||||
{
|
||||
IsRefreshing = true;
|
||||
StateHasChanged();
|
||||
await LoadRecentEmailsAsync();
|
||||
IsRefreshing = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manually refresh the emails.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task ManualRefresh()
|
||||
{
|
||||
IsLoading = true;
|
||||
@@ -202,6 +294,10 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// (Re)load recent emails by making an API call to the server.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task LoadRecentEmailsAsync()
|
||||
{
|
||||
if (!ShowComponent || EmailAddress is null)
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace AliasVault.Client.Services;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
@@ -237,6 +238,16 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
|
||||
await jsRuntime.InvokeVoidAsync("window.scrollTo", 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a visibility callback which is invoked when the visibility of component changes in client.
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">Component type.</typeparam>
|
||||
/// <param name="objRef">DotNetObjectReference.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task RegisterVisibilityCallback<TComponent>(DotNetObjectReference<TComponent> objRef)
|
||||
where TComponent : class =>
|
||||
await jsRuntime.InvokeVoidAsync("window.registerVisibilityCallback", objRef);
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a WebAuthn get credential operation.
|
||||
/// </summary>
|
||||
|
||||
@@ -298,3 +298,9 @@ async function createWebAuthnCredentialAndDeriveKey(username) {
|
||||
return { Error: "WEBAUTHN_CREATE_ERROR", Message: createError.message };
|
||||
}
|
||||
}
|
||||
|
||||
window.registerVisibilityCallback = function (dotnetHelper) {
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
dotnetHelper.invokeMethodAsync('OnVisibilityChange', !document.hidden);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ public static class AppInfo
|
||||
/// <summary>
|
||||
/// Gets the patch version number.
|
||||
/// </summary>
|
||||
public const int VersionPatch = 1;
|
||||
public const int VersionPatch = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the build number, typically used in CI/CD pipelines.
|
||||
|
||||
@@ -29,7 +29,7 @@ public abstract class PlaywrightTest
|
||||
/// <summary>
|
||||
/// Gets or sets random unique account email that is used for the test.
|
||||
/// </summary>
|
||||
protected virtual string TestUserUsername { get; set; } = $"{Guid.NewGuid()}@test.com";
|
||||
protected virtual string TestUserUsername { get; set; } = $"{Guid.NewGuid().ToString()[..10]}@test.com";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets random unique account password that is used for the test.
|
||||
@@ -201,7 +201,7 @@ public abstract class PlaywrightTest
|
||||
/// </summary>
|
||||
protected void SetRandomTestUserCredentials()
|
||||
{
|
||||
TestUserUsername = $"{Guid.NewGuid()}@test.com";
|
||||
TestUserUsername = $"{Guid.NewGuid().ToString()[..10]}@test.com";
|
||||
TestUserPassword = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
|
||||
@@ -116,4 +116,55 @@ public class UserSetupTests : ClientPlaywrightTest
|
||||
var errorMessage = await WaitForAndGetElement("text='Username is already in use.'");
|
||||
Assert.That(errorMessage, Is.Not.Null, "The 'Username is already in use' error message should appear.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test if the "Username too short" and "Username too long" error appears when trying to register with an invalid username.
|
||||
/// </summary>
|
||||
/// <returns>Async task.</returns>
|
||||
[Test]
|
||||
[Order(3)]
|
||||
public async Task UserSetupUsernameLengthTest()
|
||||
{
|
||||
// Logout.
|
||||
await Logout();
|
||||
await Page.GotoAsync(AppBaseUrl);
|
||||
await WaitForUrlAsync("user/start", "Create new vault");
|
||||
|
||||
// Click the "Create new vault" anchor tag.
|
||||
var createVaultButton = await WaitForAndGetElement("a:has-text('Create new vault')");
|
||||
await createVaultButton.ClickAsync();
|
||||
|
||||
// Wait for the terms and conditions to load.
|
||||
await WaitForUrlAsync("user/setup", "Terms and Conditions");
|
||||
|
||||
// Accept the terms and conditions.
|
||||
var acceptTermsCheckbox = await WaitForAndGetElement("input[id='agreeTerms']");
|
||||
await acceptTermsCheckbox.CheckAsync();
|
||||
|
||||
// Wait for the continue button to be enabled.
|
||||
await Task.Delay(100);
|
||||
|
||||
// Press the continue button.
|
||||
var continueButton = await WaitForAndGetElement("button:has-text('Continue')");
|
||||
await continueButton.ClickAsync();
|
||||
|
||||
// Wait for the username step to load.
|
||||
await WaitForUrlAsync("user/setup", "Username");
|
||||
var usernameField = await WaitForAndGetElement("input[id='username']");
|
||||
await usernameField.FillAsync("ts"); // Too short username (2 chars)
|
||||
|
||||
// Check if the "Username is too short" error message appears
|
||||
var errorMessage = await WaitForAndGetElement("text='Username too short: must be at least 3 characters long.'");
|
||||
Assert.That(errorMessage, Is.Not.Null, "The 'Username too short' error message should appear.");
|
||||
|
||||
// Clear the username field.
|
||||
await usernameField.FillAsync(string.Empty);
|
||||
|
||||
// Fill in a too long username (41 chars).
|
||||
await usernameField.FillAsync("asdasdasdasdasdasdasdasdasdaaaasasddsdasd"); // Too long username (41 chars)
|
||||
|
||||
// Check if the "Username is too short" error message appears
|
||||
errorMessage = await WaitForAndGetElement("text='Username too long: cannot be longer than 40 characters.'");
|
||||
Assert.That(errorMessage, Is.Not.Null, "The 'Username too long' error message should appear.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,28 +12,6 @@ namespace AliasVault.Tests.Utilities;
|
||||
/// </summary>
|
||||
public class FaviconExtractorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Test extracting a favicon from a known website.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task ExtractFaviconSpamOk()
|
||||
{
|
||||
var faviconBytes = await FaviconExtractor.FaviconExtractor.GetFaviconAsync("https://spamok.com");
|
||||
Assert.That(faviconBytes, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test extracting a favicon from a known website.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
[Test]
|
||||
public async Task ExtractFaviconDumpert()
|
||||
{
|
||||
var faviconBytes = await FaviconExtractor.FaviconExtractor.GetFaviconAsync("https://www.dumpert.nl");
|
||||
Assert.That(faviconBytes, Is.Not.Null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test extracting a favicon from a known website.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user