mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-02 11:09:42 -05:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f0104e8f9 | ||
|
|
ea37c4d8c6 | ||
|
|
95be4beb13 | ||
|
|
716ef0b30c | ||
|
|
fc0eb0e7e7 | ||
|
|
9670178aec | ||
|
|
8503be4d52 | ||
|
|
9eadcaa2ed | ||
|
|
e0ed8fd285 | ||
|
|
61748c3d03 | ||
|
|
faff4844f5 | ||
|
|
09d931484a | ||
|
|
1678595c13 | ||
|
|
8945b33705 | ||
|
|
4ee044ffb9 | ||
|
|
5443e147b1 | ||
|
|
05edda8b48 | ||
|
|
179bb62604 | ||
|
|
1f5863b066 | ||
|
|
ef36a08ef4 | ||
|
|
4f7212668e | ||
|
|
41bb7ed701 | ||
|
|
78286b1ac1 | ||
|
|
7bc8bb3fc2 | ||
|
|
c576062025 | ||
|
|
1194d54e6f | ||
|
|
e782a6a51f | ||
|
|
2071a7c4fe | ||
|
|
8c1e5a7bf8 | ||
|
|
b8f9e7fa2c | ||
|
|
a0a541aff9 | ||
|
|
d6932f33ea | ||
|
|
9ea845b497 | ||
|
|
917d6f6bcc | ||
|
|
39a263d157 | ||
|
|
c7360ee23c | ||
|
|
d1924f4044 | ||
|
|
4d86356990 | ||
|
|
505a2445eb | ||
|
|
75385c4b5d | ||
|
|
4d4053c7fb | ||
|
|
43062d0d93 | ||
|
|
956709da54 | ||
|
|
496e0ab754 | ||
|
|
ef97aac848 | ||
|
|
998fa1913f | ||
|
|
79cd265c3e | ||
|
|
ed5fd5b861 | ||
|
|
5e2dde252d | ||
|
|
79950ab9fc | ||
|
|
dffa651512 | ||
|
|
2dc36cea11 | ||
|
|
ad4c2c7b41 | ||
|
|
2022cdb58b | ||
|
|
5f779ce360 | ||
|
|
b9d981f80b | ||
|
|
65110abf4c | ||
|
|
b0e939ef23 | ||
|
|
607c0da5b4 | ||
|
|
1de7f831b5 | ||
|
|
ef328718cd | ||
|
|
465c4cc730 | ||
|
|
0dceeeffa4 | ||
|
|
af24464a8d | ||
|
|
5aa82d8149 | ||
|
|
e848e05cce | ||
|
|
323be10d03 | ||
|
|
51b382a739 | ||
|
|
7954104dfc |
106
.env.example
106
.env.example
@@ -1,10 +1,106 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
# AliasVault configuration file.
|
||||
#
|
||||
# Note: we recommend using the provided install.sh script to install and
|
||||
# configure AliasVault, as this will automatically set all of the following
|
||||
# variables for you and allow you to easily change them later via the CLI.
|
||||
# It also allows for easily updating AliasVault to a newer version in the
|
||||
# future.
|
||||
#
|
||||
# However if you still wish to manually install or configure AliasVault,
|
||||
# you can do so below.
|
||||
#
|
||||
# After changing settings here, make sure to restart all AliasVault
|
||||
# Docker containers to apply the changes.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Set the ports that your AliasVault will be accessible at.
|
||||
# These are the default ports that will be used by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these to any other ports that are available on your system.
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
SMTP_TLS_PORT=587
|
||||
|
||||
# Set the hostname that your AliasVault will be accessible at.
|
||||
# E.g. `aliasvault.mydomain.com` or if you're running it on your local machine, choose `localhost`.
|
||||
HOSTNAME=
|
||||
|
||||
# Set a random 32 character string for the JWT key.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
JWT_KEY=
|
||||
|
||||
# Set the password for the data protection certificate.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
DATA_PROTECTION_CERT_PASS=
|
||||
ADMIN_PASSWORD_HASH=
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
PRIVATE_EMAIL_DOMAINS=
|
||||
SMTP_TLS_ENABLED=false
|
||||
LETSENCRYPT_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Database configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# These are the credentials that are used by the PostgreSQL container
|
||||
# on startup to create the database and user, and for the application to
|
||||
# connect to the database.
|
||||
POSTGRES_DB=aliasvault
|
||||
POSTGRES_USER=aliasvault
|
||||
|
||||
# Set the password for the database user.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Note: in order to change the password for an existing installation
|
||||
# refer to https://docs.aliasvault.net/misc/dev/database-operations.html
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Admin user configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set the password for the admin user. This is an encrypted hash that needs
|
||||
# to be generated using the `aliasvault-cli` tool. This allows you to login
|
||||
# to the admin panel at https://your-hostname/admin.
|
||||
#
|
||||
# For example:
|
||||
# docker run --rm ghcr.io/lanedirt/aliasvault-installcli:latest hash-password "my-password"
|
||||
#
|
||||
# Then copy the output and paste it into the ADMIN_PASSWORD_HASH variable below.
|
||||
# When changing the hash, update the ADMIN_PASSWORD_GENERATED variable to the current date and time
|
||||
# and then restart the AliasVault docker containers to apply the changes.
|
||||
ADMIN_PASSWORD_HASH=
|
||||
|
||||
# Set the date and time the admin password was last generated. When changing the
|
||||
# admin password hash manually, make sure to increase this value so the system
|
||||
# knows that the password has been changed and should be overwritten with the new hash.
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Email server configuration for email aliases
|
||||
# ----------------------------------------------------------------------------
|
||||
# In order to use AliasVault's private email domains feature, you need to configure
|
||||
# your DNS. Please refer to the full documentation for more instructions on DNS:
|
||||
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup
|
||||
#
|
||||
# Set the private email domains below that are allowed to be used (comma separated values).
|
||||
# Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
|
||||
# To disable the private email domains feature, set this to "DISABLED.TLD"
|
||||
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
|
||||
|
||||
# Set whether TLS is enabled for SMTP.
|
||||
SMTP_TLS_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Let's Encrypt configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set whether Let's Encrypt is enabled. This is only supported through
|
||||
# the install.sh script.
|
||||
LETSENCRYPT_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Optional configuration settings
|
||||
# ----------------------------------------------------------------------------
|
||||
PUBLIC_REGISTRATION_ENABLED=true
|
||||
IP_LOGGING_ENABLED=true
|
||||
|
||||
# Set the support email address which is shown to users in the main web app.
|
||||
# Keep this blank if you don't want to show a support email.
|
||||
SUPPORT_EMAIL=
|
||||
|
||||
31
.gitattributes
vendored
31
.gitattributes
vendored
@@ -1,2 +1,31 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
# Set default behavior to automatically normalize line endings
|
||||
* text=auto
|
||||
|
||||
# Common files should always use LF (Unix-style) line endings
|
||||
*.sh text eol=lf
|
||||
*.cs text eol=lf
|
||||
*.razor text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# Docker files should use LF
|
||||
Dockerfile text eol=lf
|
||||
docker-compose*.yml text eol=lf
|
||||
|
||||
# Config files should use LF
|
||||
*.conf text eol=lf
|
||||
*.config text eol=lf
|
||||
.env* text eol=lf
|
||||
|
||||
# Batch scripts should always use CRLF (Windows-style) line endings
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Documentation should be normalized
|
||||
*.md text
|
||||
*.txt text
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
buy_me_a_coffee: lanedirt
|
||||
75
.github/workflows/browser-extension-build.yml
vendored
75
.github/workflows/browser-extension-build.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -164,76 +162,3 @@ jobs:
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
upload-chrome-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-chrome-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-chrome-extension.outputs.sha_short) || needs.build-chrome-extension.outputs.sha_short) }}-chrome
|
||||
path: browser-extension/dist/chrome-unpacked
|
||||
|
||||
- name: Zip Chrome Extension for release
|
||||
run: |
|
||||
cd browser-extension/dist
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-chrome.zip chrome-unpacked/*
|
||||
|
||||
- name: Upload Chrome Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*-chrome.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-firefox-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-firefox-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact Firefox
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-firefox-extension.outputs.sha_short) || needs.build-firefox-extension.outputs.sha_short) }}-firefox
|
||||
path: browser-extension/dist/firefox-unpacked
|
||||
|
||||
- name: Download built artifact Firefox sources
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-firefox-extension.outputs.sha_short) || needs.build-firefox-extension.outputs.sha_short) }}-sources
|
||||
path: browser-extension/dist/sources-unpacked
|
||||
|
||||
- name: Zip Firefox Extensions for release
|
||||
run: |
|
||||
cd browser-extension/dist
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-firefox.zip firefox-unpacked/*
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-sources.zip sources-unpacked/*
|
||||
|
||||
- name: Upload Firefox Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*{-firefox,-sources}.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-edge-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-edge-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-edge-extension.outputs.sha_short) || needs.build-edge-extension.outputs.sha_short) }}-edge
|
||||
path: browser-extension/dist/edge-unpacked
|
||||
|
||||
- name: Zip Edge Extension for release
|
||||
run: |
|
||||
cd browser-extension/dist
|
||||
zip -r aliasvault-browser-extension-${{ github.ref_name }}-edge.zip edge-unpacked/*
|
||||
|
||||
- name: Upload Edge Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*-edge.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.github/workflows/docker-compose-build.yml
vendored
4
.github/workflows/docker-compose-build.yml
vendored
@@ -92,9 +92,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
- name: Test install.sh reset-admin-password output
|
||||
run: |
|
||||
output=$(./install.sh reset-password)
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
echo "Password reset output format is incorrect"
|
||||
echo "Expected: 'New admin password: <at least 8 base64 chars>'"
|
||||
|
||||
69
.github/workflows/docker-compose-pull.yml
vendored
69
.github/workflows/docker-compose-pull.yml
vendored
@@ -43,44 +43,54 @@ jobs:
|
||||
|
||||
- name: Set permissions and run install.sh
|
||||
id: install_script
|
||||
continue-on-error: true
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
./install.sh install --verbose
|
||||
|
||||
- name: Check if failure was due to version mismatch
|
||||
if: steps.install_script.outcome == 'failure'
|
||||
run: |
|
||||
if grep -q "Install script needs updating to match version" <<< "$(./install.sh install --verbose 2>&1)"; then
|
||||
echo "Test skipped: Install script version is newer than latest release version. This is expected behavior if the install script is run on a branch that is ahead of the latest release."
|
||||
exit 0
|
||||
else
|
||||
echo "Test failed due to an unexpected error"
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
./install.sh install --verbose
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "Test skipped: Install script version is newer than latest release version. This is expected behavior if the install script is run on a branch that is ahead of the latest release."
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true # Force success exit code
|
||||
elif [ $exit_code -ne 0 ]; then
|
||||
false # Propagate failure
|
||||
fi
|
||||
} || {
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true # Version mismatch is okay
|
||||
else
|
||||
exit $exit_code # Propagate other failures
|
||||
fi
|
||||
}
|
||||
|
||||
- name: Set up Docker Compose
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
|
||||
- name: Wait for services to be up
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
# Wait for a few seconds
|
||||
sleep 10
|
||||
- name: Test if localhost:443 (WASM app) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with 200 OK"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443 (WASM app) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with 200 OK"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/api (WebApi) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -95,6 +105,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/admin (Admin) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -109,6 +120,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test if localhost:2525 (SmtpService) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -121,9 +133,10 @@ jobs:
|
||||
echo "SmtpService responded on port 2525"
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
- name: Test install.sh reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
output=$(./install.sh reset-password)
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
echo "Password reset output format is incorrect. Expected format: 'New admin password: <at least 8 base64 chars>'"
|
||||
echo "Actual output: $output"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# This workflow will publish new Docker images to the GitHub Container Registry when a new release is published.
|
||||
name: Publish Docker Images
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -11,7 +10,56 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
upload-install-script:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload install.sh to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: install.sh
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
package-browser-extensions:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
npm run zip:chrome
|
||||
npm run zip:firefox
|
||||
npm run zip:edge
|
||||
|
||||
- name: Upload extensions to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
browser-extension/dist/aliasvault-browser-extension-*-chrome.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-firefox.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-edge.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-sources.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-and-push-docker:
|
||||
needs: [upload-install-script, package-browser-extensions]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -114,4 +162,4 @@ jobs:
|
||||
file: src/Utilities/AliasVault.InstallCli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -2,10 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "C#: AliasVault.WebApp [http]",
|
||||
"name": "C#: AliasVault.Client [http]",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.WebApp/AliasVault.WebApp.csproj",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.Client/AliasVault.Client.csproj",
|
||||
"launchConfigurationId": "TargetFramework=;http"
|
||||
},
|
||||
{
|
||||
|
||||
47
.vscode/tasks.json
vendored
Normal file
47
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run and watch API",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Api"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run and watch Client",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Client"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run and watch Admin",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Admin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
# Contributing to the source code
|
||||
We welcome contributions to AliasVault. Please read the guidelines on the official AliasVault docs website on how to get your local development environment setup and the general contribution guidelines:
|
||||
|
||||
https://docs.aliasvault.net/misc/dev/contributing.html
|
||||
https://docs.aliasvault.net/misc/dev/
|
||||
|
||||
> Tip: if the URL above is not available, the raw doc pages can also be found in the `docs` folder in this repository.
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ This method uses pre-built Docker images and works on minimal hardware specifica
|
||||
|
||||
```bash
|
||||
# Download install script from latest stable release
|
||||
curl -o install.sh https://raw.githubusercontent.com/lanedirt/AliasVault/0.14.0/install.sh
|
||||
curl -o install.sh https://github.com/lanedirt/AliasVault/releases/latest/download/install.sh
|
||||
|
||||
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
|
||||
chmod +x install.sh
|
||||
@@ -81,7 +81,7 @@ The install script will output the URL where the app is available. By default th
|
||||
> Note: If you want to change the default AliasVault ports you can do so in the `.env` file.
|
||||
|
||||
## Documentation
|
||||
For more detailed information about the installation process and other topics, please see the official documentation website:
|
||||
For more information about the installation process, manual setup instructions and other topics, please see the official documentation website:
|
||||
- [Documentation website (docs.aliasvault.net) 📚](https://docs.aliasvault.net)
|
||||
|
||||
## Security Architecture
|
||||
@@ -108,6 +108,7 @@ AliasVault is under active development with new features being added regularly.
|
||||
- [x] Firefox and MS Edge browser extension
|
||||
- [x] Safari and Brave browser extension
|
||||
- [x] Add and associate TOTP MFA tokens to credentials
|
||||
- [x] Add GUI to allow customizing password generation options (length, special chars etc.) (https://github.com/lanedirt/AliasVault/issues/167)
|
||||
- [ ] Import passwords from existing password managers (https://github.com/lanedirt/AliasVault/issues/542)
|
||||
- [ ] Add support for connecting custom user domains to cloud hosted version (https://github.com/lanedirt/AliasVault/issues/485)
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Cryptography.Cli
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generators", "Generators", "{03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Generators.Password", "src\Generators\AliasVault.Generators.Password\AliasVault.Generators.Password.csproj", "{47F47A1B-49E0-406A-81C8-31FF2E4C339B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Generators.Identity", "src\Generators\AliasVault.Generators.Identity\AliasVault.Generators.Identity.csproj", "{80E74FBC-4EC8-45FB-B210-473337C484B5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DD359F0A-0180-4F8F-9E48-46213386BA4D}"
|
||||
@@ -161,10 +159,6 @@ Global
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -188,6 +182,7 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{ED328644-A152-403D-86EB-81201AA07744} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{8E6A418A-B305-465D-857D-49953605C18E} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{15EFE0D0-F41B-47D7-86B7-8F840335CB82} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
@@ -198,16 +193,14 @@ Global
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{857BCD0E-753F-437A-AF75-B995B4D9A5FE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{FF0B0E64-1AE2-415C-A404-0EB78010821A} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{E8D9C551-67D2-4651-8EDF-4262DF7375CE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{DA175274-0FF7-4436-9266-742F96C2D1ED} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{BB7E701E-B1C6-453E-800A-E12CE256318D} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{341EC443-0B6B-4E8C-AF46-D6156573CEA5} = {BB7E701E-B1C6-453E-800A-E12CE256318D}
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E} = {BB7E701E-B1C6-453E-800A-E12CE256318D}
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B} = {03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5} = {03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}
|
||||
{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}
|
||||
|
||||
6
browser-extension/package-lock.json
generated
6
browser-extension/package-lock.json
generated
@@ -12449,9 +12449,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
|
||||
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
|
||||
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -530,7 +530,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.14.0;
|
||||
MARKETING_VERSION = 0.15.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -554,7 +554,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 6;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.14.0;
|
||||
MARKETING_VERSION = 0.15.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { browser } from "wxt/browser";
|
||||
import { defineBackground } from 'wxt/sandbox';
|
||||
import { onMessage } from "webext-bridge/background";
|
||||
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential } from './background/PopupMessageHandler';
|
||||
|
||||
export default defineBackground({
|
||||
@@ -12,7 +12,7 @@ export default defineBackground({
|
||||
main() {
|
||||
// Set up context menus
|
||||
setupContextMenus();
|
||||
browser.contextMenus.onClicked.addListener((info: browser.menus.OnClickData, tab?: browser.tabs.Tab) =>
|
||||
browser.contextMenus.onClicked.addListener((info: browser.contextMenus.OnClickData, tab?: browser.tabs.Tab) =>
|
||||
handleContextMenuClick(info, tab)
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ export default defineBackground({
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_PASSWORD_SETTINGS', () => handleGetPasswordSettings());
|
||||
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
|
||||
@@ -10,6 +10,7 @@ import { BoolResponse as messageBoolResponse } from '../../utils/types/messaging
|
||||
import { VaultResponse as messageVaultResponse } from '../../utils/types/messaging/VaultResponse';
|
||||
import { CredentialsResponse as messageCredentialsResponse } from '../../utils/types/messaging/CredentialsResponse';
|
||||
import { DefaultEmailDomainResponse as messageDefaultEmailDomainResponse } from '../../utils/types/messaging/DefaultEmailDomainResponse';
|
||||
import { PasswordSettingsResponse as messagePasswordSettingsResponse } from '../../utils/types/messaging/PasswordSettingsResponse';
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked.
|
||||
@@ -20,7 +21,7 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData === null;
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
@@ -258,6 +259,22 @@ export function handleGetDefaultEmailDomain(
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password settings.
|
||||
*/
|
||||
export async function handleGetPasswordSettings(
|
||||
) : Promise<messagePasswordSettingsResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const passwordSettings = sqliteClient.getPasswordSettings();
|
||||
|
||||
return { success: true, settings: passwordSettings };
|
||||
} catch (error) {
|
||||
console.error('Error getting password settings:', error);
|
||||
return { success: false, error: 'Failed to get password settings' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the derived key for the encrypted vault.
|
||||
*/
|
||||
|
||||
@@ -39,6 +39,12 @@ export default defineContentScript({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if element itself, html or body has av-disable attribute like av-disable="true"
|
||||
const avDisable = (e.target as HTMLElement).getAttribute('av-disable') ?? document.body?.getAttribute('av-disable') ?? document.documentElement.getAttribute('av-disable');
|
||||
if (avDisable === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLInputElement;
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url'];
|
||||
|
||||
|
||||
@@ -161,51 +161,53 @@ export function injectIcon(input: HTMLInputElement, container: HTMLElement): voi
|
||||
* Trigger input events for an element to trigger form validation
|
||||
* which some websites require before the "continue" button is enabled.
|
||||
*/
|
||||
function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement) : void {
|
||||
// Create an overlay div that will show the highlight effect
|
||||
const overlay = document.createElement('div');
|
||||
function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement, animate: boolean = true) : void {
|
||||
// Add keyframe animation if animation is requested
|
||||
if (animate) {
|
||||
// Create an overlay div that will show the highlight effect
|
||||
const overlay = document.createElement('div');
|
||||
|
||||
/**
|
||||
* Update position of the overlay.
|
||||
*/
|
||||
const updatePosition = () : void => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 999999991;
|
||||
pointer-events: none;
|
||||
top: ${rect.top}px;
|
||||
left: ${rect.left}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
background-color: rgba(244, 149, 65, 0.3);
|
||||
border-radius: ${getComputedStyle(element).borderRadius};
|
||||
animation: fadeOut 1.4s ease-out forwards;
|
||||
/**
|
||||
* Update position of the overlay.
|
||||
*/
|
||||
const updatePosition = () : void => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 999999991;
|
||||
pointer-events: none;
|
||||
top: ${rect.top}px;
|
||||
left: ${rect.left}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
background-color: rgba(244, 149, 65, 0.3);
|
||||
border-radius: ${getComputedStyle(element).borderRadius};
|
||||
animation: fadeOut 1.4s ease-out forwards;
|
||||
`;
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
|
||||
// Add scroll event listener
|
||||
window.addEventListener('scroll', updatePosition);
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
};
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
updatePosition();
|
||||
|
||||
// Add scroll event listener
|
||||
window.addEventListener('scroll', updatePosition);
|
||||
|
||||
// Add keyframe animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Remove overlay and cleanup after animation
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('scroll', updatePosition);
|
||||
overlay.remove();
|
||||
style.remove();
|
||||
}, 1400);
|
||||
// Remove overlay and cleanup after animation
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('scroll', updatePosition);
|
||||
overlay.remove();
|
||||
style.remove();
|
||||
}, 1400);
|
||||
}
|
||||
|
||||
// Trigger events
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
@@ -7,6 +7,7 @@ import { storage } from "wxt/storage";
|
||||
import { sendMessage } from "webext-bridge/content-script";
|
||||
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
|
||||
import { CombinedStopWords } from '../../utils/formDetector/FieldPatterns';
|
||||
import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
|
||||
|
||||
/**
|
||||
* WeakMap to store event listeners for popup containers
|
||||
@@ -212,7 +213,11 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
const identityGenerator = new IdentityGeneratorEn();
|
||||
const identity = await identityGenerator.generateRandomIdentity();
|
||||
|
||||
const passwordGenerator = new PasswordGenerator();
|
||||
// Get password settings from background
|
||||
const passwordSettingsResponse = await sendMessage('GET_PASSWORD_SETTINGS', {}, 'background') as PasswordSettingsResponse;
|
||||
|
||||
// Initialize password generator with the retrieved settings
|
||||
const passwordGenerator = new PasswordGenerator(passwordSettingsResponse.settings);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
// Extract favicon from page and get the bytes
|
||||
@@ -325,58 +330,7 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
|
||||
// Handle search input.
|
||||
let searchTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
searchInput.addEventListener('input', async () => {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
|
||||
const response = await sendMessage('GET_CREDENTIALS', {}, 'background') as CredentialsResponse;
|
||||
if (response.success && response.credentials) {
|
||||
// Ensure we have unique credentials
|
||||
const uniqueCredentials = Array.from(new Map(response.credentials.map(cred => [cred.Id, cred])).values());
|
||||
let filteredCredentials;
|
||||
|
||||
if (searchTerm === '') {
|
||||
// If search is empty, use original URL-based filtering
|
||||
filteredCredentials = filterCredentials(
|
||||
uniqueCredentials,
|
||||
window.location.href,
|
||||
document.title
|
||||
).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
} else {
|
||||
// Otherwise filter based on search term
|
||||
filteredCredentials = uniqueCredentials.filter(cred =>
|
||||
cred.ServiceName.toLowerCase().includes(searchTerm) ||
|
||||
cred.Username.toLowerCase().includes(searchTerm) ||
|
||||
cred.Email.toLowerCase().includes(searchTerm) ||
|
||||
cred.ServiceUrl?.toLowerCase().includes(searchTerm)
|
||||
).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
// Update popup content with filtered results
|
||||
updatePopupContent(filteredCredentials, credentialList, input, rootContainer);
|
||||
}
|
||||
});
|
||||
searchInput.addEventListener('input', () => handleSearchInput(searchInput, credentials, rootContainer, searchTimeout, credentialList, input));
|
||||
|
||||
// Close button
|
||||
const closeButton = document.createElement('button');
|
||||
@@ -513,6 +467,58 @@ export function createVaultLockedPopup(input: HTMLInputElement, rootContainer: H
|
||||
rootContainer.appendChild(popup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle popup search input by filtering credentials based on the search term.
|
||||
*/
|
||||
function handleSearchInput(searchInput: HTMLInputElement, credentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement) : void {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
|
||||
// Ensure we have unique credentials
|
||||
const uniqueCredentials = Array.from(new Map(credentials.map(cred => [cred.Id, cred])).values());
|
||||
let filteredCredentials;
|
||||
|
||||
if (searchTerm === '') {
|
||||
// If search is empty, use original URL-based filtering
|
||||
filteredCredentials = filterCredentials(
|
||||
uniqueCredentials,
|
||||
window.location.href,
|
||||
document.title
|
||||
).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
} else {
|
||||
// Otherwise filter based on search term
|
||||
filteredCredentials = uniqueCredentials.filter(cred =>
|
||||
cred.ServiceName?.toLowerCase().includes(searchTerm) ||
|
||||
cred.Username?.toLowerCase().includes(searchTerm) ||
|
||||
cred.Email?.toLowerCase().includes(searchTerm) ||
|
||||
cred.ServiceUrl?.toLowerCase().includes(searchTerm)
|
||||
).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
// Update popup content with filtered results
|
||||
updatePopupContent(filteredCredentials, credentialList, input, rootContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create credential list content for popup
|
||||
*
|
||||
@@ -962,18 +968,17 @@ function detectMimeType(bytes: Uint8Array): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss vault locked popup for 4 hours if user is logged in but vault is locked,
|
||||
* or for 3 days if user is not logged in.
|
||||
* Dismiss vault locked popup for 4 hours if user is logged in, or for 3 days if user is not logged in.
|
||||
*/
|
||||
export async function dismissVaultLockedPopup(): Promise<void> {
|
||||
// First check if user is logged in but vault is locked, or not logged in at all
|
||||
// First check if user is logged in or not.
|
||||
const authStatus = await sendMessage('CHECK_AUTH_STATUS', {}, 'background') as { isLoggedIn: boolean, isVaultLocked: boolean };
|
||||
|
||||
if (authStatus.isLoggedIn && authStatus.isVaultLocked) {
|
||||
// User is logged in but vault is locked - dismiss for 4 hours
|
||||
if (authStatus.isLoggedIn) {
|
||||
// User is logged in - dismiss for 4 hours
|
||||
const fourHoursFromNow = Date.now() + (4 * 60 * 60 * 1000);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, fourHoursFromNow);
|
||||
} else if (!authStatus.isLoggedIn) {
|
||||
} else {
|
||||
// User is not logged in - dismiss for 3 days
|
||||
const threeDaysFromNow = Date.now() + (3 * 24 * 60 * 60 * 1000);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, threeDaysFromNow);
|
||||
|
||||
@@ -6,7 +6,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current extension version. This should be updated with each release of the extension.
|
||||
*/
|
||||
public static readonly VERSION = '0.14.0';
|
||||
public static readonly VERSION = '0.15.1';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
@@ -2,6 +2,7 @@ import initSqlJs, { Database } from 'sql.js';
|
||||
import { Credential } from './types/Credential';
|
||||
import { EncryptionKey } from './types/EncryptionKey';
|
||||
import { TotpCode } from './types/TotpCode';
|
||||
import { PasswordSettings } from './types/PasswordSettings';
|
||||
|
||||
/**
|
||||
* Client for interacting with the SQLite database.
|
||||
@@ -280,6 +281,33 @@ class SqliteClient {
|
||||
return this.getSetting('DefaultEmailDomain');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password settings from the database.
|
||||
*/
|
||||
public getPasswordSettings(): PasswordSettings {
|
||||
const settingsJson = this.getSetting('PasswordGenerationSettings');
|
||||
|
||||
// Default settings if none found or parsing fails
|
||||
const defaultSettings: PasswordSettings = {
|
||||
Length: 18,
|
||||
UseLowercase: true,
|
||||
UseUppercase: true,
|
||||
UseNumbers: true,
|
||||
UseSpecialChars: true,
|
||||
UseNonAmbiguousChars: false
|
||||
};
|
||||
|
||||
try {
|
||||
if (settingsJson) {
|
||||
return { ...defaultSettings, ...JSON.parse(settingsJson) };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse password settings:', error);
|
||||
}
|
||||
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new credential with associated entities
|
||||
* @param credential The credential object to insert
|
||||
|
||||
@@ -11,8 +11,13 @@ export class FormFiller {
|
||||
*/
|
||||
public constructor(
|
||||
private readonly form: FormFields,
|
||||
private readonly triggerInputEvents: (element: HTMLInputElement | HTMLSelectElement) => void
|
||||
) {}
|
||||
private readonly triggerInputEvents: (element: HTMLInputElement | HTMLSelectElement, animate?: boolean) => void
|
||||
) {
|
||||
/**
|
||||
* Trigger input events.
|
||||
*/
|
||||
this.triggerInputEvents = (element: HTMLInputElement | HTMLSelectElement, animate = true) : void => triggerInputEvents(element, animate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the fields of the form with the given credential.
|
||||
@@ -72,7 +77,7 @@ export class FormFiller {
|
||||
|
||||
/**
|
||||
* Fill the password field with the given password. This uses a small delay between each character to simulate human typing.
|
||||
* In the past there have been issues where Microsoft 365 login forms would clear the password field when just setting the value directly.
|
||||
* Simulates actual keystroke behavior by appending characters one by one.
|
||||
*
|
||||
* @param field The password field to fill.
|
||||
* @param password The password to fill the field with.
|
||||
@@ -80,14 +85,17 @@ export class FormFiller {
|
||||
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
|
||||
// Clear the field first
|
||||
field.value = '';
|
||||
this.triggerInputEvents(field);
|
||||
this.triggerInputEvents(field, false);
|
||||
|
||||
// Type each character with a small delay
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
field.value = password.substring(0, i + 1);
|
||||
for (const char of password) {
|
||||
// Append the character to the current value instead of using substring
|
||||
field.value += char;
|
||||
// Small random delay between 5-15ms to simulate human typing
|
||||
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 5));
|
||||
}
|
||||
|
||||
this.triggerInputEvents(field, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PasswordSettings } from '../../types/PasswordSettings';
|
||||
|
||||
/**
|
||||
* Generate a random password.
|
||||
*/
|
||||
@@ -6,12 +8,37 @@ export class PasswordGenerator {
|
||||
private readonly uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
private readonly numberChars = '0123456789';
|
||||
private readonly specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
private readonly ambiguousChars = 'Il1O0';
|
||||
|
||||
private length: number = 18;
|
||||
private useLowercase: boolean = true;
|
||||
private useUppercase: boolean = true;
|
||||
private useNumbers: boolean = true;
|
||||
private useSpecial: boolean = true;
|
||||
private useNonAmbiguous: boolean = false;
|
||||
|
||||
/**
|
||||
* Create a new instance of PasswordGenerator.
|
||||
* @param settings Optional password settings to initialize with.
|
||||
*/
|
||||
public constructor(settings?: PasswordSettings) {
|
||||
if (settings) {
|
||||
this.applySettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply password settings to this generator.
|
||||
*/
|
||||
public applySettings(settings: PasswordSettings): this {
|
||||
this.length = settings.Length;
|
||||
this.useLowercase = settings.UseLowercase;
|
||||
this.useUppercase = settings.UseUppercase;
|
||||
this.useNumbers = settings.UseNumbers;
|
||||
this.useSpecial = settings.UseSpecialChars;
|
||||
this.useNonAmbiguous = settings.UseNonAmbiguousChars;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of the password.
|
||||
@@ -53,11 +80,19 @@ export class PasswordGenerator {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if only non-ambiguous characters should be used.
|
||||
*/
|
||||
public useNonAmbiguousCharacters(use: boolean): this {
|
||||
this.useNonAmbiguous = use;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a random index from the crypto module.
|
||||
*/
|
||||
private getUnbiasedRandomIndex(max: number): number {
|
||||
// Calculate the largest multiple of max that fits within Uint32
|
||||
// Calculate the largest multiple of max that fits within Uint32.
|
||||
const limit = Math.floor((2 ** 32) / max) * max;
|
||||
|
||||
while (true) {
|
||||
@@ -65,7 +100,7 @@ export class PasswordGenerator {
|
||||
crypto.getRandomValues(array);
|
||||
const value = array[0];
|
||||
|
||||
// Reject values that would introduce bias
|
||||
// Reject values that would introduce bias.
|
||||
if (value < limit) {
|
||||
return value % max;
|
||||
}
|
||||
@@ -76,59 +111,149 @@ export class PasswordGenerator {
|
||||
* Generate a random password.
|
||||
*/
|
||||
public generateRandomPassword(): string {
|
||||
let chars = '';
|
||||
let password = '';
|
||||
// Build the character set based on settings
|
||||
const chars = this.buildCharacterSet();
|
||||
|
||||
// Generate initial password.
|
||||
let password = this.generateInitialPassword(chars);
|
||||
|
||||
// Ensure a character from each set is present as some websites require this.
|
||||
password = this.ensureRequirements(password);
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build character set based on selected options.
|
||||
*/
|
||||
private buildCharacterSet(): string {
|
||||
let chars = '';
|
||||
|
||||
// Build character set based on options
|
||||
if (this.useLowercase) {
|
||||
chars += this.lowercaseChars;
|
||||
}
|
||||
|
||||
if (this.useUppercase) {
|
||||
chars += this.uppercaseChars;
|
||||
}
|
||||
|
||||
if (this.useNumbers) {
|
||||
chars += this.numberChars;
|
||||
}
|
||||
|
||||
if (this.useSpecial) {
|
||||
chars += this.specialChars;
|
||||
}
|
||||
|
||||
// Ensure at least one character set is selected
|
||||
// Ensure at least one character set is selected, otherwise default to lowercase.
|
||||
if (chars.length === 0) {
|
||||
chars = this.lowercaseChars;
|
||||
}
|
||||
|
||||
// Generate password
|
||||
// Remove ambiguous characters if needed.
|
||||
if (this.useNonAmbiguous) {
|
||||
chars = this.removeAmbiguousCharacters(chars);
|
||||
}
|
||||
|
||||
return chars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove ambiguous characters from a character set.
|
||||
*/
|
||||
private removeAmbiguousCharacters(chars: string): string {
|
||||
for (const ambChar of this.ambiguousChars) {
|
||||
chars = chars.replace(ambChar, '');
|
||||
}
|
||||
return chars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate initial random password.
|
||||
*/
|
||||
private generateInitialPassword(chars: string): string {
|
||||
let password = '';
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
password += chars[this.getUnbiasedRandomIndex(chars.length)];
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
// Ensure password contains at least one character from each selected set
|
||||
/**
|
||||
* Ensure the generated password meets all specified requirements.
|
||||
*/
|
||||
private ensureRequirements(password: string): string {
|
||||
if (this.useLowercase && !/[a-z]/.exec(password)) {
|
||||
const pos = this.getUnbiasedRandomIndex(this.length);
|
||||
password = password.substring(0, pos) +
|
||||
this.lowercaseChars[this.getUnbiasedRandomIndex(this.lowercaseChars.length)] +
|
||||
password.substring(pos + 1);
|
||||
password = this.addCharacterFromSet(
|
||||
password,
|
||||
this.getSafeCharacterSet(this.lowercaseChars, true)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.useUppercase && !/[A-Z]/.exec(password)) {
|
||||
const pos = this.getUnbiasedRandomIndex(this.length);
|
||||
password = password.substring(0, pos) +
|
||||
this.uppercaseChars[this.getUnbiasedRandomIndex(this.uppercaseChars.length)] +
|
||||
password.substring(pos + 1);
|
||||
password = this.addCharacterFromSet(
|
||||
password,
|
||||
this.getSafeCharacterSet(this.uppercaseChars, true)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.useNumbers && !/\d/.exec(password)) {
|
||||
const pos = this.getUnbiasedRandomIndex(this.length);
|
||||
password = password.substring(0, pos) +
|
||||
this.numberChars[this.getUnbiasedRandomIndex(this.numberChars.length)] +
|
||||
password.substring(pos + 1);
|
||||
password = this.addCharacterFromSet(
|
||||
password,
|
||||
this.getSafeCharacterSet(this.numberChars, false)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.useSpecial && !/[!@#$%^&*()_+\-=[\]{}|;:,.<>?]/.exec(password)) {
|
||||
const pos = this.getUnbiasedRandomIndex(this.length);
|
||||
password = password.substring(0, pos) +
|
||||
this.specialChars[this.getUnbiasedRandomIndex(this.specialChars.length)] +
|
||||
password.substring(pos + 1);
|
||||
password = this.addCharacterFromSet(
|
||||
password,
|
||||
this.specialChars
|
||||
);
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a character set with ambiguous characters removed if needed.
|
||||
*/
|
||||
private getSafeCharacterSet(charSet: string, isAlpha: boolean): string {
|
||||
// If we're not using non-ambiguous characters, just return the original set.
|
||||
if (!this.useNonAmbiguous) {
|
||||
return charSet;
|
||||
}
|
||||
|
||||
let safeSet = charSet;
|
||||
for (const ambChar of this.ambiguousChars) {
|
||||
// For numeric sets, only process numeric ambiguous characters
|
||||
if (!isAlpha && !/\d/.test(ambChar)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let charToRemove = ambChar;
|
||||
|
||||
// Handle case conversion for alphabetic characters.
|
||||
if (isAlpha) {
|
||||
if (charSet === this.lowercaseChars) {
|
||||
charToRemove = ambChar.toLowerCase();
|
||||
} else {
|
||||
charToRemove = ambChar.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
safeSet = safeSet.replace(charToRemove, '');
|
||||
}
|
||||
|
||||
return safeSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a character from the given set at a random position in the password.
|
||||
*/
|
||||
private addCharacterFromSet(password: string, charSet: string): string {
|
||||
const pos = this.getUnbiasedRandomIndex(this.length);
|
||||
const char = charSet[this.getUnbiasedRandomIndex(charSet.length)];
|
||||
|
||||
return password.substring(0, pos) + char + password.substring(pos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
34
browser-extension/src/utils/types/PasswordSettings.ts
Normal file
34
browser-extension/src/utils/types/PasswordSettings.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Settings for password generation stored in SQLite database settings table as string.
|
||||
*/
|
||||
export type PasswordSettings = {
|
||||
/**
|
||||
* The length of the password.
|
||||
*/
|
||||
Length: number;
|
||||
|
||||
/**
|
||||
* Whether to use lowercase letters.
|
||||
*/
|
||||
UseLowercase: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use uppercase letters.
|
||||
*/
|
||||
UseUppercase: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use numbers.
|
||||
*/
|
||||
UseNumbers: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use special characters.
|
||||
*/
|
||||
UseSpecialChars: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use non-ambiguous characters.
|
||||
*/
|
||||
UseNonAmbiguousChars: boolean;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PasswordSettings } from "@/utils/types/PasswordSettings";
|
||||
|
||||
export type PasswordSettingsResponse = {
|
||||
success: boolean,
|
||||
error?: string,
|
||||
settings?: PasswordSettings
|
||||
};
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.14.0",
|
||||
version: "0.15.1",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -239,9 +239,9 @@ GEM
|
||||
minitest (5.25.1)
|
||||
net-http (0.5.0)
|
||||
uri
|
||||
nokogiri (1.18.3-x86_64-linux-gnu)
|
||||
nokogiri (1.18.4-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.3-x86_64-linux-musl)
|
||||
nokogiri (1.18.4-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
octokit (4.25.1)
|
||||
faraday (>= 1, < 3)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Build from Source
|
||||
parent: Advanced
|
||||
nav_order: 1
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Build from Source
|
||||
@@ -26,7 +26,7 @@ cd AliasVault
|
||||
chmod +x install.sh
|
||||
./install.sh build
|
||||
```
|
||||
> **Note:** The build process can take a while depending on your hardware (5-15 minutes).
|
||||
> **Note:** The complete build process can take a while depending on your hardware (5-15 minutes).
|
||||
|
||||
3. After the script completes, you can access AliasVault at:
|
||||
- Client: `https://localhost`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
layout: default
|
||||
title: Database Backup
|
||||
parent: Advanced
|
||||
nav_order: 2
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# Database Backup
|
||||
|
||||
@@ -2,171 +2,109 @@
|
||||
layout: default
|
||||
title: Manual Setup
|
||||
parent: Advanced
|
||||
nav_order: 3
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Manual Setup
|
||||
|
||||
If you prefer to manually set up AliasVault, this README provides step-by-step instructions. Follow these steps if you prefer to execute all statements yourself.
|
||||
If you prefer to manually set up AliasVault instead of using the `install.sh` script, this README provides step-by-step instructions.
|
||||
|
||||
{: .toc }
|
||||
* TOC
|
||||
{:toc}
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed on your system
|
||||
- Knowledge of working with direct Docker commands
|
||||
- Knowledge of .env files
|
||||
- OpenSSL for generating random passwords
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Create required directories**
|
||||
1. **Clone the git repository**
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/lanedirt/AliasVault.git
|
||||
|
||||
# Navigate to the AliasVault directory
|
||||
cd AliasVault
|
||||
```
|
||||
|
||||
2. **Create required directories**
|
||||
|
||||
Create the following directories in your project root:
|
||||
```bash
|
||||
# Create required directories
|
||||
mkdir -p certificates/ssl certificates/app database/postgres
|
||||
```
|
||||
|
||||
2. **Create .env file**
|
||||
3. **Create .env file**
|
||||
|
||||
Copy the `.env.example` file to create a new `.env` file:
|
||||
```bash
|
||||
# Copy the .env.example file to create a new .env file
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. **Set HOSTNAME**
|
||||
4. **Set all required settings in .env**
|
||||
|
||||
Open the .env file in your favorite text editor and fill in all required variables
|
||||
by following the instructions inside the file.
|
||||
|
||||
Update the .env file with your hostname (default is localhost):
|
||||
```bash
|
||||
HOSTNAME=localhost
|
||||
# Open the .env file with your favorite editor, e.g. nano.
|
||||
nano .env
|
||||
```
|
||||
|
||||
4. **Set default ports**
|
||||
5. **Start the docker containers**
|
||||
|
||||
Update the .env file with the ports you want to use for the AliasVault components. The values defined here are used by the docker-compose.yml file.
|
||||
After you are done configuring your .env file, you can start the Docker Compose stack:
|
||||
```bash
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
SMTP_TLS_PORT=587
|
||||
# Start the docker compose stack
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
5. **Generate and set JWT_KEY**
|
||||
|
||||
Generate a random 32-char string for JWT token generation:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Add the generated key to the .env file:
|
||||
```bash
|
||||
JWT_KEY=your_generated_key_here
|
||||
```
|
||||
|
||||
6. **Generate and set DATA_PROTECTION_CERT_PASS**
|
||||
|
||||
Generate a random password for the data protection certificate:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
Add it to the .env file:
|
||||
```bash
|
||||
DATA_PROTECTION_CERT_PASS=your_generated_password_here
|
||||
```
|
||||
|
||||
7. **Configure PostgreSQL Settings**
|
||||
|
||||
Set the following PostgreSQL-related variables in your .env file:
|
||||
```bash
|
||||
# Database name (default: aliasvault)
|
||||
POSTGRES_DB=aliasvault
|
||||
|
||||
# Database user (default: aliasvault)
|
||||
POSTGRES_USER=aliasvault
|
||||
|
||||
# Generate a secure password for PostgreSQL
|
||||
POSTGRES_PASSWORD=$(openssl rand -base64 32)
|
||||
```
|
||||
|
||||
8. **Set PRIVATE_EMAIL_DOMAINS**
|
||||
|
||||
Update the .env file with allowed email domains. Use DISABLED.TLD to disable email support:
|
||||
```bash
|
||||
PRIVATE_EMAIL_DOMAINS=yourdomain.com,anotherdomain.com
|
||||
```
|
||||
Or to disable email:
|
||||
```bash
|
||||
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
|
||||
```
|
||||
|
||||
9. **Set SUPPORT_EMAIL (Optional)**
|
||||
|
||||
Add a support email address if desired:
|
||||
```bash
|
||||
SUPPORT_EMAIL=support@yourdomain.com
|
||||
```
|
||||
|
||||
10. **Generate admin password**
|
||||
|
||||
Build the Docker image for password hashing:
|
||||
```bash
|
||||
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile .
|
||||
```
|
||||
|
||||
Generate the password hash:
|
||||
```bash
|
||||
docker run --rm installcli "your_preferred_admin_password_here"
|
||||
```
|
||||
|
||||
Add the password hash and generation timestamp to the .env file:
|
||||
```bash
|
||||
ADMIN_PASSWORD_HASH=<output_from_previous_command>
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
```
|
||||
|
||||
11. **Optional configuration**
|
||||
Enable or disable public registration of new users:
|
||||
```bash
|
||||
PUBLIC_REGISTRATION_ENABLED=false
|
||||
```
|
||||
|
||||
12. **Build and start Docker containers**
|
||||
|
||||
Build the Docker Compose stack:
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.build.yml build
|
||||
```
|
||||
|
||||
Start the Docker Compose stack:
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d
|
||||
```
|
||||
|
||||
13. **Access AliasVault**
|
||||
6. **Access AliasVault**
|
||||
|
||||
AliasVault should now be running. You can access it at:
|
||||
|
||||
- Admin Panel: https://localhost/admin
|
||||
- Username: admin
|
||||
- Password: [Use the password you set in step 9]
|
||||
- Password: [Use the password you set in the .env file]
|
||||
|
||||
- Client Website: https://localhost/
|
||||
- Create your own account from here
|
||||
|
||||
> Note: if you changed the default ports from 80/443 to something else in the .env file, use those ports to access AliasVault here.
|
||||
|
||||
7. **Configuring private email domains**
|
||||
|
||||
By default, the AliasVault private email domains feature is disabled. If you wish to enable this so you can use your own private domains to create email aliases with, please read the `Email Server Setup` section in the main installation guide [Basic Install](../install.md#3-email-server-setup).
|
||||
|
||||
For more information, read the article explaining the differences between AliasVault's [private and public domains](../../misc/private-vs-public-email.md).
|
||||
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Make sure to save both the admin password and PostgreSQL password in a secure location.
|
||||
- If you need to reset the admin password in the future, repeat step 9 and restart the Docker containers.
|
||||
- Always keep your .env file secure and do not share it, as it contains sensitive information.
|
||||
- The PostgreSQL data is persisted in the `database/postgres` directory.
|
||||
- The docker-compose.yml file uses the `:latest` tag for containers by default. This means it always uses the latest available AliasVault version. In order to update AliasVault to a newer version at a later time, you can pull new containers when they are available with this command:
|
||||
```
|
||||
docker compose pull && docker compose down && docker compose up -d
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues during the setup:
|
||||
|
||||
1. Check the Docker logs:
|
||||
```bash
|
||||
docker compose logs
|
||||
```
|
||||
2. Ensure all required ports (80, 443, and 5432) are available and not being used by other services.
|
||||
3. Verify that all environment variables in the .env file are set correctly.
|
||||
2. Ensure all required ports (80, 443, 25, 587 and 5432) are available and not being used by other services.
|
||||
3. Verify that all variables in the .env file are set correctly.
|
||||
4. Check PostgreSQL container logs specifically:
|
||||
```bash
|
||||
docker compose logs postgres
|
||||
|
||||
@@ -27,13 +27,16 @@ To get AliasVault up and running quickly, run the install script to pull pre-bui
|
||||
### 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
|
||||
# Download the install script
|
||||
curl -o install.sh https://github.com/lanedirt/AliasVault/releases/latest/download/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.
|
||||
|
||||
3. Run the installation wizard.
|
||||
```bash
|
||||
./install.sh install
|
||||
```
|
||||
@@ -43,6 +46,8 @@ chmod +x install.sh
|
||||
- Client: `https://localhost`
|
||||
- Admin: `https://localhost/admin`
|
||||
|
||||
> Note: if you do not wish to run the `install.sh` wizard but want to use Docker commands directly, follow the [manual setup guide](advanced/manual-setup.md). We do however encourage the use of `install.sh` as it will guide you through all configuration steps and allow for easy updating your AliasVault instance later.
|
||||
|
||||
---
|
||||
|
||||
## 2. SSL configuration
|
||||
|
||||
@@ -102,10 +102,10 @@ Refer to the [installation guide](./install.md) for more information on how to c
|
||||
|
||||
|
||||
### 4. Forgot AliasVault 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.
|
||||
If you have lost your admin password, you can reset it by running the install script with the `reset-admin-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
|
||||
./install.sh reset-admin-password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
layout: default
|
||||
title: Browser Extensions
|
||||
title: Browser extensions
|
||||
parent: Development
|
||||
grand_parent: Miscellaneous
|
||||
nav_order: 2
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# Browser Extensions
|
||||
# Browser extensions
|
||||
AliasVault offers browser extensions compatible with both Chrome and Firefox. This guide explains how to build and debug the extensions locally.
|
||||
|
||||
## Development Setup
|
||||
@@ -109,3 +109,5 @@ The following websites have been known to cause issues in the past (but should b
|
||||
| https://bloshing.com/inschrijven-nieuwsbrief | Popup CSS style conflicts |
|
||||
| https://gamefaqs.gamespot.com/user | Popup buttons not working |
|
||||
| https://news.ycombinator.com/login?goto=news | Popup and client favicon not showing due to SVG format |
|
||||
| https://vault.bitwarden.com/#/login | Autofill password not detected (input not long enough), manually typing in works |
|
||||
| https://login.microsoftonline.com/ | Password gets reset after autofill |
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
title: Contributing
|
||||
parent: Development
|
||||
grand_parent: Miscellaneous
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Contributing
|
||||
This document is a work-in-progress and will be expanded as time goes on. If you have any questions feel free to open a issue on GitHub.
|
||||
|
||||
Note: all instructions below are based on MacOS. If you are using a different operating system, you may need to adjust the commands accordingly.
|
||||
|
||||
## Getting Started
|
||||
In order to contribute to this project follow these instructions to setup your local environment:
|
||||
|
||||
### 1. Clone the repository
|
||||
```bash
|
||||
git clone https://github.com/lanedirt/AliasVault.git
|
||||
cd AliasVault
|
||||
```
|
||||
|
||||
### 2. Copy pre-commit hook script to .git/hooks directory
|
||||
{: .note }
|
||||
All commits in this repo are required to contain a reference to a GitHub issue in the format of "your commit message (#123)" where "123" references the GitHub issue number.
|
||||
|
||||
The pre-commit hook script below will check the commit message before allowing the commit to proceed. If the commit message is invalid, the commit will be aborted.
|
||||
|
||||
```bash
|
||||
# Copy the commit-msg hook script to the .git/hooks directory
|
||||
cp .github/hooks/commit-msg .git/hooks/commit-msg
|
||||
|
||||
# Make the script executable
|
||||
chmod +x .git/hooks/commit-msg
|
||||
```
|
||||
|
||||
### 3. Install the latest version of .NET SDK 9
|
||||
```bash
|
||||
# Install .NET SDK 9
|
||||
|
||||
# On MacOS via brew:
|
||||
brew install --cask dotnet-sdk
|
||||
|
||||
# On Windows via winget
|
||||
winget install Microsoft.DotNet.SDK.9
|
||||
```
|
||||
|
||||
### 4. Install dotnet CLI EF Tools
|
||||
```bash
|
||||
# Install dotnet EF tools globally
|
||||
dotnet tool install --global dotnet-ef
|
||||
# Include dotnet tools in your PATH
|
||||
nano ~/.zshrc
|
||||
# Add the following line to your .zshrc file
|
||||
export PATH="$PATH:$HOME/.dotnet/tools"
|
||||
# Start a new terminal and test that this command works:
|
||||
dotnet ef
|
||||
```
|
||||
|
||||
### 5. Install dev database
|
||||
AliasVault uses PostgreSQL as its database. In order to run the project locally from Visual Studio / Rider you will need to install the dev database. You can do this by running the following command. This will start a separate PostgreSQL instance on port 5433 accessible via the `localhost:5433` address.
|
||||
|
||||
```bash
|
||||
./install.sh configure-dev-db
|
||||
```
|
||||
|
||||
After the database is running you can start the project from Visual Studio / Rider in run or debug mode and it should be able to connect to the dev database.
|
||||
|
||||
### 6. Run Tailwind CSS compiler when changing HTML files to update compiled CSS
|
||||
```bash
|
||||
# For Admin project (in the admin project directory)
|
||||
npm run build:admin-css
|
||||
# For Client project (in the client project directory)
|
||||
npm run build:client-css
|
||||
```
|
||||
|
||||
### 7. Install Playwright in order to locally run NUnit E2E (end-to-end) tests
|
||||
```bash
|
||||
# First install PowerShell for Mac (if you don't have it already)
|
||||
brew install powershell/tap/powershell
|
||||
# Install Playwright
|
||||
dotnet tool install --global Microsoft.Playwright.CLI
|
||||
# Run Playwright install script to download local browsers
|
||||
# Note: make sure the E2E test project has been built at least once so the bin dir exists.
|
||||
pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install
|
||||
```
|
||||
|
||||
### 8. Create AliasVault.Client appsettings.Development.json
|
||||
The WASM client app supports a development specific appsettings.json file. This appsettings file is optional but can override various options to make debugging easier.
|
||||
|
||||
1. Copy `wwwroot/appsettings.json` to `wwwroot/appsettings.Development.json`
|
||||
|
||||
Here is an example file with the various options explained:
|
||||
|
||||
```json
|
||||
{
|
||||
"ApiUrl": "http://localhost:5092",
|
||||
"PrivateEmailDomains": ["example.tld"],
|
||||
"SupportEmail": "support@example.tld",
|
||||
"UseDebugEncryptionKey": "true",
|
||||
"CryptographyOverrideType" : "Argon2Id",
|
||||
"CryptographyOverrideSettings" : "{\"DegreeOfParallelism\":1,\"MemorySize\":1024,\"Iterations\":1}"
|
||||
}
|
||||
```
|
||||
|
||||
- **UseDebugEncryptionKey**
|
||||
- This setting will use a static encryption key so that if you login as a user you can refresh the page without needing to unlock the database again. This speeds up development when changing things in the WebApp WASM project. Note: the project needs to be run in "Development" mode for this setting to be used.
|
||||
|
||||
- **CryptographyOverrideType**
|
||||
- This setting allows overriding the default encryption type (Argon2id) with a different encryption type. This is useful for testing different encryption types without having to change code.
|
||||
|
||||
- **CryptographyOverrideSettings**
|
||||
- This setting allows overriding the default encryption settings (Argon2id) with different settings. This is useful for testing different encryption settings without having to change code. The default Argon2id settings are defined in the project as `Utilities/Cryptography/Cryptography.Client/Defaults.cs`. These default settings are focused on security but NOT performance. Normally for key derivation purposes the slower/heavier the algorithm the better protection against attackers. For production builds this is what we want, however in case of automated testing or debugging extra performance can be gained by tweaking (lowering) these settings.
|
||||
```
|
||||
@@ -1,21 +1,51 @@
|
||||
---
|
||||
layout: default
|
||||
title: PostgreSQL Commands
|
||||
title: Database operations
|
||||
parent: Development
|
||||
grand_parent: Miscellaneous
|
||||
nav_order: 2
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# PostgreSQL Commands
|
||||
# Database operations
|
||||
This article contains tips for how to work with the AliasVault PostgreSQL database in both production and development environments.
|
||||
|
||||
## Backup database to file
|
||||
## Using install.sh helper methods (recommended)
|
||||
The `install.sh` script contains helper methods that makes it easy to export and import databases with a simple single command.
|
||||
|
||||
|
||||
### Export database
|
||||
```bash
|
||||
# Export from normal database container (port 5432, production)
|
||||
./install.sh db-export > aliasvault-db-export.sql.gz
|
||||
|
||||
# Export from dev database container (port 5433, development)
|
||||
./install.sh db-export --dev > aliasvault-db-export.sql.gz
|
||||
```
|
||||
|
||||
### Import database
|
||||
```bash
|
||||
# Import to normal database container (port 5432, production)
|
||||
./install.sh db-import < aliasvault-db-export.sql.gz
|
||||
|
||||
# Import to dev database container (port 5433, development)
|
||||
./install.sh db-import --dev < aliasvault-db-export.sql.gz
|
||||
```
|
||||
|
||||
> Tip: you can also use the optional parameters `--yes` (to skip confirmation prompt) and `--verbose` (to get more output on what the operation is doing).
|
||||
|
||||
---
|
||||
|
||||
## Using docker commands
|
||||
Instead of using the `install.sh script, you can also use manual Docker commands.
|
||||
|
||||
### Backup database to file
|
||||
To backup the database to a file, you can use the following command:
|
||||
|
||||
```bash
|
||||
docker compose exec postgres pg_dump -U aliasvault aliasvault | gzip > aliasvault.sql.gz
|
||||
```
|
||||
|
||||
## Import database from file
|
||||
### Import database from file
|
||||
To drop the existing database and restore the database from a file, you can use the following command:
|
||||
|
||||
{: .warning }
|
||||
@@ -27,7 +57,7 @@ docker compose exec postgres psql -U aliasvault postgres -c "CREATE DATABASE ali
|
||||
gunzip < aliasvault.sql.gz | docker compose exec -iT postgres psql -U aliasvault aliasvault
|
||||
```
|
||||
|
||||
## Change master password
|
||||
### Change master password
|
||||
By default during initial installation the PostgreSQL master password is set to a random string that is
|
||||
stored in the `.env` file with the `POSTGRES_PASSWORD` variable.
|
||||
|
||||
@@ -2,5 +2,23 @@
|
||||
layout: default
|
||||
title: Development
|
||||
parent: Miscellaneous
|
||||
nav_order: 2
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Development Guide
|
||||
|
||||
Choose your platform to get started with AliasVault development:
|
||||
|
||||
## Platform-Specific Dev Guides
|
||||
|
||||
- [Linux/MacOS Development Setup](linux-macos-development.md)
|
||||
- [Windows Development Setup](windows-development.md)
|
||||
|
||||
## Common Development Topics
|
||||
|
||||
- [Browser extensions](browser-extensions.md)
|
||||
- [Database operations](database-operations.md)
|
||||
- [Running GitHub Actions Locally](run-github-actions-locally.md)
|
||||
- [Upgrading EF Server Model](upgrade-ef-server-model.md)
|
||||
- [Upgrading EF Client Model](upgrade-ef-client-model.md)
|
||||
- [Enabling WebAuthn PFR in Chrome](enable-webauthn-pfr-chrome.md)
|
||||
|
||||
157
docs/misc/dev/linux-macos-development.md
Normal file
157
docs/misc/dev/linux-macos-development.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
layout: default
|
||||
title: Linux/MacOS development
|
||||
parent: Development
|
||||
grand_parent: Miscellaneous
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Setting Up AliasVault Development Environment on Linux/MacOS
|
||||
|
||||
This guide will help you set up AliasVault for development on Linux or MacOS systems.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Install .NET 9 SDK**
|
||||
```bash
|
||||
# On MacOS via brew:
|
||||
brew install --cask dotnet-sdk
|
||||
|
||||
# On Linux:
|
||||
# Follow instructions at https://dotnet.microsoft.com/download/dotnet/9.0
|
||||
```
|
||||
|
||||
2. **Install Docker**
|
||||
- Follow instructions at [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||
- For Linux, you can also use the native Docker daemon
|
||||
|
||||
## Setup Steps
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://github.com/lanedirt/AliasVault.git
|
||||
cd AliasVault
|
||||
```
|
||||
|
||||
2. **Copy pre-commit hook script**
|
||||
{: .note }
|
||||
All commits in this repo are required to contain a reference to a GitHub issue in the format of "your commit message (#123)" where "123" references the GitHub issue number.
|
||||
|
||||
```bash
|
||||
# Copy the commit-msg hook script
|
||||
cp .github/hooks/commit-msg .git/hooks/commit-msg
|
||||
chmod +x .git/hooks/commit-msg
|
||||
```
|
||||
|
||||
3. **Install dotnet CLI EF Tools**
|
||||
```bash
|
||||
# Install dotnet EF tools globally
|
||||
dotnet tool install --global dotnet-ef
|
||||
|
||||
# Add to your shell's PATH (if not already done)
|
||||
# For bash/zsh, add to ~/.bashrc or ~/.zshrc:
|
||||
export PATH="$PATH:$HOME/.dotnet/tools"
|
||||
|
||||
# Verify installation
|
||||
dotnet ef
|
||||
```
|
||||
|
||||
4. **Install dev database**
|
||||
```bash
|
||||
./install.sh configure-dev-db
|
||||
```
|
||||
|
||||
5. **Run Tailwind CSS compiler**
|
||||
```bash
|
||||
# For Admin project
|
||||
cd src/AliasVault.Admin
|
||||
npm run build:admin-css
|
||||
|
||||
# For Client project
|
||||
cd src/AliasVault.Client
|
||||
npm run build:client-css
|
||||
```
|
||||
|
||||
6. **Install Playwright for E2E tests**
|
||||
```bash
|
||||
# Install Playwright CLI
|
||||
dotnet tool install --global Microsoft.Playwright.CLI
|
||||
|
||||
# Install browsers
|
||||
pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install
|
||||
```
|
||||
|
||||
7. **Configure Development Settings**
|
||||
Create `wwwroot/appsettings.Development.json` in the Client project:
|
||||
```json
|
||||
{
|
||||
"ApiUrl": "http://localhost:5092",
|
||||
"PrivateEmailDomains": ["example.tld"],
|
||||
"SupportEmail": "support@example.tld",
|
||||
"UseDebugEncryptionKey": "true",
|
||||
"CryptographyOverrideType": "Argon2Id",
|
||||
"CryptographyOverrideSettings": "{\"DegreeOfParallelism\":1,\"MemorySize\":1024,\"Iterations\":1}"
|
||||
}
|
||||
```
|
||||
|
||||
## Running the Application
|
||||
|
||||
1. **Start the Development Database**
|
||||
```bash
|
||||
./install.sh configure-dev-db
|
||||
```
|
||||
|
||||
2. **Run the Application**
|
||||
```bash
|
||||
# Using dotnet CLI
|
||||
cd src/AliasVault.Api
|
||||
dotnet run
|
||||
|
||||
# Or using your preferred IDE (VS Code, Rider, etc.)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database Issues
|
||||
If you encounter database connection issues:
|
||||
|
||||
1. **Check Database Status**
|
||||
```bash
|
||||
docker ps | grep postgres-dev
|
||||
```
|
||||
|
||||
2. **Check Logs**
|
||||
```bash
|
||||
docker logs aliasvault-dev-postgres-dev-1
|
||||
```
|
||||
|
||||
3. **Restart Database**
|
||||
```bash
|
||||
./install.sh configure-dev-db
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Permission Issues**
|
||||
```bash
|
||||
# Fix script permissions
|
||||
chmod +x install.sh
|
||||
```
|
||||
|
||||
2. **Port Conflicts**
|
||||
- Check if port 5433 is available for the development database
|
||||
- Check if port 5092 is available for the API
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- Keep your .NET SDK and Docker up to date
|
||||
- The development database runs on port 5433 to avoid conflicts
|
||||
- Use the debug encryption key in development for easier testing
|
||||
- Store sensitive data in environment variables or user secrets
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues not covered in this guide, please:
|
||||
1. Check the [GitHub Issues](https://github.com/lanedirt/AliasVault/issues)
|
||||
2. Search for existing solutions
|
||||
3. Create a new issue if needed
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
layout: default
|
||||
title: Run GitHub Actions Locally
|
||||
title: Run GitHub actions locally
|
||||
parent: Development
|
||||
grand_parent: Miscellaneous
|
||||
nav_order: 9
|
||||
|
||||
152
docs/misc/dev/windows-development.md
Normal file
152
docs/misc/dev/windows-development.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
layout: default
|
||||
title: Windows development
|
||||
parent: Development
|
||||
grand_parent: Miscellaneous
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
|
||||
# Setting Up AliasVault Development Environment on Windows
|
||||
|
||||
This guide will help you set up AliasVault for development on Windows using WSL (Windows Subsystem for Linux).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Install WSL**
|
||||
- Open PowerShell as Administrator and run:
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
- This will install Ubuntu by default
|
||||
- Restart your computer after installation
|
||||
|
||||
2. **Install Visual Studio 2022**
|
||||
- Download from [Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/)
|
||||
- Required Workloads:
|
||||
- ASP.NET and web development
|
||||
- .NET WebAssembly development tools
|
||||
- .NET cross-platform development
|
||||
|
||||
3. **Install .NET 9 SDK**
|
||||
- Download from [.NET Downloads](https://dotnet.microsoft.com/download/dotnet/9.0)
|
||||
- Install both Windows and Linux versions (you'll need both)
|
||||
|
||||
## Setup Steps
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://github.com/lanedirt/AliasVault.git
|
||||
cd AliasVault
|
||||
```
|
||||
|
||||
2. **Copy pre-commit hook script**
|
||||
{: .note }
|
||||
All commits in this repo are required to contain a reference to a GitHub issue in the format of "your commit message (#123)" where "123" references the GitHub issue number.
|
||||
|
||||
```bash
|
||||
# Copy the commit-msg hook script
|
||||
cp .github/hooks/commit-msg .git/hooks/commit-msg
|
||||
chmod +x .git/hooks/commit-msg
|
||||
```
|
||||
|
||||
3. **Configure WSL**
|
||||
- Open WSL terminal
|
||||
- Edit WSL configuration:
|
||||
```bash
|
||||
sudo nano /etc/wsl.conf
|
||||
```
|
||||
- Add the following configuration:
|
||||
```ini
|
||||
[automount]
|
||||
enabled = true
|
||||
options = "metadata,umask=22,fmask=11"
|
||||
mountFsTab = false
|
||||
|
||||
[boot]
|
||||
systemd=true
|
||||
```
|
||||
- Save the file (Ctrl+X, then Y)
|
||||
- Restart WSL from PowerShell:
|
||||
```powershell
|
||||
wsl --shutdown
|
||||
```
|
||||
|
||||
4. **Setup Development Database**
|
||||
- Open a new WSL terminal in the AliasVault directory
|
||||
- Run the development database setup:
|
||||
```bash
|
||||
./install.sh configure-dev-db
|
||||
```
|
||||
- Select option 1 to start the development database
|
||||
- Verify the database is running:
|
||||
```bash
|
||||
docker ps | grep postgres-dev
|
||||
```
|
||||
|
||||
5. **Run the Application**
|
||||
- Open the solution in Visual Studio 2022
|
||||
- Set WebApi as the startup project
|
||||
- Press F5 to run in debug mode
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database Connection Issues
|
||||
If the WebApi fails to start due to database connection issues:
|
||||
|
||||
1. **Check Database Status**
|
||||
```bash
|
||||
docker ps | grep postgres-dev
|
||||
```
|
||||
|
||||
2. **Check Database Logs**
|
||||
```bash
|
||||
docker logs aliasvault-dev-postgres-dev-1
|
||||
```
|
||||
|
||||
3. **Permission Issues**
|
||||
If you see permission errors, try:
|
||||
```bash
|
||||
sudo mkdir -p ./database/postgres
|
||||
sudo chown -R 999:999 ./database/postgres
|
||||
sudo chmod -R 700 ./database/postgres
|
||||
```
|
||||
|
||||
4. **Restart Development Database**
|
||||
```bash
|
||||
./install.sh configure-dev-db
|
||||
# Select option 2 to stop, then option 1 to start again
|
||||
```
|
||||
|
||||
### WSL Issues
|
||||
If you experience WSL-related issues:
|
||||
|
||||
1. Make sure you have the latest WSL version:
|
||||
```powershell
|
||||
wsl --update
|
||||
```
|
||||
|
||||
2. Verify WSL is running correctly:
|
||||
```powershell
|
||||
wsl --status
|
||||
```
|
||||
|
||||
3. If problems persist, try resetting WSL:
|
||||
```powershell
|
||||
wsl --shutdown
|
||||
wsl
|
||||
```
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- Always run the development database before starting the WebApi project
|
||||
- Make sure you're using the correct .NET SDK version in both Windows and WSL
|
||||
- If you modify the WSL configuration, always restart WSL afterward
|
||||
- For best performance, store the project files in the Linux filesystem rather than the Windows filesystem
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter any issues not covered in this guide, please:
|
||||
1. Check the [GitHub Issues](https://github.com/lanedirt/AliasVault/issues)
|
||||
2. Search for existing solutions
|
||||
3. Create a new issue if needed
|
||||
@@ -14,7 +14,6 @@ Follow the steps in the checklist below to prepare a new release.
|
||||
- [ ] Update ./src/Shared/AliasVault.Shared.Core/AppInfo.cs with the minimum supported client versions.
|
||||
- In case API output breaks earlier client versions and/or this version of the client/API will upgrade the client vault model to a new major version.
|
||||
- [ ] Update ./install.sh `@version` in header if the install script has changed. This allows the install script to self-update when running the `./install.sh update` command on default installations.
|
||||
- [ ] Update README.md install.sh download link to point to the new release version
|
||||
|
||||
## Versioning browser extension
|
||||
- [ ] Update ./browser-extension/wxt.config.ts with the new version for the extension. This will be shown in the browser extension web stores. This version should be equal to the git release tag.
|
||||
@@ -25,7 +24,7 @@ Follow the steps in the checklist below to prepare a new release.
|
||||
|
||||
## Docker Images
|
||||
If docker containers have been added or removed:
|
||||
- [ ] Verify that `.github/workflows/publish-docker-images.yml` contains references to all docker images that need to be published.
|
||||
- [ ] Verify that `.github/workflows/release.yml` contains references to all docker images that need to be published.
|
||||
- [ ] Update `install.sh` and verify that the `images=()` array that takes care of pulling the images from the GitHub Container Registry is updated.
|
||||
|
||||
## Manual Testing (since v0.10.0+)
|
||||
|
||||
132
install.sh
132
install.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# @version 0.12.2
|
||||
# @version 0.15.1
|
||||
|
||||
# Repository information used for downloading files and images from GitHub
|
||||
REPO_OWNER="lanedirt"
|
||||
@@ -38,26 +38,27 @@ show_usage() {
|
||||
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 " update-installer Check and update install.sh script if newer version available\n"
|
||||
printf " build Build AliasVault containers locally from source (takes longer and requires sufficient specs)\n"
|
||||
printf " start Start AliasVault containers\n"
|
||||
printf " restart Restart AliasVault containers\n"
|
||||
printf " stop Stop AliasVault containers\n"
|
||||
printf "\n"
|
||||
printf " configure-hostname Configure the hostname where AliasVault can be accessed from\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 " configure-registration Configure new account registration (enable or disable)\n"
|
||||
printf " configure-ip-logging Configure IP address logging (enable or disable)\n"
|
||||
printf " start Start AliasVault containers using remote images\n"
|
||||
printf " stop Stop AliasVault containers using remote images\n"
|
||||
printf " restart Restart AliasVault containers using remote images\n"
|
||||
printf " reset-password Reset admin password\n"
|
||||
printf " build [operation] Build AliasVault from source (takes longer and requires sufficient specs)\n"
|
||||
printf " Optional operations: start|stop|restart (uses locally built images)\n"
|
||||
printf " reset-admin-password Reset admin password\n"
|
||||
printf " uninstall Uninstall AliasVault\n"
|
||||
printf "\n"
|
||||
printf " update Update AliasVault including install.sh script to the latest version\n"
|
||||
printf " update-installer Update install.sh script if newer version is available\n"
|
||||
printf "\n"
|
||||
printf " db-export Export database to file\n"
|
||||
printf " db-import Import database from file\n"
|
||||
printf "\n"
|
||||
printf " configure-dev-db Enable/disable development database (for local development only)\n"
|
||||
printf " migrate-db Migrate data from SQLite to PostgreSQL when upgrading from a version prior to 0.10.0\n"
|
||||
printf " migrate-db Migrate data from SQLite to PostgreSQL (only when upgrading from a version prior to 0.10.0)\n"
|
||||
printf "\n"
|
||||
printf "Options:\n"
|
||||
printf " --verbose Show detailed output\n"
|
||||
@@ -115,7 +116,7 @@ parse_args() {
|
||||
shift
|
||||
;;
|
||||
reset-password|reset-admin-password|rp)
|
||||
COMMAND="reset-password"
|
||||
COMMAND="reset-admin-password"
|
||||
shift
|
||||
;;
|
||||
configure-hostname|hostname)
|
||||
@@ -235,7 +236,7 @@ main() {
|
||||
"uninstall")
|
||||
handle_uninstall
|
||||
;;
|
||||
"reset-password")
|
||||
"reset-admin-password")
|
||||
generate_admin_password
|
||||
if [ $? -eq 0 ]; then
|
||||
printf "${CYAN}> Restarting admin container...${NC}\n"
|
||||
@@ -293,6 +294,16 @@ main() {
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to get the latest release version from GitHub
|
||||
get_latest_version() {
|
||||
local latest_version=$(curl -s "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
|
||||
if [ -z "$latest_version" ]; then
|
||||
printf "${RED}> Failed to get latest version from GitHub.${NC}\n" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$latest_version"
|
||||
}
|
||||
|
||||
# Function to create required directories
|
||||
create_directories() {
|
||||
printf "${CYAN}> Checking workspace...${NC}\n"
|
||||
@@ -410,13 +421,26 @@ print_logo() {
|
||||
create_env_file() {
|
||||
printf "${CYAN}> Checking .env file...${NC}\n"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$ENV_EXAMPLE_FILE" ]; then
|
||||
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
|
||||
printf " ${GREEN}> New.env file created from .env.example.${NC}\n"
|
||||
else
|
||||
touch "$ENV_FILE"
|
||||
printf " ${YELLOW}> New blank .env file created.${NC}\n"
|
||||
if [ ! -f "$ENV_EXAMPLE_FILE" ]; then
|
||||
# Get latest release version
|
||||
local latest_version=$(get_latest_version) || {
|
||||
printf " ${YELLOW}> Failed to check latest version. Creating blank .env file.${NC}\n"
|
||||
touch "$ENV_FILE"
|
||||
return 0
|
||||
}
|
||||
|
||||
printf " ${CYAN}> Downloading .env.example...${NC}"
|
||||
if curl -sSf "${GITHUB_RAW_URL_REPO}/${latest_version}/.env.example" -o "$ENV_EXAMPLE_FILE" > /dev/null 2>&1; then
|
||||
printf "\n ${GREEN}> .env.example downloaded successfully.${NC}\n"
|
||||
else
|
||||
printf "\n ${YELLOW}> Failed to download .env.example. Creating blank .env file.${NC}\n"
|
||||
touch "$ENV_FILE"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
|
||||
printf " ${GREEN}> New .env file created from .env.example.${NC}\n"
|
||||
else
|
||||
printf " ${GREEN}> .env file already exists.${NC}\n"
|
||||
fi
|
||||
@@ -615,10 +639,19 @@ update_env_var() {
|
||||
local value=$2
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
sed -i.bak "/^${key}=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
|
||||
# Check if key exists
|
||||
if grep -q "^${key}=" "$ENV_FILE"; then
|
||||
# Update existing key inline
|
||||
sed -i.bak "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
|
||||
else
|
||||
# Key doesn't exist, append it
|
||||
echo "$key=$value" >> "$ENV_FILE"
|
||||
fi
|
||||
else
|
||||
# File doesn't exist, create it with the key-value pair
|
||||
echo "$key=$value" > "$ENV_FILE"
|
||||
fi
|
||||
|
||||
echo "$key=$value" >> "$ENV_FILE"
|
||||
printf " ${GREEN}> $key has been set in $ENV_FILE.${NC}\n"
|
||||
}
|
||||
|
||||
@@ -651,7 +684,7 @@ print_success_message() {
|
||||
else
|
||||
printf "Admin Panel: https://localhost/admin\n"
|
||||
printf "Username: admin\n"
|
||||
printf "Password: (Previously set. Use ./install.sh reset-password to generate new one.)\n"
|
||||
printf "Password: (Previously set. Use ./install.sh reset-admin-password to generate new one.)\n"
|
||||
fi
|
||||
printf "\n"
|
||||
printf "${CYAN}===========================${NC}\n"
|
||||
@@ -814,6 +847,8 @@ handle_install() {
|
||||
# Function to handle build
|
||||
handle_build() {
|
||||
printf "${YELLOW}+++ Building AliasVault from source +++${NC}\n"
|
||||
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
|
||||
|
||||
# Set deployment mode to build to ensure container lifecycle uses build configuration
|
||||
set_deployment_mode "build"
|
||||
printf "\n"
|
||||
@@ -837,7 +872,6 @@ handle_build() {
|
||||
fi
|
||||
|
||||
# Initialize environment with proper error handling
|
||||
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
|
||||
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
|
||||
populate_jwt_key || { printf "${RED}> Failed to set JWT key${NC}\n"; exit 1; }
|
||||
populate_data_protection_cert_pass || { printf "${RED}> Failed to set certificate password${NC}\n"; exit 1; }
|
||||
@@ -1330,7 +1364,10 @@ handle_update() {
|
||||
fi
|
||||
|
||||
current_version=$(grep "^ALIASVAULT_VERSION=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
latest_version=$(curl -s "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
latest_version=$(get_latest_version) || {
|
||||
printf "${RED}> Failed to check for updates. Please try again later.${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "$latest_version" ]; then
|
||||
printf "${RED}> Failed to check for updates. Please try again later.${NC}\n"
|
||||
@@ -1412,7 +1449,10 @@ check_install_script_update() {
|
||||
printf "${CYAN}> Checking for install script updates...${NC}\n"
|
||||
|
||||
# Get latest release version
|
||||
local latest_version=$(curl -s "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
local latest_version=$(get_latest_version) || {
|
||||
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ -z "$latest_version" ]; then
|
||||
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
|
||||
@@ -1505,13 +1545,18 @@ handle_install_version() {
|
||||
|
||||
# If latest, get actual version number from GitHub API
|
||||
if [ "$target_version" = "latest" ]; then
|
||||
local actual_version=$(curl -s "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
local actual_version=$(get_latest_version) || {
|
||||
printf "${RED}> Failed to get latest version. Please try again later.${NC}\n"
|
||||
exit 1
|
||||
}
|
||||
if [ -n "$actual_version" ]; then
|
||||
target_version="$actual_version"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "${YELLOW}+++ Installing AliasVault ${target_version} +++${NC}\n"
|
||||
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
|
||||
|
||||
# Set deployment mode to install to ensure container lifecycle uses install configuration
|
||||
set_deployment_mode "install"
|
||||
printf "\n"
|
||||
@@ -1544,7 +1589,7 @@ handle_install_version() {
|
||||
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 same install command again to continue with the installation.${NC}\n"
|
||||
exit 0
|
||||
exit 2
|
||||
else
|
||||
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
|
||||
mv "install.sh.backup" "install.sh"
|
||||
@@ -1557,7 +1602,6 @@ handle_install_version() {
|
||||
handle_docker_compose "$target_version"
|
||||
|
||||
# Initialize environment
|
||||
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
|
||||
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
|
||||
populate_jwt_key || { printf "${RED}> Failed to set JWT key${NC}\n"; exit 1; }
|
||||
populate_data_protection_cert_pass || { printf "${RED}> Failed to set certificate password${NC}\n"; exit 1; }
|
||||
@@ -2032,25 +2076,27 @@ handle_hostname_configuration() {
|
||||
|
||||
# Get current hostname
|
||||
CURRENT_HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
|
||||
printf "${CYAN}Removing current hostname ${CURRENT_HOSTNAME}${NC}...\n"
|
||||
printf "Current hostname: ${CYAN}${CURRENT_HOSTNAME}${NC}\n"
|
||||
printf "\n"
|
||||
|
||||
# Force hostname to be empty so populate_hostname will ask for a new one
|
||||
sed -i.bak "/^HOSTNAME=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
|
||||
# Ask for new hostname
|
||||
while true; do
|
||||
read -p "Enter new hostname (e.g. aliasvault.net): " NEW_HOSTNAME
|
||||
if [ -n "$NEW_HOSTNAME" ]; then
|
||||
break
|
||||
else
|
||||
printf "${YELLOW}> Hostname cannot be empty. Please enter a valid hostname.${NC}\n"
|
||||
fi
|
||||
done
|
||||
|
||||
# Reuse existing hostname population logic
|
||||
populate_hostname
|
||||
# Update the hostname
|
||||
update_env_var "HOSTNAME" "$NEW_HOSTNAME"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
printf "New hostname: ${CYAN}${HOSTNAME}${NC}\n"
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
else
|
||||
printf "${RED}> Failed to update hostname. Please try again.${NC}\n"
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
exit 1
|
||||
fi
|
||||
printf "\n"
|
||||
printf "${GREEN}Hostname updated successfully!${NC}\n"
|
||||
printf "New hostname: ${CYAN}${NEW_HOSTNAME}${NC}\n"
|
||||
printf "\n"
|
||||
printf "${MAGENTA}=========================================================${NC}\n"
|
||||
}
|
||||
|
||||
# Function to handle IP logging configuration
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
|
||||
<PackageReference Include="Blazor-ApexCharts" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -32,6 +32,11 @@ public class UserEmailClaimWithCount
|
||||
/// </summary>
|
||||
public string AddressDomain { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the email claim is disabled.
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the created at timestamp.
|
||||
/// </summary>
|
||||
|
||||
@@ -42,6 +42,11 @@ public class UserViewModel
|
||||
/// </summary>
|
||||
public int VaultCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the credential count.
|
||||
/// </summary>
|
||||
public int CredentialCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email claim count.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
<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">Total 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>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
This card shows the number of active users in the last 24 hours, 7 days, and 14 days. This includes users who have created their accounts in these time periods.
|
||||
</p>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
@@ -21,63 +11,31 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 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 class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last24Hours</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users">(@UserStats.ReturningLast24Hours)</span>
|
||||
</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 3 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last3Days</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last3DayUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<div class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last3Days</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users">(@UserStats.ReturningLast3Days)</span>
|
||||
</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 class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last7Days</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users">(@UserStats.ReturningLast7Days)</span>
|
||||
</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>
|
||||
}
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<div class="flex items-baseline gap-2">
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last30Days</h4>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400" title="Returning users (activity 24h after registration)">(@UserStats.ReturningLast30Days)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -86,7 +44,6 @@
|
||||
@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.
|
||||
@@ -100,56 +57,56 @@
|
||||
var last24Hours = now.AddHours(-24);
|
||||
var last3Days = now.AddDays(-3);
|
||||
var last7Days = now.AddDays(-7);
|
||||
var last14Days = now.AddDays(-14);
|
||||
var last30Days = now.AddDays(-30);
|
||||
|
||||
// Get user statistics
|
||||
var (count24h, users24h) = await GetActiveUserCount(last24Hours);
|
||||
var (count3d, users3d) = await GetActiveUserCount(last3Days);
|
||||
var (count7d, users7d) = await GetActiveUserCount(last7Days);
|
||||
var (count14d, users14d) = await GetActiveUserCount(last14Days);
|
||||
var (count24h, returning24h) = await GetActiveUserCount(last24Hours);
|
||||
var (count3d, returning3d) = await GetActiveUserCount(last3Days);
|
||||
var (count7d, returning7d) = await GetActiveUserCount(last7Days);
|
||||
var (count30d, returning30d) = await GetActiveUserCount(last30Days);
|
||||
|
||||
UserStats = new UserStatistics
|
||||
{
|
||||
Last24Hours = count24h,
|
||||
Last3Days = count3d,
|
||||
Last7Days = count7d,
|
||||
Last14Days = count14d,
|
||||
Last24HourUsers = users24h,
|
||||
Last3DayUsers = users3d,
|
||||
Last7DayUsers = users7d,
|
||||
Last14DayUsers = users14d
|
||||
Last30Days = count30d,
|
||||
ReturningLast24Hours = returning24h,
|
||||
ReturningLast3Days = returning3d,
|
||||
ReturningLast7Days = returning7d,
|
||||
ReturningLast30Days = returning30d,
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task<(int count, List<string> users)> GetActiveUserCount(DateTime since)
|
||||
private async Task<(int totalCount, int returningCount)> GetActiveUserCount(DateTime since)
|
||||
{
|
||||
// Get unique users who either:
|
||||
// 1. Have successful auth logs
|
||||
// 2. Have updated their vault
|
||||
// 3. Are not the admin user
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Get all active users for the period
|
||||
var activeUsers = await dbContext.AuthLogs
|
||||
.Where(l => l.Timestamp >= since && l.IsSuccess && l.Username != "admin")
|
||||
.Select(l => l.Username)
|
||||
.Union(
|
||||
dbContext.Vaults
|
||||
.Where(v => v.UpdatedAt >= since)
|
||||
.Select(v => v.User.UserName!)
|
||||
)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
return (activeUsers.Count, activeUsers);
|
||||
}
|
||||
// Get returning users (those who have activity at least 24h after registration
|
||||
var returningUsers = await dbContext.AuthLogs
|
||||
.Where(l => l.Timestamp >= since && l.IsSuccess && l.Username != "admin")
|
||||
.Join(
|
||||
dbContext.AliasVaultUsers,
|
||||
log => log.Username,
|
||||
user => user.UserName,
|
||||
(log, user) => new { log, user }
|
||||
)
|
||||
.Where(x => x.log.Timestamp >= x.user.CreatedAt.AddHours(24))
|
||||
.Select(x => x.log.Username)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
private void ToggleUserNames()
|
||||
{
|
||||
ShowUserNames = !ShowUserNames;
|
||||
StateHasChanged();
|
||||
return (activeUsers.Count, returningUsers.Count);
|
||||
}
|
||||
|
||||
private sealed class UserStatistics
|
||||
@@ -157,10 +114,10 @@
|
||||
public int Last24Hours { get; set; }
|
||||
public int Last3Days { get; set; }
|
||||
public int Last7Days { get; set; }
|
||||
public int Last14Days { get; set; }
|
||||
public List<string> Last24HourUsers { get; set; } = new();
|
||||
public List<string> Last3DayUsers { get; set; } = new();
|
||||
public List<string> Last7DayUsers { get; set; } = new();
|
||||
public List<string> Last14DayUsers { get; set; } = new();
|
||||
public int Last30Days { get; set; }
|
||||
public int ReturningLast24Hours { get; set; }
|
||||
public int ReturningLast3Days { get; set; }
|
||||
public int ReturningLast7Days { get; set; }
|
||||
public int ReturningLast30Days { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
@rendermode InteractiveServer
|
||||
@using AliasVault.Shared.Server.Models
|
||||
@using AliasVault.Shared.Server.Services
|
||||
@inject ServerSettingsService SettingsService
|
||||
|
||||
<div class="col-span-2 p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800 max-h-[500px]">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<ApexChart TItem="DailyUserCount"
|
||||
Title="@($"User activity - last {DaysToShow} days")"
|
||||
Height="400">
|
||||
<ApexPointSeries TItem="DailyUserCount"
|
||||
Items="TotalDailyUserCounts"
|
||||
SeriesType="@SeriesType.Area"
|
||||
Name="Total Active Users"
|
||||
XValue="@(e => e.Date.ToString("MM-dd"))"
|
||||
YValue="@(e => e.Count)" />
|
||||
<ApexPointSeries TItem="DailyUserCount"
|
||||
Items="DailyUserCounts"
|
||||
SeriesType="@SeriesType.Area"
|
||||
Name="Returning Users"
|
||||
XValue="@(e => e.Date.ToString("MM-dd"))"
|
||||
YValue="@(e => e.Count)" />
|
||||
|
||||
<ApexPointSeries TItem="DailyUserCount"
|
||||
Items="NewUserRegistrations"
|
||||
SeriesType="@SeriesType.Area"
|
||||
Name="New Registrations"
|
||||
XValue="@(e => e.Date.ToString("MM-dd"))"
|
||||
YValue="@(e => e.Count)" />
|
||||
</ApexChart>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private List<DailyUserCount> DailyUserCounts = new();
|
||||
private List<DailyUserCount> TotalDailyUserCounts = new();
|
||||
private List<DailyUserCount> NewUserRegistrations = new();
|
||||
private int DaysToShow { get; set; } = 30;
|
||||
private ServerSettingsModel Settings { get; set; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Settings = await SettingsService.GetAllSettingsAsync();
|
||||
|
||||
// Set the number of days to show to the auth log retention days up to a maximum of 60 days (for performance reasons).
|
||||
int maxDays = Math.Min(Settings.AuthLogRetentionDays, 60);
|
||||
|
||||
// If the auth log retention days is 0 (unlimited), set the number of days to show to 60.
|
||||
DaysToShow = maxDays == 0 ? 60 : maxDays;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the card.
|
||||
/// </summary>
|
||||
public async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Get daily active user counts for the past 14 days
|
||||
await GetDailyActiveUserCounts();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get daily active user counts for up to the last 90 days to display on the chart.
|
||||
/// </summary>
|
||||
private async Task GetDailyActiveUserCounts()
|
||||
{
|
||||
DailyUserCounts = new List<DailyUserCount>();
|
||||
TotalDailyUserCounts = new List<DailyUserCount>();
|
||||
NewUserRegistrations = new List<DailyUserCount>();
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Define the date range (defaults to amount of days in auth log retention, with a maximum of 90 days)
|
||||
var endDate = DateTime.UtcNow.Date;
|
||||
var startDate = endDate.AddDays(-DaysToShow);
|
||||
|
||||
// Get total active users (all users who logged in based on auth logs)
|
||||
var totalUsersByDay = await dbContext.AuthLogs
|
||||
.Where(l => l.Timestamp >= startDate && l.Timestamp < endDate && l.IsSuccess && l.Username != "admin")
|
||||
.GroupBy(x => x.Timestamp.Date)
|
||||
.Select(g => new { Day = g.Key, Count = g.Select(x => x.Username).Distinct().Count() })
|
||||
.ToListAsync();
|
||||
|
||||
// Get new user registrations by day
|
||||
var newUsersByDay = await dbContext.AliasVaultUsers
|
||||
.Where(u => u.CreatedAt >= startDate && u.CreatedAt < endDate && u.UserName != "admin")
|
||||
.GroupBy(u => u.CreatedAt.Date)
|
||||
.Select(g => new { Day = g.Key, Count = g.Count() })
|
||||
.ToListAsync();
|
||||
|
||||
// Fill in the results for all days
|
||||
for (int i = 0; i < DaysToShow; i++)
|
||||
{
|
||||
// Subtract 1 day to avoid showing the current day as those numbers are not complete yet.
|
||||
var day = endDate.AddDays(-i - 1);
|
||||
|
||||
var totalActiveCount = totalUsersByDay.FirstOrDefault(d => d.Day == day)?.Count ?? 0;
|
||||
var registeredUsersCount = newUsersByDay.FirstOrDefault(d => d.Day == day)?.Count ?? 0;
|
||||
|
||||
// Calculate the number of returning users by subtracting the number of users registered that day from the total active users.
|
||||
var returningUsersCount = totalActiveCount - registeredUsersCount;
|
||||
|
||||
DailyUserCounts.Add(new DailyUserCount
|
||||
{
|
||||
Date = day,
|
||||
Count = returningUsersCount
|
||||
});
|
||||
|
||||
TotalDailyUserCounts.Add(new DailyUserCount
|
||||
{
|
||||
Date = day,
|
||||
Count = totalActiveCount
|
||||
});
|
||||
|
||||
NewUserRegistrations.Add(new DailyUserCount
|
||||
{
|
||||
Date = day,
|
||||
Count = registeredUsersCount
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DailyUserCount
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
<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">Email aliases created</h3>
|
||||
<button
|
||||
@onclick="ToggleChart"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
@(ShowChart ? "Hide chart" : "Show chart")
|
||||
</button>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
@@ -11,27 +16,60 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<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">@EmailClaimsStats.Hours24</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Hours24.ToString("N0")</h4>
|
||||
</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 3 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Days3</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Days3.ToString("N0")</h4>
|
||||
</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">@EmailClaimsStats.Days7</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Days7.ToString("N0")</h4>
|
||||
</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">@EmailClaimsStats.Days14</h4>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailClaimsStats.Days30.ToString("N0")</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowChart && !IsLoading)
|
||||
{
|
||||
<div class="mt-6">
|
||||
<ApexChart TItem="DailyEmailClaimCount"
|
||||
Title="@($"Aliases created - last {DaysToShow} days")"
|
||||
Height="250">
|
||||
<ApexPointSeries TItem="DailyEmailClaimCount"
|
||||
Items="DailyEmailClaimCounts"
|
||||
SeriesType="@SeriesType.Bar"
|
||||
Name="Aliases created"
|
||||
XValue="@(e => e.Date.ToString("MM-dd"))"
|
||||
YValue="@(e => e.Count)" />
|
||||
</ApexChart>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private EmailClaimsStatistics EmailClaimsStats { get; set; } = new();
|
||||
private List<DailyEmailClaimCount> DailyEmailClaimCounts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The number of days to show in the chart.
|
||||
/// </summary>
|
||||
private int DaysToShow { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the chart is visible.
|
||||
/// </summary>
|
||||
private bool ShowChart { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the card.
|
||||
@@ -41,11 +79,23 @@
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await RefreshCardData();
|
||||
await RefreshChartData();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the card data.
|
||||
/// </summary>
|
||||
private async Task RefreshCardData()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var hours24 = now.AddHours(-24);
|
||||
var days3 = now.AddDays(-3);
|
||||
var days7 = now.AddDays(-7);
|
||||
var days14 = now.AddDays(-14);
|
||||
var days30 = now.AddDays(-30);
|
||||
|
||||
// Get email claims statistics
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
@@ -55,11 +105,61 @@
|
||||
Hours24 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= hours24),
|
||||
Days3 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days3),
|
||||
Days7 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days7),
|
||||
Days14 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days14)
|
||||
Days30 = await emailClaimsQuery.CountAsync(e => e.CreatedAt >= days30)
|
||||
};
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
/// <summary>
|
||||
/// Refreshes the chart data.
|
||||
/// </summary>
|
||||
private async Task RefreshChartData()
|
||||
{
|
||||
// Only fetch chart data if the chart is visible
|
||||
if (ShowChart)
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var dateFrom = DateTime.UtcNow.AddDays(-DaysToShow);
|
||||
|
||||
// Get daily email claim counts for the chart
|
||||
DailyEmailClaimCounts = await dbContext.UserEmailClaims
|
||||
.Where(e => e.CreatedAt >= dateFrom)
|
||||
.GroupBy(e => e.CreatedAt.Date)
|
||||
.Select(g => new DailyEmailClaimCount
|
||||
{
|
||||
Date = g.Key,
|
||||
Count = g.Count()
|
||||
}).ToListAsync();
|
||||
|
||||
// Fill in any missing days with zero counts
|
||||
var allDates = Enumerable.Range(0, DaysToShow)
|
||||
.Select(offset => DateTime.UtcNow.Date.AddDays(-offset))
|
||||
.Reverse();
|
||||
|
||||
DailyEmailClaimCounts = allDates
|
||||
.GroupJoin(
|
||||
DailyEmailClaimCounts,
|
||||
date => date,
|
||||
claimCount => claimCount.Date,
|
||||
(date, claimCounts) => claimCounts.FirstOrDefault() ?? new DailyEmailClaimCount { Date = date, Count = 0 }
|
||||
)
|
||||
.OrderByDescending(e => e.Date)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleChart()
|
||||
{
|
||||
ShowChart = !ShowChart;
|
||||
|
||||
// If we're showing the chart but haven't loaded the data yet
|
||||
if (ShowChart && DailyEmailClaimCounts.Count == 0)
|
||||
{
|
||||
_ = RefreshData();
|
||||
}
|
||||
else
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EmailClaimsStatistics
|
||||
@@ -67,6 +167,12 @@
|
||||
public int Hours24 { get; set; }
|
||||
public int Days3 { get; set; }
|
||||
public int Days7 { get; set; }
|
||||
public int Days14 { get; set; }
|
||||
public int Days30 { get; set; }
|
||||
}
|
||||
|
||||
private sealed class DailyEmailClaimCount
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<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">Emails received</h3>
|
||||
<button
|
||||
@onclick="ToggleChart"
|
||||
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
@(ShowChart ? "Hide chart" : "Show chart")
|
||||
</button>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
@@ -11,27 +16,60 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<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.Hours24</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Hours24.ToString("N0")</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 3 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Days3</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Days3.ToString("N0")</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.Days7</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Days7.ToString("N0")</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.Days14</h4>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@EmailStats.Days30.ToString("N0")</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowChart && !IsLoading)
|
||||
{
|
||||
<div class="mt-6">
|
||||
<ApexChart TItem="DailyEmailCount"
|
||||
Title="@($"Emails received - last {DaysToShow} days")"
|
||||
Height="250">
|
||||
<ApexPointSeries TItem="DailyEmailCount"
|
||||
Items="DailyEmailCounts"
|
||||
SeriesType="@SeriesType.Bar"
|
||||
Name="Emails received"
|
||||
XValue="@(e => e.Date.ToString("MM-dd"))"
|
||||
YValue="@(e => e.Count)" />
|
||||
</ApexChart>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private EmailStatistics EmailStats { get; set; } = new();
|
||||
private List<DailyEmailCount> DailyEmailCounts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The number of days to show in the chart.
|
||||
/// </summary>
|
||||
private int DaysToShow { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the chart is visible.
|
||||
/// </summary>
|
||||
private bool ShowChart { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the data displayed on the card.
|
||||
@@ -41,11 +79,23 @@
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await RefreshCardData();
|
||||
await RefreshChartData();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the card data.
|
||||
/// </summary>
|
||||
private async Task RefreshCardData()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var hours24 = now.AddHours(-24);
|
||||
var days3 = now.AddDays(-3);
|
||||
var days7 = now.AddDays(-7);
|
||||
var days14 = now.AddDays(-14);
|
||||
var days30 = now.AddDays(-30);
|
||||
|
||||
// Get email statistics
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
@@ -55,11 +105,61 @@
|
||||
Hours24 = await emailQuery.CountAsync(e => e.DateSystem >= hours24),
|
||||
Days3 = await emailQuery.CountAsync(e => e.DateSystem >= days3),
|
||||
Days7 = await emailQuery.CountAsync(e => e.DateSystem >= days7),
|
||||
Days14 = await emailQuery.CountAsync(e => e.DateSystem >= days14)
|
||||
Days30 = await emailQuery.CountAsync(e => e.DateSystem >= days30)
|
||||
};
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
/// <summary>
|
||||
/// Refreshes the chart data.
|
||||
/// </summary>
|
||||
private async Task RefreshChartData()
|
||||
{
|
||||
// Only fetch chart data if the chart is visible.
|
||||
if (ShowChart)
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var dateFrom = DateTime.UtcNow.AddDays(-DaysToShow);
|
||||
|
||||
// Get daily email counts for the chart.
|
||||
DailyEmailCounts = await dbContext.Emails
|
||||
.Where(e => e.DateSystem >= dateFrom)
|
||||
.GroupBy(e => e.DateSystem.Date)
|
||||
.Select(g => new DailyEmailCount
|
||||
{
|
||||
Date = g.Key,
|
||||
Count = g.Count()
|
||||
}).ToListAsync();
|
||||
|
||||
// Fill in any missing days with zero counts
|
||||
var allDates = Enumerable.Range(0, DaysToShow)
|
||||
.Select(offset => DateTime.UtcNow.Date.AddDays(-offset))
|
||||
.Reverse();
|
||||
|
||||
DailyEmailCounts = allDates
|
||||
.GroupJoin(
|
||||
DailyEmailCounts,
|
||||
date => date,
|
||||
emailCount => emailCount.Date,
|
||||
(date, emailCounts) => emailCounts.FirstOrDefault() ?? new DailyEmailCount { Date = date, Count = 0 }
|
||||
)
|
||||
.OrderByDescending(e => e.Date)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleChart()
|
||||
{
|
||||
ShowChart = !ShowChart;
|
||||
|
||||
// If we're showing the chart but haven't loaded the data yet
|
||||
if (ShowChart && DailyEmailCounts.Count == 0)
|
||||
{
|
||||
_ = RefreshData();
|
||||
}
|
||||
else
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EmailStatistics
|
||||
@@ -67,6 +167,12 @@
|
||||
public int Hours24 { get; set; }
|
||||
public int Days3 { get; set; }
|
||||
public int Days7 { get; set; }
|
||||
public int Days14 { get; set; }
|
||||
public int Days30 { get; set; }
|
||||
}
|
||||
|
||||
private sealed class DailyEmailCount
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public int Count { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<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.Hours24</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Hours24.ToString("N0")</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 3 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Days3</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Days3.ToString("N0")</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.Days7</h4>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Days7.ToString("N0")</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.Days14</h4>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 30 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@RegistrationStats.Days30.ToString("N0")</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
var hours24 = now.AddHours(-24);
|
||||
var days3 = now.AddDays(-3);
|
||||
var days7 = now.AddDays(-7);
|
||||
var days14 = now.AddDays(-14);
|
||||
var days30 = now.AddDays(-30);
|
||||
|
||||
// Get registration statistics
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
@@ -55,7 +55,7 @@
|
||||
Hours24 = await registrationQuery.CountAsync(u => u.CreatedAt >= hours24),
|
||||
Days3 = await registrationQuery.CountAsync(u => u.CreatedAt >= days3),
|
||||
Days7 = await registrationQuery.CountAsync(u => u.CreatedAt >= days7),
|
||||
Days14 = await registrationQuery.CountAsync(u => u.CreatedAt >= days14)
|
||||
Days30 = await registrationQuery.CountAsync(u => u.CreatedAt >= days30)
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
@@ -67,6 +67,6 @@
|
||||
public int Hours24 { get; set; }
|
||||
public int Days3 { get; set; }
|
||||
public int Days7 { get; set; }
|
||||
public int Days14 { get; set; }
|
||||
public int Days30 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
<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">Returning 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>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
This card shows the number of returning users in the last 24 hours, 3 days, 7 days, and 14 days. This excludes users who have created their accounts in these time periods.
|
||||
</p>
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-yellow-50 dark:bg-yellow-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-yellow-50 dark:bg-yellow-900/30 p-4 rounded-lg">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Last 3 days</p>
|
||||
<h4 class="text-2xl font-bold text-gray-900 dark:text-white">@UserStats.Last3Days</h4>
|
||||
@if (ShowUserNames)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<ul>
|
||||
@foreach (var user in UserStats.Last3DayUsers)
|
||||
{
|
||||
<li>@user</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="bg-yellow-50 dark:bg-yellow-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-yellow-50 dark:bg-yellow-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 bool ShowUserNames { get; set; } = false;
|
||||
private UserStatistics UserStats { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the visibility of user names.
|
||||
/// </summary>
|
||||
private void ToggleUserNames()
|
||||
{
|
||||
ShowUserNames = !ShowUserNames;
|
||||
}
|
||||
|
||||
/// <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 last3Days = now.AddDays(-3);
|
||||
var last7Days = now.AddDays(-7);
|
||||
var last14Days = now.AddDays(-14);
|
||||
|
||||
var (count24h, users24h) = await GetReturningUserCount(last24Hours);
|
||||
var (count3d, users3d) = await GetReturningUserCount(last3Days);
|
||||
var (count7d, users7d) = await GetReturningUserCount(last7Days);
|
||||
var (count14d, users14d) = await GetReturningUserCount(last14Days);
|
||||
|
||||
UserStats = new UserStatistics
|
||||
{
|
||||
Last24Hours = count24h,
|
||||
Last3Days = count3d,
|
||||
Last7Days = count7d,
|
||||
Last14Days = count14d,
|
||||
Last24HourUsers = users24h,
|
||||
Last3DayUsers = users3d,
|
||||
Last7DayUsers = users7d,
|
||||
Last14DayUsers = users14d
|
||||
};
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task<(int count, List<string?> users)> GetReturningUserCount(DateTime since)
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Get users who have auth logs in the time period
|
||||
var activeUserNames = await dbContext.AuthLogs
|
||||
.Where(l => l.Timestamp >= since)
|
||||
.Select(l => l.Username)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// Get returning users (created before the time period)
|
||||
var returningUsers = await dbContext.AliasVaultUsers
|
||||
.Where(u => activeUserNames.Contains(u.UserName!) && u.CreatedAt < since)
|
||||
.Select(u => u.UserName)
|
||||
.ToListAsync();
|
||||
|
||||
return (returningUsers.Count, returningUsers);
|
||||
}
|
||||
|
||||
private sealed class UserStatistics
|
||||
{
|
||||
public int Last24Hours { get; set; }
|
||||
public int Last3Days { get; set; }
|
||||
public int Last7Days { get; set; }
|
||||
public int Last14Days { get; set; }
|
||||
public List<string?> Last24HourUsers { get; set; } = new();
|
||||
public List<string?> Last3DayUsers { get; set; } = new();
|
||||
public List<string?> Last7DayUsers { get; set; } = new();
|
||||
public List<string?> Last14DayUsers { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<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.">
|
||||
Description="Welcome to the AliasVault admin dashboard. Below you can find statistics about recent activity on this server.">
|
||||
<CustomActions>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
@@ -15,17 +15,17 @@
|
||||
|
||||
<div class="px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<ActiveUsersChart @ref="_activeUsersChart" />
|
||||
<ActiveUsersCard @ref="_activeUsersCard" />
|
||||
<RegistrationStatisticsCard @ref="_registrationStatisticsCard" />
|
||||
<EmailStatisticsCard @ref="_emailStatisticsCard" />
|
||||
<ReturningUsersCard @ref="_returningUsersCard" />
|
||||
<ActiveUsersCard @ref="_activeUsersCard" />
|
||||
<EmailClaimsCard @ref="_emailClaimsCard" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private ActiveUsersChart? _activeUsersChart;
|
||||
private ActiveUsersCard? _activeUsersCard;
|
||||
private ReturningUsersCard? _returningUsersCard;
|
||||
private RegistrationStatisticsCard? _registrationStatisticsCard;
|
||||
private EmailStatisticsCard? _emailStatisticsCard;
|
||||
private EmailClaimsCard? _emailClaimsCard;
|
||||
@@ -45,6 +45,8 @@
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await RefreshData();
|
||||
@@ -56,15 +58,15 @@
|
||||
/// </summary>
|
||||
private async Task RefreshData()
|
||||
{
|
||||
if (_activeUsersCard != null &&
|
||||
_returningUsersCard != null &&
|
||||
if (_activeUsersChart != null &&
|
||||
_activeUsersCard != null &&
|
||||
_registrationStatisticsCard != null &&
|
||||
_emailStatisticsCard != null &&
|
||||
_emailClaimsCard != null)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
_activeUsersChart.RefreshData(),
|
||||
_activeUsersCard.RefreshData(),
|
||||
_returningUsersCard.RefreshData(),
|
||||
_registrationStatisticsCard.RefreshData(),
|
||||
_emailStatisticsCard.RefreshData(),
|
||||
_emailClaimsCard.RefreshData()
|
||||
|
||||
@@ -7,12 +7,25 @@
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="@(TotalRecords > 0 ? $"Emails ({TotalRecords:N0})" : "Emails")"
|
||||
Description="This page gives an overview of recently received mails by this AliasVault server. Note that all email fields except 'To' are encrypted with the public key of the user and cannot be decrypted by the server.">
|
||||
Description="This page shows an overview of recently received mails by this AliasVault server. Note: all email fields except 'To' are encrypted with the public key of the user and are unreadable by the server.">
|
||||
<CustomActions>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@if (IsInitialized)
|
||||
{
|
||||
<div class="px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
<div class="mb-3">
|
||||
<div class="relative">
|
||||
<SearchIcon />
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search emails..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
@@ -20,23 +33,25 @@
|
||||
else
|
||||
{
|
||||
<div class="overflow-x-auto px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var email in EmailList)
|
||||
@foreach (var viewModel in EmailViewModelList)
|
||||
{
|
||||
<SortableTableRow>
|
||||
<SortableTableColumn IsPrimary="true">@email.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@email.DateSystem.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@(email.FromLocal.Length > 15 ? email.FromLocal.Substring(0, 15) : email.FromLocal)@@@(email.FromDomain.Length > 15 ? email.FromDomain.Substring(0, 15) : email.FromDomain)</SortableTableColumn>
|
||||
<SortableTableColumn>@email.ToLocal@@@email.ToDomain</SortableTableColumn>
|
||||
<SortableTableColumn>@(email.Subject.Length > 30 ? email.Subject.Substring(0, 30) : email.Subject)</SortableTableColumn>
|
||||
<SortableTableColumn IsPrimary="true">@viewModel.Email.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@viewModel.Email.DateSystem.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@(viewModel.Email.FromLocal.Length > 10 ? viewModel.Email.FromLocal.Substring(0, 10) : viewModel.Email.FromLocal)@@@(viewModel.Email.FromDomain.Length > 10 ? viewModel.Email.FromDomain.Substring(0, 10) : viewModel.Email.FromDomain)</SortableTableColumn>
|
||||
<SortableTableColumn>@viewModel.Email.ToLocal@@@viewModel.Email.ToDomain</SortableTableColumn>
|
||||
<SortableTableColumn>
|
||||
<span class="line-clamp-1">
|
||||
@(email.MessagePreview?.Length > 30 ? email.MessagePreview.Substring(0, 30) : email.MessagePreview)
|
||||
</span>
|
||||
@if (viewModel.UserName.Length > 0)
|
||||
{
|
||||
<span class="line-clamp-1"><a href="users/@viewModel.UserId">@viewModel.UserName</a></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="line-clamp-1">n/a</span>
|
||||
}
|
||||
</SortableTableColumn>
|
||||
<SortableTableColumn>@email.Attachments.Count</SortableTableColumn>
|
||||
<SortableTableColumn>@viewModel.Email.Attachments.Count</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
@@ -44,25 +59,50 @@ else
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The search term from the query parameter.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery(Name = "search")]
|
||||
public string? SearchTermFromQuery { get; set; }
|
||||
|
||||
private readonly List<TableColumn> _tableColumns = [
|
||||
new TableColumn { Title = "ID", PropertyName = "Id" },
|
||||
new TableColumn { Title = "Time", PropertyName = "DateSystem" },
|
||||
new TableColumn { Title = "From", PropertyName = "From" },
|
||||
new TableColumn { Title = "To", PropertyName = "To" },
|
||||
new TableColumn { Title = "Subject", PropertyName = "Subject" },
|
||||
new TableColumn { Title = "Preview", PropertyName = "MessagePreview" },
|
||||
new TableColumn { Title = "User", Sortable = false },
|
||||
new TableColumn { Title = "Attachments", PropertyName = "Attachments" },
|
||||
];
|
||||
|
||||
private List<Email> EmailList { get; set; } = [];
|
||||
private List<EmailViewModel> EmailViewModelList { get; set; } = [];
|
||||
private bool IsInitialized { get; set; } = false;
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
private string _searchTerm = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The last search term.
|
||||
/// </summary>
|
||||
private string _lastSearchTerm = string.Empty;
|
||||
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
set
|
||||
{
|
||||
if (_searchTerm != value)
|
||||
{
|
||||
_searchTerm = value;
|
||||
_ = RefreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string SortColumn { get; set; } = "Id";
|
||||
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
||||
|
||||
private async Task HandleSortChanged((string column, SortDirection direction) sort)
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
@@ -75,6 +115,12 @@ else
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// Set the search term from the query parameter if it exists
|
||||
if (!string.IsNullOrEmpty(SearchTermFromQuery))
|
||||
{
|
||||
_searchTerm = SearchTermFromQuery;
|
||||
}
|
||||
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
@@ -91,8 +137,68 @@ else
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
IQueryable<Email> query = dbContext.Emails;
|
||||
|
||||
query = ApplySearchFilter(query);
|
||||
query = ApplySort(query);
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
var emailList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// Get all usernames for the emails in the current list
|
||||
var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
|
||||
var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
|
||||
.Where(x => encryptionKeyIds.Contains(x.Id))
|
||||
.Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
|
||||
.ToListAsync();
|
||||
|
||||
// Create new list of viewmodels
|
||||
EmailViewModelList = new List<EmailViewModel>();
|
||||
|
||||
foreach (var email in emailList)
|
||||
{
|
||||
var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
|
||||
EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a search filter to the query based on the search term.
|
||||
/// </summary>
|
||||
/// <param name="query">The query to filter.</param>
|
||||
/// <returns>The filtered query.</returns>
|
||||
private IQueryable<Email> ApplySearchFilter(IQueryable<Email> query)
|
||||
{
|
||||
if (SearchTerm.Length > 0)
|
||||
{
|
||||
// Reset page number back to 1 if the search term has changed.
|
||||
if (SearchTerm != _lastSearchTerm && CurrentPage != 1)
|
||||
{
|
||||
CurrentPage = 1;
|
||||
}
|
||||
_lastSearchTerm = SearchTerm;
|
||||
|
||||
query = query.Where(x => EF.Functions.Like(x.To.ToLower(), "%" + SearchTerm.Trim().ToLower() + "%"));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies sorting to the query based on the sort column and direction.
|
||||
/// </summary>
|
||||
/// <param name="query">The query to sort.</param>
|
||||
/// <returns>The sorted query.</returns>
|
||||
private IQueryable<Email> ApplySort(IQueryable<Email> query)
|
||||
{
|
||||
// Apply sort
|
||||
switch (SortColumn)
|
||||
{
|
||||
@@ -116,16 +222,6 @@ else
|
||||
? query.OrderBy(x => x.ToLocal + "@" + x.ToDomain)
|
||||
: query.OrderByDescending(x => x.ToLocal + "@" + x.ToDomain);
|
||||
break;
|
||||
case "Subject":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Subject)
|
||||
: query.OrderByDescending(x => x.Subject);
|
||||
break;
|
||||
case "MessagePreview":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.MessagePreview)
|
||||
: query.OrderByDescending(x => x.MessagePreview);
|
||||
break;
|
||||
case "Attachments":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Attachments.Count)
|
||||
@@ -136,13 +232,13 @@ else
|
||||
break;
|
||||
}
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
EmailList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
return query;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
private sealed class EmailViewModel
|
||||
{
|
||||
public Email Email { get; set; } = new();
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,25 @@
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="@(TotalRecords > 0 ? $"Auth logs ({TotalRecords:N0})" : "Auth logs")"
|
||||
Description="This page gives an overview of recent auth attempts.">
|
||||
Description="This page shows an overview of recent auth attempts.">
|
||||
<CustomActions>
|
||||
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
@if (IsInitialized)
|
||||
{
|
||||
<div class="px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<div class="mb-4 flex space-x-4">
|
||||
<div class="mb-3 flex space-x-4">
|
||||
<div class="flex w-full">
|
||||
<div class="w-2/3 pr-2">
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
<div class="relative">
|
||||
<SearchIcon />
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/3 pl-2">
|
||||
<select @bind="SelectedEventType" class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
@@ -40,7 +39,16 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="px-4">
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var log in LogList)
|
||||
{
|
||||
@@ -70,12 +78,14 @@ else
|
||||
];
|
||||
|
||||
private List<AuthLog> LogList { get; set; } = [];
|
||||
private bool IsInitialized { get; set; } = false;
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
private string _lastSearchTerm = string.Empty;
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
@@ -130,12 +140,33 @@ else
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var query = dbContext.AuthLogs.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
query = query.Where(x => EF.Functions.Like(x.Username.ToLower(), "%" + SearchTerm.ToLower() + "%"));
|
||||
// Reset page number back to 1 if the search term has changed.
|
||||
if (SearchTerm != _lastSearchTerm)
|
||||
{
|
||||
CurrentPage = 1;
|
||||
}
|
||||
_lastSearchTerm = SearchTerm;
|
||||
|
||||
// If the search term starts with "client:", we search for the client header.
|
||||
if (SearchTerm.StartsWith("client:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var clientSearchTerm = SearchTerm.Substring(7).ToLower();
|
||||
query = query.Where(x => EF.Functions.Like((x.Client ?? string.Empty).ToLower(), "%" + clientSearchTerm + "%"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchTerm = SearchTerm.Trim().ToLower();
|
||||
query = query.Where(x => EF.Functions.Like((x.Username ?? string.Empty).ToLower(), "%" + searchTerm + "%") ||
|
||||
EF.Functions.Like((x.IpAddress ?? string.Empty).ToLower(), "%" + searchTerm + "%"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedEventType))
|
||||
@@ -156,6 +187,7 @@ else
|
||||
.ToListAsync();
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,26 +7,25 @@
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="@(TotalRecords > 0 ? $"General logs ({TotalRecords:N0})" : "General logs")"
|
||||
Description="This page gives an overview of recent system logs.">
|
||||
Description="This page shows an overview of recent system logs.">
|
||||
<CustomActions>
|
||||
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
@if (IsInitialized)
|
||||
{
|
||||
<div class="px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<div class="mb-4 flex space-x-4">
|
||||
<div class="mb-3 flex space-x-4">
|
||||
<div class="flex w-full">
|
||||
<div class="w-2/3 pr-2">
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
<div class="relative">
|
||||
<SearchIcon />
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/3 pl-2">
|
||||
<select @bind="SelectedServiceName" class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
@@ -39,7 +38,16 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="px-4">
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var log in LogList)
|
||||
{
|
||||
@@ -85,12 +93,15 @@ else
|
||||
];
|
||||
|
||||
private List<Log> LogList { get; set; } = [];
|
||||
private bool IsInitialized { get; set; } = false;
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
private string _lastSearchTerm = string.Empty;
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
@@ -149,20 +160,14 @@ else
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var query = dbContext.Logs.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
query = query.Where(x => EF.Functions.Like(x.Application.ToLower(), "%" + SearchTerm.ToLower() + "%") ||
|
||||
EF.Functions.Like(x.Message.ToLower(), "%" + SearchTerm.ToLower() + "%") ||
|
||||
EF.Functions.Like(x.Level.ToLower(), "%" + SearchTerm.ToLower() + "%"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedServiceName))
|
||||
{
|
||||
query = query.Where(x => x.Application == SelectedServiceName);
|
||||
}
|
||||
query = ApplySearchTermFilter(query);
|
||||
query = ApplyServiceNameFilter(query);
|
||||
|
||||
// Apply sort.
|
||||
switch (SortColumn)
|
||||
@@ -201,9 +206,49 @@ else
|
||||
.ToListAsync();
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a search term filter to the query.
|
||||
/// </summary>
|
||||
/// <param name="query">The query to apply the filter to.</param>
|
||||
private IQueryable<Log> ApplySearchTermFilter(IQueryable<Log> query)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
// Reset page number back to 1 if the search term has changed.
|
||||
if (SearchTerm != _lastSearchTerm)
|
||||
{
|
||||
CurrentPage = 1;
|
||||
}
|
||||
_lastSearchTerm = SearchTerm;
|
||||
|
||||
var searchTerm = SearchTerm.Trim().ToLower();
|
||||
query = query.Where(x => EF.Functions.Like(x.Application.ToLower(), "%" + searchTerm + "%") ||
|
||||
EF.Functions.Like(x.Message.ToLower(), "%" + searchTerm + "%") ||
|
||||
EF.Functions.Like(x.Level.ToLower(), "%" + searchTerm + "%") ||
|
||||
EF.Functions.Like(x.SourceContext.ToLower(), "%" + searchTerm + "%"));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a service name filter to the query.
|
||||
/// </summary>
|
||||
/// <param name="query">The query to apply the filter to.</param>
|
||||
private IQueryable<Log> ApplyServiceNameFilter(IQueryable<Log> query)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(SelectedServiceName))
|
||||
{
|
||||
query = query.Where(x => x.Application == SelectedServiceName);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private async Task DeleteLogsWithConfirmation()
|
||||
{
|
||||
if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone."))
|
||||
|
||||
@@ -12,9 +12,9 @@ using AliasVault.Admin.Services;
|
||||
using AliasVault.Auth;
|
||||
using AliasVault.RazorComponents.Models;
|
||||
using AliasVault.RazorComponents.Services;
|
||||
using ApexCharts;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
@@ -73,6 +73,12 @@ public abstract class MainBase : OwningComponentBase
|
||||
[Inject]
|
||||
protected ConfirmModalService ConfirmModalService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ApexChartService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
protected IApexChartService ApexChartService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the injected JSRuntime instance.
|
||||
/// </summary>
|
||||
@@ -96,6 +102,18 @@ public abstract class MainBase : OwningComponentBase
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Home", Url = NavigationService.BaseUri });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
// Update default ApexCharts chart color based on the dark mode setting.
|
||||
await SetDefaultApexChartOptionsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the username from the authentication state asynchronously.
|
||||
/// </summary>
|
||||
@@ -104,4 +122,50 @@ public abstract class MainBase : OwningComponentBase
|
||||
{
|
||||
return UserService.User().UserName ?? "[Unknown]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default ApexCharts chart color based on the dark mode setting.
|
||||
/// </summary>
|
||||
private async Task SetDefaultApexChartOptionsAsync()
|
||||
{
|
||||
var darkMode = await JsInvokeService.RetryInvokeWithResultAsync<bool>("isDarkMode", TimeSpan.Zero, 5);
|
||||
var options = new ApexChartBaseOptions
|
||||
{
|
||||
Chart = new Chart
|
||||
{
|
||||
ForeColor = darkMode ? "#bbb" : "#555",
|
||||
},
|
||||
Fill = new Fill
|
||||
{
|
||||
Colors = darkMode ?
|
||||
[
|
||||
"#FFB84D", // Bright gold
|
||||
"#8B6CB9", // Darker Purple
|
||||
"#68A890", // Darker Sea Green
|
||||
"#CD5C5C", // Darker Coral
|
||||
"#4F94CD", // Darker Sky Blue
|
||||
"#BA55D3", // Darker Plum
|
||||
"#CDC673", // Darker Khaki
|
||||
"#6B8E23", // Darker Sage Green
|
||||
"#CD853F", // Darker Burlywood
|
||||
"#7B68EE", // Darker Slate Blue
|
||||
]
|
||||
:
|
||||
[
|
||||
"#FFB366", // Light Orange
|
||||
"#B19CD9", // Light Purple
|
||||
"#98D8C1", // Light Sea Green
|
||||
"#F08080", // Light Coral
|
||||
"#87CEEB", // Sky Blue
|
||||
"#DDA0DD", // Plum
|
||||
"#F0E68C", // Khaki
|
||||
"#9CB071", // Sage Green
|
||||
"#DEB887", // Burlywood
|
||||
"#A7A1E8", // Light Slate Blue
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await ApexChartService.SetGlobalOptionsAsync(options, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,25 @@
|
||||
<PageHeader
|
||||
BreadcrumbItems="@BreadcrumbItems"
|
||||
Title="@(TotalRecords > 0 ? $"Users ({TotalRecords:N0})" : "Users")"
|
||||
Description="This page gives an overview of all registered users and the associated vaults.">
|
||||
Description="This page shows an overview of all registered users and the associated vaults.">
|
||||
<CustomActions>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@if (IsInitialized)
|
||||
{
|
||||
<div class="px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
<div class="mb-3">
|
||||
<div class="relative">
|
||||
<SearchIcon />
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search users..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
@@ -20,12 +33,6 @@
|
||||
else
|
||||
{
|
||||
<div class="px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
|
||||
|
||||
<div class="mb-4">
|
||||
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search users..." class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
</div>
|
||||
|
||||
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var user in UserList)
|
||||
{
|
||||
@@ -33,15 +40,19 @@ else
|
||||
<SortableTableColumn IsPrimary="true">@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>@user.UserName</SortableTableColumn>
|
||||
<SortableTableColumn>@user.VaultCount</SortableTableColumn>
|
||||
<SortableTableColumn>@user.CredentialCount</SortableTableColumn>
|
||||
<SortableTableColumn>@user.EmailClaimCount</SortableTableColumn>
|
||||
<SortableTableColumn>@Math.Round((double)user.VaultStorageInKb / 1024, 1) MB</SortableTableColumn>
|
||||
<SortableTableColumn><StatusPill Enabled="user.TwoFactorEnabled" /></SortableTableColumn>
|
||||
<SortableTableColumn>@user.LastVaultUpdate.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn>
|
||||
@if (user.Blocked)
|
||||
{
|
||||
<StatusPill Enabled="false" TextFalse="Blocked" />
|
||||
}
|
||||
@if (user.TwoFactorEnabled)
|
||||
{
|
||||
<StatusPill Enabled="true" TextTrue="2FA enabled" />
|
||||
}
|
||||
</SortableTableColumn>
|
||||
<SortableTableColumn>
|
||||
<LinkButton Color="primary" Href="@($"users/{user.Id}")" Text="View" />
|
||||
@@ -57,21 +68,28 @@ else
|
||||
new TableColumn { Title = "Registered", PropertyName = "CreatedAt" },
|
||||
new TableColumn { Title = "Username", PropertyName = "UserName" },
|
||||
new TableColumn { Title = "# Vaults", PropertyName = "VaultCount" },
|
||||
new TableColumn { Title = "# Credentials", PropertyName = "CredentialCount" },
|
||||
new TableColumn { Title = "# Email claims", PropertyName = "EmailClaimCount" },
|
||||
new TableColumn { Title = "Storage", PropertyName = "VaultStorageInKb" },
|
||||
new TableColumn { Title = "2FA", PropertyName = "TwoFactorEnabled" },
|
||||
new TableColumn { Title = "LastVaultUpdate", PropertyName = "LastVaultUpdate" },
|
||||
new TableColumn { Title = "Status", Sortable = false },
|
||||
new TableColumn { Title = "Actions", Sortable = false},
|
||||
];
|
||||
|
||||
private List<UserViewModel> UserList { get; set; } = [];
|
||||
private bool IsInitialized { get; set; } = false;
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The last search term.
|
||||
/// </summary>
|
||||
private string _lastSearchTerm = string.Empty;
|
||||
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
@@ -112,15 +130,13 @@ else
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
IQueryable<AliasVaultUser> query = dbContext.AliasVaultUsers;
|
||||
|
||||
if (SearchTerm.Length > 0)
|
||||
{
|
||||
query = query.Where(x => EF.Functions.Like(x.UserName!.ToLower(), "%" + SearchTerm.ToLower() + "%"));
|
||||
}
|
||||
|
||||
// Apply sort.
|
||||
query = ApplySearchFilter(query);
|
||||
query = ApplySort(query);
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
@@ -137,7 +153,9 @@ else
|
||||
Vaults = u.Vaults.Select(v => new
|
||||
{
|
||||
v.FileSize,
|
||||
v.CreatedAt
|
||||
v.CreatedAt,
|
||||
v.RevisionNumber,
|
||||
CredentialCount = v.CredentialsCount,
|
||||
}),
|
||||
EmailClaims = u.EmailClaims.Select(ec => new
|
||||
{
|
||||
@@ -154,15 +172,38 @@ else
|
||||
Blocked = user.Blocked,
|
||||
CreatedAt = user.CreatedAt,
|
||||
VaultCount = user.Vaults.Count(),
|
||||
CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
|
||||
EmailClaimCount = user.EmailClaims.Count(),
|
||||
VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
|
||||
LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt,
|
||||
}).ToList();
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply search filter to the query.
|
||||
/// </summary>
|
||||
private IQueryable<AliasVaultUser> ApplySearchFilter(IQueryable<AliasVaultUser> query)
|
||||
{
|
||||
if (SearchTerm.Length > 0)
|
||||
{
|
||||
// Reset page number back to 1 if the search term has changed.
|
||||
if (SearchTerm != _lastSearchTerm && CurrentPage != 1)
|
||||
{
|
||||
CurrentPage = 1;
|
||||
}
|
||||
_lastSearchTerm = SearchTerm;
|
||||
|
||||
var searchTerm = SearchTerm.Trim().ToLower();
|
||||
query = query.Where(x => EF.Functions.Like(x.UserName!.ToLower(), "%" + searchTerm + "%"));
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply sort to the query.
|
||||
/// </summary>
|
||||
@@ -191,6 +232,11 @@ else
|
||||
? query.OrderBy(x => x.Vaults.Count)
|
||||
: query.OrderByDescending(x => x.Vaults.Count);
|
||||
break;
|
||||
case "CredentialCount":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialsCount)
|
||||
: query.OrderByDescending(x => x.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialsCount);
|
||||
break;
|
||||
case "EmailClaimCount":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.EmailClaims.Count)
|
||||
@@ -201,11 +247,6 @@ else
|
||||
? query.OrderBy(x => x.Vaults.Sum(v => v.FileSize))
|
||||
: query.OrderByDescending(x => x.Vaults.Sum(v => v.FileSize));
|
||||
break;
|
||||
case "TwoFactorEnabled":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.TwoFactorEnabled)
|
||||
: query.OrderByDescending(x => x.TwoFactorEnabled);
|
||||
break;
|
||||
case "LastVaultUpdate":
|
||||
query = SortDirection == SortDirection.Ascending
|
||||
? query.OrderBy(x => x.Vaults.Max(v => v.CreatedAt))
|
||||
|
||||
@@ -1,35 +1,125 @@
|
||||
@using AliasVault.RazorComponents.Tables
|
||||
|
||||
<SortableTable Columns="@_emailClaimTableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var entry in SortedEmailClaimList)
|
||||
{
|
||||
<SortableTableRow>
|
||||
<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>
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Button Color="secondary" OnClick="ToggleShowDisabled">
|
||||
@(ShowDisabled ? "Hide Disabled Claims" : "Show Disabled Claims")
|
||||
</Button>
|
||||
@if (EmailClaimList.Any(e => !e.Disabled))
|
||||
{
|
||||
<Button Color="danger" OnClick="DisableAllEmailClaims">Disable All</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<SortableTable Columns="@_emailClaimTableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
|
||||
@foreach (var entry in SortedEmailClaimList)
|
||||
{
|
||||
<SortableTableRow Class="@(entry.Disabled ? "bg-secondary" : "")">
|
||||
<SortableTableColumn IsPrimary="true">@entry.Id</SortableTableColumn>
|
||||
<SortableTableColumn>@entry.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
|
||||
<SortableTableColumn><a href="emails?search=@entry.Address">@entry.Address</a></SortableTableColumn>
|
||||
<SortableTableColumn>@entry.EmailCount</SortableTableColumn>
|
||||
<SortableTableColumn>@(entry.Disabled ? "Disabled" : "Enabled")</SortableTableColumn>
|
||||
<SortableTableColumn>
|
||||
@if (entry.Disabled)
|
||||
{
|
||||
<Button Color="success" OnClick="() => ToggleEmailClaimStatus(entry)">Enable</Button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Button Color="danger" OnClick="() => ToggleEmailClaimStatus(entry)">Disable</Button>
|
||||
}
|
||||
</SortableTableColumn>
|
||||
</SortableTableRow>
|
||||
}
|
||||
</SortableTable>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets or sets the list of email claims to display.
|
||||
/// Gets or sets the user.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public List<UserEmailClaimWithCount> EmailClaimList { get; set; } = [];
|
||||
public AliasVaultUser User { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback for when an email claim is enabled or disabled.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<(Guid id, bool disabled)> OnEmailClaimStatusChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of email claims to display.
|
||||
/// </summary>
|
||||
private List<UserEmailClaimWithCount> EmailClaimList { get; set; } = [];
|
||||
|
||||
private bool IsLoading { get; set; } = true;
|
||||
|
||||
private string SortColumn { get; set; } = "CreatedAt";
|
||||
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
|
||||
private bool ShowDisabled { get; set; } = false;
|
||||
|
||||
private readonly List<TableColumn> _emailClaimTableColumns = [
|
||||
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" },
|
||||
new TableColumn { Title = "Status", PropertyName = "Disabled" },
|
||||
new TableColumn { Title = "Actions", PropertyName = "" },
|
||||
];
|
||||
|
||||
private IEnumerable<UserEmailClaimWithCount> SortedEmailClaimList => SortList(EmailClaimList, SortColumn, SortDirection);
|
||||
private IEnumerable<UserEmailClaimWithCount> SortedEmailClaimList =>
|
||||
SortList(ShowDisabled ? EmailClaimList : EmailClaimList.Where(e => !e.Disabled).ToList(), SortColumn, SortDirection);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await RefreshData();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method will refresh the email claim list.
|
||||
/// </summary>
|
||||
private async Task RefreshData()
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(User.Id))
|
||||
{
|
||||
EmailClaimList = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Load all email claims for this user.
|
||||
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),
|
||||
Disabled = x.Disabled
|
||||
})
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private void HandleSortChanged((string column, SortDirection direction) sort)
|
||||
{
|
||||
@@ -38,6 +128,87 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ToggleShowDisabled()
|
||||
{
|
||||
ShowDisabled = !ShowDisabled;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method will toggle the disabled status of an email claim.
|
||||
/// </summary>
|
||||
private async Task ToggleEmailClaimStatus(UserEmailClaimWithCount entry)
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
if (entry.Disabled)
|
||||
{
|
||||
// Enable email claim without confirmation.
|
||||
var emailClaim = await dbContext.UserEmailClaims.FindAsync(entry.Id);
|
||||
if (emailClaim != null)
|
||||
{
|
||||
// Re-enable the email claim.
|
||||
emailClaim.Disabled = false;
|
||||
emailClaim.UpdatedAt = DateTime.UtcNow;
|
||||
await dbContext.SaveChangesAsync();
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (await ConfirmModalService.ShowConfirmation(
|
||||
title: "Confirm Email Claim Disable",
|
||||
message: @"Are you sure you want to disable this email claim?
|
||||
|
||||
Important notes:
|
||||
• Disabling an email claim means that emails will no longer be received for this address and will be rejected by the server.
|
||||
• The user can re-enable this at will by re-saving their vault which will claim it again.
|
||||
|
||||
Do you want to proceed with disabling this claim?"))
|
||||
{
|
||||
// Load email claim
|
||||
var emailClaim = await dbContext.UserEmailClaims.FindAsync(entry.Id);
|
||||
if (emailClaim != null)
|
||||
{
|
||||
// Set the disabled status to true.
|
||||
emailClaim.Disabled = true;
|
||||
emailClaim.UpdatedAt = DateTime.UtcNow;
|
||||
await dbContext.SaveChangesAsync();
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DisableAllEmailClaims()
|
||||
{
|
||||
if (await ConfirmModalService.ShowConfirmation(
|
||||
title: "Confirm Email Claim Disable",
|
||||
message: @"Are you sure you want to disable all email claims?
|
||||
|
||||
Important notes:
|
||||
• Disabling an email claim means that emails will no longer be received for this address and will be rejected by the server.
|
||||
• The user can re-enable this at will by re-saving their vault which will claim it again.
|
||||
|
||||
Do you want to proceed with disabling all email claims?"))
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
// Load email claims
|
||||
var emailClaims = await dbContext.UserEmailClaims.Where(x => x.UserId == User.Id).ToListAsync();
|
||||
|
||||
// Disable all email claims.
|
||||
foreach (var emailClaim in emailClaims)
|
||||
{
|
||||
emailClaim.Disabled = true;
|
||||
emailClaim.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<UserEmailClaimWithCount> SortList(List<UserEmailClaimWithCount> emailClaims, string sortColumn, SortDirection sortDirection)
|
||||
{
|
||||
return sortColumn switch
|
||||
@@ -46,6 +217,7 @@
|
||||
"CreatedAt" => SortableTable.SortListByProperty(emailClaims, e => e.CreatedAt, sortDirection),
|
||||
"Address" => SortableTable.SortListByProperty(emailClaims, e => e.Address, sortDirection),
|
||||
"EmailCount" => SortableTable.SortListByProperty(emailClaims, e => e.EmailCount, sortDirection),
|
||||
"Disabled" => SortableTable.SortListByProperty(emailClaims, e => e.Disabled, sortDirection),
|
||||
_ => emailClaims
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,39 +25,57 @@ else
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="items-center xl:block sm:space-x-4 xl:space-x-0 2xl:space-x-4">
|
||||
<div>
|
||||
<h3 class="mb-1 text-xl font-bold text-gray-900 dark:text-white">@User.UserName</h3>
|
||||
<h3 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white border-b border-gray-200 pb-2">@User.UserName</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Id</label>
|
||||
<div class="text-gray-700 dark:text-gray-300">@User.Id</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">2FA Status:</span>
|
||||
<StatusPill Enabled="@User.TwoFactorEnabled"/>
|
||||
<span class="text-gray-700 dark:text-gray-300">Authenticator key(s) active: @TwoFactorKeysCount</span>
|
||||
@if (User.TwoFactorEnabled)
|
||||
{
|
||||
<Button Color="danger" OnClick="DisableTwoFactor">Disable 2FA</Button>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (TwoFactorKeysCount > 0)
|
||||
{
|
||||
<Button Color="success" OnClick="EnableTwoFactor">Enable 2FA</Button>
|
||||
<Button Color="danger" OnClick="ResetTwoFactor">Remove 2FA keys</Button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 mt-4">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Account Status:</span>
|
||||
<StatusPill Enabled="@(!User.Blocked)" TextTrue="Active" TextFalse="Blocked" />
|
||||
<Button Color="@(User.Blocked ? "success" : "danger")" OnClick="ToggleBlockStatus">
|
||||
@(User.Blocked ? "Unblock User" : "Block User")
|
||||
</Button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Blocking a user prevents them from logging in or accessing AliasVault
|
||||
</span>
|
||||
<div class="w-full mb-4 overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<tbody>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">Id</th>
|
||||
<td class="px-4 py-3">@User.Id</td>
|
||||
</tr>
|
||||
<tr class="border-b dark:border-gray-700">
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">Registered at</th>
|
||||
<td class="px-4 py-3">@User.CreatedAt.ToString("yyyy-MM-dd HH:mm")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">2FA Status</th>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<StatusPill Enabled="@User.TwoFactorEnabled"/>
|
||||
<span class="text-gray-700 dark:text-gray-300">Authenticator key(s) active: @TwoFactorKeysCount</span>
|
||||
@if (User.TwoFactorEnabled)
|
||||
{
|
||||
<Button Color="danger" OnClick="DisableTwoFactor">Disable 2FA</Button>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (TwoFactorKeysCount > 0)
|
||||
{
|
||||
<Button Color="success" OnClick="EnableTwoFactor">Enable 2FA</Button>
|
||||
<Button Color="danger" OnClick="ResetTwoFactor">Remove 2FA keys</Button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">Account Status</th>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<StatusPill Enabled="@(!User.Blocked)" TextTrue="Active" TextFalse="Blocked" />
|
||||
<Button Color="@(User.Blocked ? "success" : "danger")" OnClick="ToggleBlockStatus">
|
||||
@(User.Blocked ? "Unblock User" : "Block User")
|
||||
</Button>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Blocking a user prevents them from logging in or accessing AliasVault
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,8 +105,12 @@ else
|
||||
<div class="items-center">
|
||||
<div>
|
||||
<h3 class="mb-1 text-xl font-bold text-gray-900 dark:text-white">Email claims</h3>
|
||||
|
||||
<EmailClaimTable EmailClaimList="@EmailClaimList" />
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Email claims represent the email addresses that the user has (historically) used. Whenever a user deletes an email alias
|
||||
the claim gets disabled and the server will reject all emails sent to that alias. A user can always re-enable
|
||||
the claim by using it again. Email claims are permanently tied to a user and cannot be transferred to another user.
|
||||
</p>
|
||||
<EmailClaimTable User="@User" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,7 +130,6 @@ else
|
||||
private int TwoFactorKeysCount { get; set; }
|
||||
private List<AliasVaultUserRefreshToken> RefreshTokenList { get; set; } = [];
|
||||
private List<Vault> VaultList { get; set; } = [];
|
||||
private List<UserEmailClaimWithCount> EmailClaimList { get; set; } = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -183,22 +204,6 @@ else
|
||||
.OrderBy(x => x.UpdatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
// Load all email claims for this user.
|
||||
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();
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@using AliasVault.Admin.Main.Layout
|
||||
@using AliasVault.Admin.Main.Components
|
||||
@using AliasVault.Admin.Main.Components.Alerts
|
||||
@using AliasVault.Admin.Main.Components.Icons
|
||||
@using AliasVault.Admin.Main.Components.Layout
|
||||
@using AliasVault.Admin.Main.Components.Loading
|
||||
@using AliasVault.Admin.Main.Components.WorkerStatus
|
||||
@@ -27,4 +28,5 @@
|
||||
@using AliasVault.Admin.Main.Pages
|
||||
@using AliasVault.Admin.Services
|
||||
@using AliasServerDb
|
||||
@using ApexCharts
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
|
||||
@@ -19,6 +19,7 @@ using AliasVault.Logging;
|
||||
using AliasVault.RazorComponents.Services;
|
||||
using AliasVault.Shared.Models.Configuration;
|
||||
using AliasVault.Shared.Server.Services;
|
||||
using ApexCharts;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@@ -61,6 +62,7 @@ builder.Services.AddScoped<AuthLoggingService>();
|
||||
builder.Services.AddScoped<ConfirmModalService>();
|
||||
builder.Services.AddScoped<ServerSettingsService>();
|
||||
builder.Services.AddSingleton(new VersionedContentService(Directory.GetCurrentDirectory() + "/wwwroot"));
|
||||
builder.Services.AddApexCharts();
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
|
||||
@@ -50,4 +50,44 @@ public class JsInvokeService(IJSRuntime js)
|
||||
|
||||
// Optionally log that the JS function could not be called after maxAttempts
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a JavaScript function with retry and exponential backoff that returns a value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of value to return from the JavaScript function.</typeparam>
|
||||
/// <param name="functionName">The JS function name to call.</param>
|
||||
/// <param name="initialDelay">Initial delay before calling the function.</param>
|
||||
/// <param name="maxAttempts">Maximum attempts before giving up.</param>
|
||||
/// <param name="args">Arguments to pass on to the javascript function.</param>
|
||||
/// <returns>The value returned from the JavaScript function.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the JS function could not be called after all attempts.</exception>
|
||||
public async Task<TValue> RetryInvokeWithResultAsync<TValue>(string functionName, TimeSpan initialDelay, int maxAttempts, params object[] args)
|
||||
{
|
||||
TimeSpan delay = initialDelay;
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool isDefined = await js.InvokeAsync<bool>("isFunctionDefined", functionName);
|
||||
if (isDefined)
|
||||
{
|
||||
return await js.InvokeAsync<TValue>(functionName, args);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Optionally log the exception
|
||||
}
|
||||
|
||||
// Wait for the delay before the next attempt
|
||||
await Task.Delay(delay);
|
||||
|
||||
// Exponential backoff: double the delay for the next attempt
|
||||
delay = TimeSpan.FromTicks(delay.Ticks * 2);
|
||||
}
|
||||
|
||||
// All attempts failed, throw an exception
|
||||
throw new InvalidOperationException($"Failed to invoke JavaScript function '{functionName}' after {maxAttempts} attempts.");
|
||||
}
|
||||
}
|
||||
|
||||
4
src/AliasVault.Admin/package-lock.json
generated
4
src/AliasVault.Admin/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "aliasvault.client",
|
||||
"name": "aliasvault.admin",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aliasvault.client",
|
||||
"name": "aliasvault.admin",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -566,6 +566,10 @@ video {
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -594,6 +598,15 @@ video {
|
||||
inset: 0px;
|
||||
}
|
||||
|
||||
.inset-y-0 {
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0px;
|
||||
}
|
||||
@@ -614,6 +627,10 @@ video {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.col-span-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -646,10 +663,18 @@ video {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-5 {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@@ -805,6 +830,10 @@ video {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.max-h-\[500px\] {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.w-1\/2 {
|
||||
width: 50%;
|
||||
}
|
||||
@@ -947,6 +976,10 @@ video {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@@ -963,6 +996,10 @@ video {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -1267,11 +1304,6 @@ video {
|
||||
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-purple-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(250 245 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-red-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
|
||||
@@ -1430,14 +1462,26 @@ video {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-3 {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.ps-10 {
|
||||
padding-inline-start: 2.5rem;
|
||||
}
|
||||
|
||||
.ps-2 {
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
@@ -1653,6 +1697,10 @@ video {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.filter {
|
||||
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||
}
|
||||
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
|
||||
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
|
||||
@@ -1933,10 +1981,6 @@ video {
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-purple-900\/30:is(.dark *) {
|
||||
background-color: rgb(88 28 135 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-red-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||
@@ -1966,10 +2010,6 @@ video {
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-yellow-900\/30:is(.dark *) {
|
||||
background-color: rgb(113 63 18 / 0.3);
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ window.initTopMenu = function() {
|
||||
initDarkModeSwitcher();
|
||||
};
|
||||
|
||||
window.isDarkMode = function() {
|
||||
return document.documentElement.classList.contains('dark');
|
||||
};
|
||||
|
||||
window.registerClickOutsideHandler = (dotNetHelper) => {
|
||||
document.addEventListener('click', (event) => {
|
||||
const menu = document.getElementById('userMenuDropdown');
|
||||
@@ -89,3 +93,35 @@ function generateQrCode(id) {
|
||||
qrcode.makeCode(dataUrl);
|
||||
}
|
||||
|
||||
// Keyboard navigation for pagination
|
||||
window.enablePaginationKeyboardNavigation = (element, dotNetHelper, currentPage, maxPage) => {
|
||||
if (!element) return;
|
||||
|
||||
// Add tabindex and focus if not already set
|
||||
if (!element.hasAttribute('tabindex')) {
|
||||
element.setAttribute('tabindex', '0');
|
||||
}
|
||||
|
||||
// Remove any existing event listener to prevent duplicates
|
||||
if (element._paginationKeyHandler) {
|
||||
element.removeEventListener('keydown', element._paginationKeyHandler);
|
||||
}
|
||||
|
||||
// Create keyboard event handler
|
||||
element._paginationKeyHandler = (e) => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
|
||||
const newPage = e.key === 'ArrowLeft'
|
||||
? Math.max(1, currentPage - 1)
|
||||
: Math.min(maxPage, currentPage + 1);
|
||||
|
||||
if (newPage !== currentPage) {
|
||||
dotNetHelper.invokeMethodAsync('NavigateToPage', newPage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener
|
||||
element.addEventListener('keydown', element._paginationKeyHandler);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -22,12 +22,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -406,14 +406,17 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
// Get all existing user email claims.
|
||||
var existingEmailClaims = await context.UserEmailClaims
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.Select(x => x.Address)
|
||||
.ToListAsync();
|
||||
|
||||
// Keep track of processed and sanitized email addresses to know which ones still exist.
|
||||
var processedEmailAddresses = new List<string>();
|
||||
|
||||
// Register new email addresses.
|
||||
foreach (var email in newEmailAddresses)
|
||||
{
|
||||
// Sanitize email address.
|
||||
var sanitizedEmail = EmailHelper.SanitizeEmail(email);
|
||||
processedEmailAddresses.Add(sanitizedEmail);
|
||||
|
||||
// If email address is invalid according to the EmailAddressAttribute, skip it.
|
||||
if (!new EmailAddressAttribute().IsValid(sanitizedEmail))
|
||||
@@ -421,9 +424,14 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
continue;
|
||||
}
|
||||
|
||||
// If email address is already claimed by current user, we don't need to claim it again.
|
||||
if (existingEmailClaims.Any(x => x.Address == sanitizedEmail))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the email address is already claimed (by another user).
|
||||
var existingClaim = await context.UserEmailClaims
|
||||
.FirstOrDefaultAsync(x => x.Address == sanitizedEmail);
|
||||
var existingClaim = await context.UserEmailClaims.FirstOrDefaultAsync(x => x.Address == sanitizedEmail);
|
||||
|
||||
if (existingClaim != null && existingClaim.UserId != user.Id)
|
||||
{
|
||||
@@ -432,11 +440,10 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingEmailClaims.Contains(sanitizedEmail))
|
||||
// If we get to this point, the email address is new and not claimed by another user, so we can add it.
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
context.UserEmailClaims.Add(new UserEmailClaim
|
||||
context.UserEmailClaims.Add(new UserEmailClaim
|
||||
{
|
||||
UserId = user.Id,
|
||||
Address = sanitizedEmail,
|
||||
@@ -445,19 +452,27 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
});
|
||||
}
|
||||
catch (DbUpdateException ex)
|
||||
{
|
||||
// Error while adding email claim. Log the error and continue.
|
||||
logger.LogWarning(ex, "Error while adding UserEmailClaim with email: {Email} for user: {UserId}.", sanitizedEmail, user.UserName);
|
||||
}
|
||||
}
|
||||
catch (DbUpdateException ex)
|
||||
{
|
||||
// Error while adding email claim. Log the error and continue.
|
||||
logger.LogWarning(ex, "Error while adding UserEmailClaim with email: {Email} for user: {UserId}.", sanitizedEmail, user.UserName);
|
||||
}
|
||||
}
|
||||
|
||||
// Do not delete email claims that are not in the new list
|
||||
// as they may be re-used by the user in the future. We don't want
|
||||
// to allow other users to re-use emails used by other users.
|
||||
// Disable email claims that are no longer in the new list and have not been disabled yet.
|
||||
// Important: we do not delete email claims ever, as they may be re-used by the user in the future.
|
||||
// We also don't want to allow other users to re-use emails used by other users.
|
||||
// Email claims are considered permanent.
|
||||
foreach (var existingClaim in existingEmailClaims.Where(x => !x.Disabled).ToList())
|
||||
{
|
||||
if (!processedEmailAddresses.Contains(existingClaim.Address))
|
||||
{
|
||||
// Email address is no longer in the new list and has not been disabled yet, so disable it.
|
||||
existingClaim.Disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>AliasVault.Client</RootNamespace>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
@@ -49,11 +49,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.2" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.3" />
|
||||
<PackageReference Include="SpamOK.PasswordGenerator" Version="1.1.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -77,7 +78,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Databases\AliasClientDb\AliasClientDb.csproj" />
|
||||
<ProjectReference Include="..\Generators\AliasVault.Generators.Identity\AliasVault.Generators.Identity.csproj" />
|
||||
<ProjectReference Include="..\Generators\AliasVault.Generators.Password\AliasVault.Generators.Password.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.RazorComponents\AliasVault.RazorComponents.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared.Core\AliasVault.Shared.Core.csproj" />
|
||||
<ProjectReference Include="..\Shared\AliasVault.Shared\AliasVault.Shared.csproj" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@using AliasVault.Shared.Models.Spamok
|
||||
@using AliasVault.Shared.Utilities
|
||||
@using AliasVault.Shared.Core;
|
||||
@using AliasVault.Client.Main.Components.Layout
|
||||
@inject JsInteropService JsInteropService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
@@ -8,8 +9,8 @@
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<ClickOutsideHandler OnClose="OnClose" ContentId="emailModal">
|
||||
<div class="fixed inset-0 z-50 overflow-auto bg-gray-500 bg-opacity-75 flex items-center justify-center">
|
||||
<div id="emailModal" class="relative bg-white w-3/4 flex flex-col rounded-lg shadow-xl max-h-[90vh]">
|
||||
<ModalWrapper OnEnter="Close">
|
||||
<div id="emailModal" class="relative bg-white w-3/4 flex flex-col rounded-lg shadow-xl max-h-[90vh] border-2 border-gray-300 dark:border-gray-400">
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -72,7 +73,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ClickOutsideHandler>
|
||||
|
||||
@code {
|
||||
|
||||
@@ -113,71 +113,77 @@
|
||||
|
||||
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
|
||||
|
||||
private PeriodicTimer? _refreshTimer;
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
private CancellationTokenSource? _pollingCts;
|
||||
private DotNetObjectReference<RecentEmails>? _dotNetRef;
|
||||
private bool _isPageVisible = true;
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked by JavaScript when the page visibility changes. This is used to start/stop the polling for new emails.
|
||||
/// </summary>
|
||||
/// <param name="isVisible">Indicates whether the page is visible or not.</param>
|
||||
[JSInvokable]
|
||||
public async Task OnVisibilityChange(bool isVisible)
|
||||
public void OnVisibilityChange(bool isVisible)
|
||||
{
|
||||
_isPageVisible = isVisible;
|
||||
|
||||
if (isVisible && DbService.Settings.AutoEmailRefresh)
|
||||
{
|
||||
await StartPolling();
|
||||
// Start polling if visible and auto-refresh is enabled
|
||||
StartPolling();
|
||||
}
|
||||
else
|
||||
{
|
||||
await StopPolling();
|
||||
// Stop polling if hidden
|
||||
StopPolling();
|
||||
}
|
||||
|
||||
// Refresh immediately when tab becomes visible
|
||||
|
||||
// If becoming visible, do an immediate refresh
|
||||
if (isVisible)
|
||||
{
|
||||
await ManualRefresh();
|
||||
_ = ManualRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartPolling()
|
||||
private void StartPolling()
|
||||
{
|
||||
// If already polling, no need to start again
|
||||
if (_pollingCts != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_pollingCts = new CancellationTokenSource();
|
||||
|
||||
// Start polling task
|
||||
_ = PollForEmails(_pollingCts.Token);
|
||||
}
|
||||
|
||||
private void StopPolling()
|
||||
{
|
||||
if (_pollingCts != null)
|
||||
{
|
||||
_pollingCts.Cancel();
|
||||
_pollingCts.Dispose();
|
||||
_pollingCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PollForEmails(CancellationToken cancellationToken)
|
||||
{
|
||||
await StopPolling();
|
||||
|
||||
// Create a new CancellationTokenSource since the old one might have been cancelled
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_refreshTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(ACTIVE_TAB_REFRESH_INTERVAL));
|
||||
|
||||
try
|
||||
{
|
||||
while (await _refreshTimer.WaitForNextTickAsync(_cancellationTokenSource.Token))
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await LoadRecentEmailsAsync();
|
||||
await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal cancellation, ignore
|
||||
// Normal cancellation, ignore.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StopPolling()
|
||||
{
|
||||
if (_refreshTimer is not null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _cancellationTokenSource.CancelAsync();
|
||||
await Task.Delay(100); // Give time for the polling loop to complete
|
||||
_refreshTimer.Dispose();
|
||||
_refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_refreshTimer is not null)
|
||||
{
|
||||
await StopPolling();
|
||||
_cancellationTokenSource.Dispose();
|
||||
Logger.LogError(ex, "Error in email refresh polling");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,11 +201,28 @@
|
||||
ShowComponent = IsSpamOkDomain(EmailAddress) || IsAliasVaultDomain(EmailAddress);
|
||||
IsSpamOk = IsSpamOkDomain(EmailAddress);
|
||||
|
||||
await JsInteropService.RegisterVisibilityCallback(DotNetObjectReference.Create(this));
|
||||
// Create a single object reference for JS interop
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
await JsInteropService.RegisterVisibilityCallback(_dotNetRef);
|
||||
|
||||
if (DbService.Settings.AutoEmailRefresh)
|
||||
// Only start polling if auto-refresh is enabled and page is visible
|
||||
if (DbService.Settings.AutoEmailRefresh && _isPageVisible)
|
||||
{
|
||||
await StartPolling();
|
||||
StartPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Stop polling
|
||||
StopPolling();
|
||||
|
||||
// Unregister the visibility callback using the same reference
|
||||
if (_dotNetRef != null)
|
||||
{
|
||||
await JsInteropService.UnregisterVisibilityCallback(_dotNetRef);
|
||||
_dotNetRef.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
@inject JsInteropService JsInteropService
|
||||
@implements IDisposable
|
||||
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="@Id" class="outline-0 shadow-sm bg-gray-50 border @(Copied ? "border-green-500 border-2" : "border-gray-300") text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-@(Copied ? "green-500" : "gray-600") dark:placeholder-gray-400 dark:text-white" value="@Value" @onclick="CopyToClipboard" readonly>
|
||||
@if (Label != null)
|
||||
{
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
}
|
||||
|
||||
<div class="relative flex-grow">
|
||||
<input type="text" autocomplete="off" id="@Id" class="outline-0 shadow-sm bg-gray-50 border @(Copied ? "border-green-500 border-2" : "border-gray-300") text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-@(Copied ? "green-500" : "gray-600") dark:placeholder-gray-400 dark:text-white" value="@Value" @onclick="CopyToClipboard" readonly>
|
||||
@if (Copied)
|
||||
{
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-green-500 dark:text-green-400">
|
||||
@@ -24,7 +28,7 @@
|
||||
/// The label for the input.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "Value";
|
||||
public string? Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value to copy to the clipboard.
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
@inject DbService DbService
|
||||
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="flex">
|
||||
<div class="relative flex-grow">
|
||||
<input type="@(_internalShowPassword ? "text" : "password")" id="@Id" autocomplete="off" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 pr-16 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged" placeholder="@Placeholder">
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button type="button" class="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium text-sm dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="TogglePasswordVisibility">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
@if (_internalShowPassword)
|
||||
{
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
}
|
||||
else
|
||||
{
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
|
||||
}
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium text-sm border-l border-gray-300 dark:border-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="ShowPasswordSettings">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="px-3 text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-r-lg text-sm border-l border-gray-300 dark:border-gray-700 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GeneratePassword">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsPasswordSettingsVisible)
|
||||
{
|
||||
<PasswordSettingsPopup
|
||||
PasswordSettings="@_internalPasswordSettings"
|
||||
IsTemporary="true"
|
||||
OnSaveSettings="@HandlePasswordSettingsSaved"
|
||||
OnClose="@ClosePasswordSettings" />
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Id for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Label for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "Password";
|
||||
|
||||
/// <summary>
|
||||
/// Value of the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string?> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder text for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the password is visible in plain text or not.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowPassword { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the password settings popup is visible.
|
||||
/// </summary>
|
||||
private bool IsPasswordSettingsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the password is visible in plain text or not.
|
||||
/// </summary>
|
||||
private bool _internalShowPassword;
|
||||
|
||||
/// <summary>
|
||||
/// Internal copy of the password settings which can be mutated without affecting the global settings.
|
||||
/// </summary>
|
||||
private PasswordSettings _internalPasswordSettings = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the component has been initialized.
|
||||
/// </summary>
|
||||
private bool _hasInitialized;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
_internalShowPassword = ShowPassword;
|
||||
_internalPasswordSettings = DbService.Settings.PasswordSettings;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
if (!_hasInitialized)
|
||||
{
|
||||
_internalShowPassword = ShowPassword;
|
||||
_hasInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the password plain text visibility.
|
||||
/// </summary>
|
||||
private void TogglePasswordVisibility()
|
||||
{
|
||||
_internalShowPassword = !_internalShowPassword;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the password settings popup.
|
||||
/// </summary>
|
||||
private void ShowPasswordSettings()
|
||||
{
|
||||
IsPasswordSettingsVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the password settings popup.
|
||||
/// </summary>
|
||||
private void ClosePasswordSettings()
|
||||
{
|
||||
IsPasswordSettingsVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the local value when the input field changes.
|
||||
/// </summary>
|
||||
private async Task OnInputChanged(ChangeEventArgs e)
|
||||
{
|
||||
Value = e.Value?.ToString() ?? string.Empty;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates current password when password settings have been changed.
|
||||
/// </summary>
|
||||
private async Task HandlePasswordSettingsSaved((PasswordSettings settings, string generatedPassword) args)
|
||||
{
|
||||
_internalPasswordSettings = args.settings;
|
||||
_internalShowPassword = true;
|
||||
Value = args.generatedPassword;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new password.
|
||||
/// </summary>
|
||||
private async Task GeneratePassword()
|
||||
{
|
||||
string newPassword = CredentialService.GenerateRandomPassword(_internalPasswordSettings);
|
||||
|
||||
// Update the local value.
|
||||
Value = newPassword;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
|
||||
// Make password visible when it's (re)generated.
|
||||
_internalShowPassword = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
@inject DbService DbService
|
||||
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="flex">
|
||||
<div class="relative flex-grow">
|
||||
<input type="text" id="@Id" autocomplete="off" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged" placeholder="@Placeholder">
|
||||
</div>
|
||||
<button type="button" id="generate-username-button" class="px-3 text-gray-500 bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-r-lg text-sm dark:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateNewUsername">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Id for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Label for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "Username";
|
||||
|
||||
/// <summary>
|
||||
/// Value of the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<string> ValueChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the generate new username button is clicked.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback OnGenerateNewUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder text for the input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Placeholder { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Invoke data binding ValueChanged param.
|
||||
/// </summary>
|
||||
/// <param name="e"></param>
|
||||
private async Task OnInputChanged(ChangeEventArgs e)
|
||||
{
|
||||
Value = e.Value?.ToString() ?? string.Empty;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke parent assigned method that takes care of generating the username.
|
||||
/// </summary>
|
||||
private async Task GenerateNewUsername()
|
||||
{
|
||||
if (OnGenerateNewUsername.HasDelegate)
|
||||
{
|
||||
await OnGenerateNewUsername.InvokeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div @onkeydown="KeyWasPressed" @onkeydown:preventDefault="@preventDefault" class="@CssClass">
|
||||
@ChildContent
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Callback when Enter key is pressed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback OnEnter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The child content of the component.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public required RenderFragment ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CSS class string to apply to the wrapper div.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string CssClass { get; set; } = "modal-dialog fixed inset-0 z-50 overflow-auto bg-gray-500 bg-opacity-75 flex items-center justify-center";
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the default behavior should be prevented for keyboard events.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool preventDefault { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Handle keyboard events.
|
||||
/// </summary>
|
||||
private async Task KeyWasPressed(KeyboardEventArgs e)
|
||||
{
|
||||
// Listen for Enter key and submit the modal
|
||||
if (e.Key == "Enter")
|
||||
{
|
||||
preventDefault = true;
|
||||
await OnEnter.InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
@inject DbService DbService
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password-generator-settings-modal" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default password generator settings</label>
|
||||
<button type="button" id="password-generator-settings-modal" class="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-primary-700 dark:hover:bg-primary-600" @onclick="OpenSettings">
|
||||
Configure
|
||||
</button>
|
||||
<span class="block text-sm font-normal text-gray-500 truncate dark:text-gray-400 mt-2">
|
||||
Configure the default settings used when generating new passwords. These settings will be used for all new passwords unless overridden for specific entries.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (IsSettingsVisible)
|
||||
{
|
||||
<PasswordSettingsPopup
|
||||
PasswordSettings="@PasswordSettings"
|
||||
IsTemporary="false"
|
||||
OnSaveSettings="@HandlePasswordSettingsSaved"
|
||||
OnClose="@CloseSettings" />
|
||||
}
|
||||
|
||||
@code {
|
||||
private PasswordSettings PasswordSettings { get; set; } = new();
|
||||
private bool IsSettingsVisible { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
PasswordSettings = DbService.Settings.PasswordSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the password settings popup.
|
||||
/// </summary>
|
||||
private void OpenSettings()
|
||||
{
|
||||
IsSettingsVisible = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the password settings popup.
|
||||
/// </summary>
|
||||
private void CloseSettings()
|
||||
{
|
||||
IsSettingsVisible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the password settings saved event.
|
||||
/// </summary>
|
||||
private void HandlePasswordSettingsSaved((PasswordSettings settings, string generatedPassword) args)
|
||||
{
|
||||
// The settings are already saved in the PasswordSettingsPopup component
|
||||
// We just need to update our local state
|
||||
PasswordSettings = args.settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
@using AliasVault.Client.Main.Components.Layout
|
||||
@inject DbService DbService
|
||||
@inject GlobalLoadingService GlobalLoadingService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
|
||||
<ClickOutsideHandler OnClose="OnClose" ContentId="passwordSettingsModal">
|
||||
<ModalWrapper OnEnter="HandleEnterKey">
|
||||
<div id="passwordSettingsModal" class="relative top-20 mx-auto p-5 shadow-lg rounded-md bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-400">
|
||||
<div class="m-2">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Change password generator settings</h3>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label for="password-length" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Password Length: @_workingSettings.Length</label>
|
||||
<input type="range" id="password-length" min="8" max="64"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
@bind="_workingSettings.Length" @oninput="HandleLengthInput">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input id="use-lowercase" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
@bind="_workingSettings.UseLowercase" @bind:after="OnPasswordSettingsChanged">
|
||||
<label for="use-lowercase" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">Include lowercase letters (a-z)</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input id="use-uppercase" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
@bind="_workingSettings.UseUppercase" @bind:after="OnPasswordSettingsChanged">
|
||||
<label for="use-uppercase" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">Include uppercase letters (A-Z)</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input id="use-numbers" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
@bind="_workingSettings.UseNumbers" @bind:after="OnPasswordSettingsChanged">
|
||||
<label for="use-numbers" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">Include numbers (0-9)</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input id="use-special-chars" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
@bind="_workingSettings.UseSpecialChars" @bind:after="OnPasswordSettingsChanged">
|
||||
<label for="use-special-chars" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">Include special characters (!@@#$%^&*)</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input id="use-non-ambiguous" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded dark:bg-gray-700 dark:border-gray-600"
|
||||
@bind="_workingSettings.UseNonAmbiguousChars" @bind:after="OnPasswordSettingsChanged">
|
||||
<label for="use-non-ambiguous" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">Avoid ambiguous characters (1, l, I, 0, O, etc.)</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Preview</label>
|
||||
<div class="mt-1 flex">
|
||||
<CopyPasteFormRow Id="preview-password" Value="@_previewPassword" />
|
||||
<button type="button" class="ml-2 px-3 py-2 text-sm text-gray-500 dark:text-white bg-gray-200 hover:bg-gray-300 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-md dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="RefreshPreview">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex @(IsTemporary ? "justify-between" : "justify-end") pt-4 gap-2">
|
||||
<button type="button" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600" @onclick="OnClose">
|
||||
Cancel
|
||||
</button>
|
||||
@if (IsTemporary)
|
||||
{
|
||||
<button type="button" class="px-4 py-2 bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-white rounded-md hover:bg-gray-300 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500" @onclick="OnSaveTemporary">
|
||||
Use Just Once
|
||||
</button>
|
||||
}
|
||||
<button type="button" id="save-button" class="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-primary-700 dark:hover:bg-primary-600" @onclick="OnSaveGlobal">
|
||||
Save Globally
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</ClickOutsideHandler>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The PasswordSettings to mutate.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public PasswordSettings PasswordSettings { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether temporary change is allowed. If true, component will show both global and temporary options.
|
||||
/// If false, only global settings are available.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool IsTemporary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked when settings have been changed, with a boolean indicating if it's a global save.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<(PasswordSettings Settings, string GeneratedPassword)> OnSaveSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked when popup is closed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback OnClose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local copy of password settings that are currently being edited.
|
||||
/// </summary>
|
||||
private PasswordSettings _workingSettings = new();
|
||||
|
||||
/// <summary>
|
||||
/// The preview password.
|
||||
/// </summary>
|
||||
private string _previewPassword = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Clone the settings to avoid reference issues
|
||||
_workingSettings = new PasswordSettings
|
||||
{
|
||||
Length = PasswordSettings.Length,
|
||||
UseLowercase = PasswordSettings.UseLowercase,
|
||||
UseUppercase = PasswordSettings.UseUppercase,
|
||||
UseNumbers = PasswordSettings.UseNumbers,
|
||||
UseSpecialChars = PasswordSettings.UseSpecialChars,
|
||||
UseNonAmbiguousChars = PasswordSettings.UseNonAmbiguousChars
|
||||
};
|
||||
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the preview password.
|
||||
/// </summary>
|
||||
private void RefreshPreview()
|
||||
{
|
||||
try {
|
||||
_previewPassword = CredentialService.GenerateRandomPassword(_workingSettings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If password generation fails, ignore it. This can happen if the settings are invalid.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle input from the password length input.
|
||||
/// </summary>
|
||||
private void HandleLengthInput(ChangeEventArgs e)
|
||||
{
|
||||
int newLength;
|
||||
if (int.TryParse(e.Value?.ToString(), out newLength))
|
||||
{
|
||||
_workingSettings.Length = newLength;
|
||||
RefreshPreview();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle changes to the password settings.
|
||||
/// </summary>
|
||||
private void OnPasswordSettingsChanged()
|
||||
{
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist changed password settings globally in vault.
|
||||
/// </summary>
|
||||
private async Task OnSaveGlobal()
|
||||
{
|
||||
// Save globally to DB.
|
||||
GlobalLoadingService.Show();
|
||||
var settingsJson = System.Text.Json.JsonSerializer.Serialize(_workingSettings);
|
||||
await DbService.Settings.SetSettingAsync("PasswordGenerationSettings", settingsJson);
|
||||
GlobalLoadingService.Hide();
|
||||
GlobalNotificationService.AddSuccessMessage("Password generation settings updated globally.", true);
|
||||
|
||||
// Notify parent with both settings and the generated password.
|
||||
await OnSaveSettings.InvokeAsync((_workingSettings, _previewPassword));
|
||||
await OnClose.InvokeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not persist changes in vault but just return the new settings to the parent component.
|
||||
/// </summary>
|
||||
private async Task OnSaveTemporary()
|
||||
{
|
||||
await OnSaveSettings.InvokeAsync((_workingSettings, _previewPassword));
|
||||
await OnClose.InvokeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle Enter key press, submits the form based on context.
|
||||
/// </summary>
|
||||
private async Task HandleEnterKey()
|
||||
{
|
||||
if (IsTemporary)
|
||||
{
|
||||
await OnSaveTemporary();
|
||||
}
|
||||
else
|
||||
{
|
||||
await OnSaveGlobal();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/AliasVault.Client/Main/Models/PasswordSettings.cs
Normal file
44
src/AliasVault.Client/Main/Models/PasswordSettings.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="PasswordSettings.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.Client.Main.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for password generation.
|
||||
/// </summary>
|
||||
public class PasswordSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the length of the password.
|
||||
/// </summary>
|
||||
public int Length { get; set; } = 18;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use lowercase letters.
|
||||
/// </summary>
|
||||
public bool UseLowercase { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use uppercase letters.
|
||||
/// </summary>
|
||||
public bool UseUppercase { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use numbers.
|
||||
/// </summary>
|
||||
public bool UseNumbers { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use special characters.
|
||||
/// </summary>
|
||||
public bool UseSpecialChars { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use non-ambiguous characters.
|
||||
/// </summary>
|
||||
public bool UseNonAmbiguousChars { get; set; } = false;
|
||||
}
|
||||
@@ -92,16 +92,10 @@ else
|
||||
<EditEmailFormRow Id="email" Label="Email" @bind-Value="Obj.Alias.Email"></EditEmailFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<div class="relative">
|
||||
<EditFormRow Id="username" Label="Username" @bind-Value="Obj.Username"></EditFormRow>
|
||||
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomUsername">New Username</button>
|
||||
</div>
|
||||
<EditUsernameFormRow Id="username" Label="Username" @bind-Value="Obj.Username" OnGenerateNewUsername="GenerateRandomUsername"></EditUsernameFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<div class="relative">
|
||||
<EditFormRow Id="password" Label="Password" @bind-Value="Obj.Password.Value"></EditFormRow>
|
||||
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">New Password</button>
|
||||
</div>
|
||||
<EditPasswordFormRow Id="password" Label="Password" @bind-Value="Obj.Password.Value" ShowPassword="IsPasswordVisible"></EditPasswordFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,6 +139,7 @@ else
|
||||
private bool EditMode { get; set; }
|
||||
private EditForm EditFormRef { get; set; } = null!;
|
||||
private bool Loading { get; set; } = true;
|
||||
private bool IsPasswordVisible { get; set; } = false;
|
||||
private CredentialEdit Obj { get; set; } = new();
|
||||
private IJSObjectReference? Module;
|
||||
|
||||
@@ -283,6 +278,7 @@ else
|
||||
StateHasChanged();
|
||||
|
||||
Obj = CredentialEdit.FromEntity(await CredentialService.GenerateRandomIdentity(Obj.ToEntity()));
|
||||
IsPasswordVisible = true;
|
||||
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
@@ -317,14 +313,6 @@ else
|
||||
Obj.Username = generator.GenerateUsername(identity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new random password.
|
||||
/// </summary>
|
||||
private void GenerateRandomPassword()
|
||||
{
|
||||
Obj.Password.Value = CredentialService.GenerateRandomPassword();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel the edit operation and navigate back to the credentials view.
|
||||
/// </summary>
|
||||
|
||||
@@ -44,9 +44,6 @@ else
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged"/>
|
||||
|
||||
<div class="bg-white border rounded-lg dark:bg-gray-800 dark:border-gray-700 overflow-hidden mt-6">
|
||||
<div class="px-4 py-2 bg-gray-100 border-b dark:bg-gray-600 dark:border-gray-700">
|
||||
<h2 class="font-semibold text-gray-800 dark:text-gray-200">Inbox</h2>
|
||||
</div>
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-600">
|
||||
@if (EmailList.Count == 0)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Email Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="defaultEmailDomain" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default Email Domain</label>
|
||||
<label for="defaultEmailDomain" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Default email domain</label>
|
||||
<select @bind="DefaultEmailDomain" @bind:after="UpdateDefaultEmailDomain" id="defaultEmailDomain" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
@if (ShowPrivateDomains)
|
||||
{
|
||||
@@ -57,6 +57,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mx-4 mb-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">Password Settings</h3>
|
||||
|
||||
<DefaultPasswordSettings />
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<string> PrivateDomains => Config.PrivateEmailDomains;
|
||||
private List<string> PublicDomains => Config.PublicEmailDomains;
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5067",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
"DOTNET_MODIFIABLE_ASSEMBLIES": "debug",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
}
|
||||
},
|
||||
"http-release": {
|
||||
|
||||
@@ -16,7 +16,6 @@ using System.Threading.Tasks;
|
||||
using AliasClientDb;
|
||||
using AliasVault.Generators.Identity.Implementations.Factories;
|
||||
using AliasVault.Generators.Identity.Models;
|
||||
using AliasVault.Generators.Password.Implementations;
|
||||
using AliasVault.Shared.Models.WebApi.Favicon;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -32,14 +31,33 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
public const string DefaultServiceUrl = "https://";
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random password for a credential.
|
||||
/// Generates a random password for a credential using the specified settings.
|
||||
/// </summary>
|
||||
/// <param name="settings">PasswordSettings model.</param>
|
||||
/// <returns>Random password.</returns>
|
||||
public static string GenerateRandomPassword()
|
||||
public static string GenerateRandomPassword(PasswordSettings settings)
|
||||
{
|
||||
// Generate a random password using a IPasswordGenerator implementation.
|
||||
var passwordGenerator = new SpamOkPasswordGenerator();
|
||||
return passwordGenerator.GenerateRandomPassword();
|
||||
var passwordBuilder = new SpamOK.PasswordGenerator.BasicPasswordBuilder();
|
||||
|
||||
// Sanity check: if all settings are false, then default to use lowercase letters only.
|
||||
if (!settings.UseLowercase && !settings.UseUppercase && !settings.UseNumbers && !settings.UseSpecialChars && !settings.UseNonAmbiguousChars)
|
||||
{
|
||||
settings.UseLowercase = true;
|
||||
}
|
||||
|
||||
// Apply the settings.
|
||||
var password = passwordBuilder
|
||||
.SetLength(settings.Length)
|
||||
.UseLowercaseLetters(settings.UseLowercase)
|
||||
.UseUppercaseLetters(settings.UseUppercase)
|
||||
.UseNumbers(settings.UseNumbers)
|
||||
.UseSpecialChars(settings.UseSpecialChars)
|
||||
.UseNonAmbiguousChars(settings.UseNonAmbiguousChars)
|
||||
.GeneratePassword()
|
||||
.ToString();
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -88,7 +106,8 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
|
||||
while (isEmailTaken && attempts < MaxAttempts);
|
||||
|
||||
// Generate password
|
||||
credential.Passwords.First().Value = GenerateRandomPassword();
|
||||
var passwordSettings = dbService.Settings.PasswordSettings;
|
||||
credential.Passwords.First().Value = GenerateRandomPassword(passwordSettings);
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
@@ -248,6 +248,16 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
|
||||
where TComponent : class =>
|
||||
await jsRuntime.InvokeVoidAsync("window.registerVisibilityCallback", objRef);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the visibility callback to prevent memory leaks.
|
||||
/// </summary>
|
||||
/// <typeparam name="TComponent">Component type.</typeparam>
|
||||
/// <param name="objRef">DotNetObjectReference.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task UnregisterVisibilityCallback<TComponent>(DotNetObjectReference<TComponent> objRef)
|
||||
where TComponent : class =>
|
||||
await jsRuntime.InvokeVoidAsync("window.unregisterVisibilityCallback", objRef);
|
||||
|
||||
/// <summary>
|
||||
/// Symmetrically decrypts a byte array using the provided encryption key.
|
||||
/// </summary>
|
||||
|
||||
@@ -62,6 +62,32 @@ public sealed class SettingsService
|
||||
/// <returns>Credentials sort order as string.</returns>
|
||||
public string CredentialsSortOrder => GetSetting("CredentialsSortOrder", "asc")!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the password settings from the database. If it fails, we use the model's default values.
|
||||
/// </summary>
|
||||
public PasswordSettings PasswordSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsJson = GetSetting<string>("PasswordGenerationSettings");
|
||||
if (!string.IsNullOrEmpty(settingsJson))
|
||||
{
|
||||
// If settings are saved, load them.
|
||||
return System.Text.Json.JsonSerializer.Deserialize<PasswordSettings>(settingsJson) ?? new PasswordSettings();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
// If no settings are saved, return default settings.
|
||||
return new PasswordSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the DefaultEmailDomain setting.
|
||||
/// </summary>
|
||||
@@ -104,6 +130,30 @@ public sealed class SettingsService
|
||||
/// <returns>Task.</returns>
|
||||
public Task SetCredentialsSortOrder(string value) => SetSettingAsync("CredentialsSortOrder", value);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a setting value by key.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to cast the setting to.</typeparam>
|
||||
/// <param name="key">The key of the setting.</param>
|
||||
/// <returns>The setting value cast to type T, or default if not found.</returns>
|
||||
public Task<T?> GetSettingAsync<T>(string key)
|
||||
{
|
||||
return Task.FromResult(GetSetting<T>(key, default));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a setting asynchronously, converting the value to a string so its compatible with the database field.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value being set.</typeparam>
|
||||
/// <param name="key">The key of the setting.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SetSettingAsync<T>(string key, T value)
|
||||
{
|
||||
string stringValue = ConvertToString(value);
|
||||
return SetSettingAsync(key, stringValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the settings service asynchronously.
|
||||
/// </summary>
|
||||
@@ -245,19 +295,6 @@ public sealed class SettingsService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a setting asynchronously, converting the value to a string so its compatible with the database field.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value being set.</typeparam>
|
||||
/// <param name="key">The key of the setting.</param>
|
||||
/// <param name="value">The value to set.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private Task SetSettingAsync<T>(string key, T value)
|
||||
{
|
||||
string stringValue = ConvertToString(value);
|
||||
return SetSettingAsync(key, stringValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set setting value in database.
|
||||
/// </summary>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
@using AliasVault.Client.Main.Components.Forms
|
||||
@using AliasVault.Client.Main.Components.Layout
|
||||
@using AliasVault.Client.Main.Components.Loading
|
||||
@using AliasVault.Client.Main.Components.Settings
|
||||
@using AliasVault.Client.Main.Components.TotpCodes
|
||||
@using AliasVault.Client.Main.Components.Widgets
|
||||
@using AliasVault.Client.Main.Models
|
||||
|
||||
6
src/AliasVault.Client/package-lock.json
generated
6
src/AliasVault.Client/package-lock.json
generated
@@ -280,9 +280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001626",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001626.tgz",
|
||||
"integrity": "sha512-JRW7kAH8PFJzoPCJhLSHgDgKg5348hsQ68aqb+slnzuB5QFERv846oA/mRChmlLAOdEDeOkRn3ynb1gSFnjt3w==",
|
||||
"version": "1.0.30001706",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz",
|
||||
"integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -642,14 +642,6 @@ video {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.bottom-1 {
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.end-1 {
|
||||
inset-inline-end: 0.25rem;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0px;
|
||||
}
|
||||
@@ -706,6 +698,10 @@ video {
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
.m-2 {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.mx-4 {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
@@ -1172,6 +1168,10 @@ video {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@@ -1184,10 +1184,6 @@ video {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
|
||||
@@ -1353,6 +1349,10 @@ video {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.border-l {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.border-l-0 {
|
||||
border-left-width: 0px;
|
||||
}
|
||||
@@ -1719,6 +1719,10 @@ video {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pe-3 {
|
||||
padding-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
@@ -1727,6 +1731,10 @@ video {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.pr-16 {
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
.pr-20 {
|
||||
padding-right: 5rem;
|
||||
}
|
||||
@@ -1735,6 +1743,10 @@ video {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.ps-3 {
|
||||
padding-inline-start: 0.75rem;
|
||||
}
|
||||
|
||||
.pt-16 {
|
||||
padding-top: 4rem;
|
||||
}
|
||||
@@ -1755,14 +1767,6 @@ video {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.pe-3 {
|
||||
padding-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.ps-3 {
|
||||
padding-inline-start: 0.75rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1860,6 +1864,10 @@ video {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -2328,6 +2336,11 @@ video {
|
||||
--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-green-300:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(134 239 172 / var(--tw-ring-opacity));
|
||||
@@ -2506,6 +2519,11 @@ video {
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-primary-700:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-primary-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(123 74 30 / var(--tw-bg-opacity));
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="img/icon-192.png" />
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 dark:bg-gray-900">
|
||||
<body class="bg-gray-100 dark:bg-gray-900" av-disable="true">
|
||||
<div id="loading-screen">
|
||||
<div class="fixed inset-0 flex items-center justify-center px-6 pt-8 pb-8">
|
||||
<div class="w-full max-w-md space-y-4">
|
||||
|
||||
@@ -299,8 +299,39 @@ async function createWebAuthnCredentialAndDeriveKey(username) {
|
||||
}
|
||||
}
|
||||
|
||||
// Store the event listener references.
|
||||
const visibilityChangeHandlers = new Map();
|
||||
|
||||
/**
|
||||
* Registers visibility callback that is invoked when the visibility state of the current page/tab changes.
|
||||
*
|
||||
* @param {any} dotnetHelper
|
||||
*/
|
||||
window.registerVisibilityCallback = function (dotnetHelper) {
|
||||
document.addEventListener("visibilitychange", function () {
|
||||
// Create a named function so we can reference it later for removal.
|
||||
const handler = function() {
|
||||
dotnetHelper.invokeMethodAsync('OnVisibilityChange', !document.hidden);
|
||||
});
|
||||
};
|
||||
|
||||
visibilityChangeHandlers.set(dotnetHelper, handler);
|
||||
document.addEventListener("visibilitychange", handler);
|
||||
|
||||
// Initial call to set the correct initial state.
|
||||
dotnetHelper.invokeMethodAsync('OnVisibilityChange', !document.hidden);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unregisters any previously registered visibility callbacks to prevent memory leaks.
|
||||
*
|
||||
* @param {any} dotnetHelper
|
||||
*/
|
||||
window.unregisterVisibilityCallback = function (dotnetHelper) {
|
||||
// Get the stored handler.
|
||||
const handler = visibilityChangeHandlers.get(dotnetHelper);
|
||||
|
||||
if (handler) {
|
||||
// Remove the event listener with the same function reference.
|
||||
document.removeEventListener("visibilitychange", handler);
|
||||
visibilityChangeHandlers.delete(dotnetHelper);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,16 +18,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -17,21 +17,21 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
// <auto-generated/>
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -0,0 +1,920 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasServerDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations.PostgresqlMigrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContextPostgresql))]
|
||||
[Migration("20250320101427_AddEmailClaimDisabledFlag")]
|
||||
partial class AddEmailClaimDisabledFlag
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
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("boolean");
|
||||
|
||||
b.Property<DateTime?>("LastPasswordChanged")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
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("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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<bool>("Blocked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("PasswordChangedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AliasVaultUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DeviceIdentifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("ExpireDate")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("character varying(45)");
|
||||
|
||||
b.Property<string>("PreviousTokenValue")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AliasVaultUserRefreshTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Browser")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DeviceType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("EventType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("FailureReason")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<bool>("IsSuccess")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsSuspiciousActivity")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("RequestPath")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
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");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("DateSystem")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
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("boolean");
|
||||
|
||||
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("uuid");
|
||||
|
||||
b.Property<bool>("Visible")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
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");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Bytes")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<DateTime>("Date")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
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");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Application")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Exception")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Level")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
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()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("TimeStamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
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("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsOnDemand")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("time without time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TaskRunnerJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressDomain")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("AddressLocal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEmailClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserEncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.Vault", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Client")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
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("bigint");
|
||||
|
||||
b.Property<string>("Salt")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("VaultBlob")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Verifier")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Vaults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("CurrentStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("DesiredStatus")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("Heartbeat")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ServiceName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkerServiceStatuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
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");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
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");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
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")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
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,30 @@
|
||||
// <auto-generated/>
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations.PostgresqlMigrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEmailClaimDisabledFlag : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Disabled",
|
||||
table: "UserEmailClaims",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Disabled",
|
||||
table: "UserEmailClaims");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace AliasServerDb.Migrations.PostgresqlMigrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true)
|
||||
@@ -564,6 +564,9 @@ namespace AliasServerDb.Migrations.PostgresqlMigrations
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
// <auto-generated/>
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -0,0 +1,898 @@
|
||||
// <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.SqliteMigrations
|
||||
{
|
||||
[DbContext(typeof(AliasServerDbContextSqlite))]
|
||||
[Migration("20250320101616_AddEmailClaimDisabledFlag")]
|
||||
partial class AddEmailClaimDisabledFlag
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.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<bool>("Blocked")
|
||||
.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>("Client")
|
||||
.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("INTEGER");
|
||||
|
||||
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()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("TimeStamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Application");
|
||||
|
||||
b.HasIndex("TimeStamp");
|
||||
|
||||
b.ToTable("Logs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("ServerSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<TimeOnly?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsOnDemand")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("RunDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<TimeOnly>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TaskRunnerJobs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressDomain")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressLocal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
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<string>("Client")
|
||||
.HasMaxLength(255)
|
||||
.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("TEXT");
|
||||
|
||||
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")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
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,30 @@
|
||||
// <auto-generated/>
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasServerDb.Migrations.SqliteMigrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEmailClaimDisabledFlag : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Disabled",
|
||||
table: "UserEmailClaims",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Disabled",
|
||||
table: "UserEmailClaims");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace AliasServerDb.Migrations.SqliteMigrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.0")
|
||||
.HasAnnotation("ProductVersion", "9.0.3")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
@@ -550,6 +550,9 @@ namespace AliasServerDb.Migrations.SqliteMigrations
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ public class UserEmailClaim
|
||||
[StringLength(255)]
|
||||
public string AddressDomain { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the email claim has been disabled. Disabled means that
|
||||
/// the email claim was claimed by a user previously, but that user has deleted this alias since.
|
||||
/// Incoming emails addressed to dusabled aliases are rejected by the server. However if the user
|
||||
/// later claims this alias again it will be automatically re-enabled.
|
||||
/// </summary>
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets created timestamp.
|
||||
/// </summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user