Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4c7b44c04a | ||
|
|
b41449f892 | ||
|
|
934d0d9e56 | ||
|
|
99d0da1119 | ||
|
|
c74e05d400 | ||
|
|
844bdab92f | ||
|
|
1345e3c657 | ||
|
|
4fdf7ce92c | ||
|
|
852d9b5e98 | ||
|
|
3c72fa3fde | ||
|
|
b61b747e4b | ||
|
|
1b4389c7d7 | ||
|
|
499d2759ce | ||
|
|
d0140a8ddb | ||
|
|
76dc465032 | ||
|
|
84420104ee | ||
|
|
1109bde521 | ||
|
|
134a173148 | ||
|
|
83be492b3a | ||
|
|
fac72e5a11 | ||
|
|
5eb885da20 | ||
|
|
da4f286757 | ||
|
|
f6db447ad4 | ||
|
|
b472ba749c | ||
|
|
ef68b3b265 | ||
|
|
08d4a8b656 | ||
|
|
93ac131508 | ||
|
|
a7d1536140 | ||
|
|
4fa3fedea2 | ||
|
|
038e8babb1 | ||
|
|
0845477041 | ||
|
|
90156dd1f8 | ||
|
|
fe4b11cf4d | ||
|
|
2cbf234d05 | ||
|
|
a53575b4bf | ||
|
|
697abc6828 | ||
|
|
e96cfa3940 | ||
|
|
61a88e6715 | ||
|
|
e07a35b214 | ||
|
|
4a79fafbb9 | ||
|
|
02b9bff64e |
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
@@ -0,0 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
buy_me_a_coffee: lanedirt
|
||||
59
.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,60 +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
|
||||
|
||||
- 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
|
||||
|
||||
- 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/aliasvault-browser-extension-*-sources.zip
|
||||
|
||||
- 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
|
||||
|
||||
- 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
@@ -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
@@ -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: |
|
||||
dist/aliasvault-browser-extension-*-chrome.zip
|
||||
dist/aliasvault-browser-extension-*-firefox.zip
|
||||
dist/aliasvault-browser-extension-*-edge.zip
|
||||
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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
17
README.md
@@ -10,7 +10,7 @@
|
||||
> AliasVault is an end-to-end encrypted password and (email) alias manager that protects your privacy by creating alternative identities, passwords and email addresses for every website you use. Use the official supported cloud version or self-host AliasVault on your own server with Docker.
|
||||
|
||||
## Quick links
|
||||
- <a href="https://app.aliasvault.net">Try the cloud version 🔥</a> - <a href="https://aliasvault.net?utm_source=gh-readme">Website 🌐</a> - <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation 📚</a> - <a href="#self-hosting">Self-host instructions ⚙️</a>
|
||||
- <a href="https://app.aliasvault.net">Try the cloud version 🔥</a> - <a href="https://aliasvault.net?utm_source=gh-readme">Website 🌐</a> - <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation 📚</a> - <a href="#self-hosting">Self-host instructions ⚙️</a> - <a href="https://aliasvault.net/plugins?utm_source=gh-readme">Browser Extensions 🔌</a>
|
||||
|
||||
### What makes AliasVault unique:
|
||||
- **Zero-knowledge architecture**: All data is end-to-end encrypted on the client and stored in encrypted state on the server. Your master password never leaves your device and the server never has access to your data.
|
||||
@@ -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.13.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
|
||||
@@ -105,10 +105,12 @@ AliasVault is under active development with new features being added regularly.
|
||||
- [x] Built-in email server for aliases
|
||||
- [x] Single-command Docker-based installation
|
||||
- [x] Chrome browser extension
|
||||
- [ ] Firefox browser extension (https://github.com/lanedirt/AliasVault/issues/581)
|
||||
- [ ] Add and associate TOTP MFA tokens to credentials (https://github.com/lanedirt/AliasVault/issues/181)
|
||||
- [ ] Add support for connecting custom user domains to cloud hosted version (https://github.com/lanedirt/AliasVault/issues/485)
|
||||
- [x] Firefox and MS Edge browser extension
|
||||
- [x] Safari and Brave browser extension
|
||||
- [x] Add and associate TOTP MFA tokens to credentials
|
||||
- [ ] 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)
|
||||
|
||||
### Future Plans
|
||||
- [ ] Mobile apps (iOS, Android)
|
||||
@@ -117,6 +119,11 @@ AliasVault is under active development with new features being added regularly.
|
||||
|
||||
Want to suggest a feature? Join our [Discord](https://discord.gg/DsaXMTEtpF) or create an issue on GitHub.
|
||||
|
||||
### Support the mission
|
||||
Your donation helps me dedicate more time and resources to improving AliasVault, making the internet safer for everyone!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
## Tech Stack & Security
|
||||
|
||||
AliasVault is built with a modern, secure, and scalable technology stack, ensuring robust encryption and privacy protection.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -18,4 +18,8 @@ npm run zip:firefox
|
||||
|
||||
# Build the Edge extension (saves in dist/edge-mv3)
|
||||
npm run zip:edge
|
||||
|
||||
# Build the Safari extension (saves in dist/safari-mv2 which is referenced by the dist/safari-xcode/AliasVault.xcodeproj project)
|
||||
npm run build:safari
|
||||
# Open the dist/safari-xcode/AliasVault.xcodeproj project in MacOS Xcode and run the project. This will install the extension to your Safari browser locally.
|
||||
```
|
||||
|
||||
34
browser-extension/package-lock.json
generated
@@ -9,9 +9,11 @@
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
@@ -1473,6 +1475,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz",
|
||||
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2056,10 +2070,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
||||
"devOptional": true,
|
||||
"version": "22.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@@ -9114,6 +9127,18 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/otpauth": {
|
||||
"version": "9.3.6",
|
||||
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.6.tgz",
|
||||
"integrity": "sha512-eIcCvuEvcAAPHxUKC9Q4uCe0Fh/yRc5jv9z+f/kvyIF2LPrhgAOuLB7J9CssGYhND/BL8M9hlHBTFmffpoQlMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.6.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/hectorm/otpauth?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -12223,7 +12248,6 @@
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unimport": {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"build:chrome": "wxt build -b chrome",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"build:edge": "wxt build -b edge",
|
||||
"build:safari": "wxt build -b safari",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint src",
|
||||
@@ -24,9 +25,11 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
|
||||
62
browser-extension/safari-xcode/.gitignore
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.Safari.web-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SafariWebExtensionHandler.swift
|
||||
// AliasVault Extension
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import SafariServices
|
||||
import os.log
|
||||
|
||||
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
|
||||
|
||||
func beginRequest(with context: NSExtensionContext) {
|
||||
let request = context.inputItems.first as? NSExtensionItem
|
||||
|
||||
let profile: UUID?
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
|
||||
} else {
|
||||
profile = request?.userInfo?["profile"] as? UUID
|
||||
}
|
||||
|
||||
let message: Any?
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
message = request?.userInfo?[SFExtensionMessageKey]
|
||||
} else {
|
||||
message = request?.userInfo?["message"]
|
||||
}
|
||||
|
||||
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
|
||||
|
||||
let response = NSExtensionItem()
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
|
||||
} else {
|
||||
response.userInfo = [ "message": [ "echo": message ] ]
|
||||
}
|
||||
|
||||
context.completeRequest(returningItems: [ response ], completionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */; };
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAA2D81A9F7006174AB /* Base */; };
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAC2D81A9F7006174AB /* Icon.png */; };
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAE2D81A9F7006174AB /* Style.css */; };
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB02D81A9F7006174AB /* Script.js */; };
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFB22D81A9F7006174AB /* ViewController.swift */; };
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB52D81A9F7006174AB /* Base */; };
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB72D81A9F8006174AB /* Assets.xcassets */; };
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */; };
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD32D81A9F8006174AB /* background.js */; };
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD42D81A9F8006174AB /* popup.html */; };
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD52D81A9F8006174AB /* chunks */; };
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD62D81A9F8006174AB /* content-scripts */; };
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD72D81A9F8006174AB /* manifest.json */; };
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD82D81A9F8006174AB /* icon */; };
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD92D81A9F8006174AB /* assets */; };
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFDA2D81A9F8006174AB /* src */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = CE0CAFBF2D81A9F8006174AB;
|
||||
remoteInfo = "AliasVault Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AliasVault.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = ../Base.lproj/Main.html; sourceTree = "<group>"; };
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = "<group>"; };
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = Style.css; sourceTree = "<group>"; };
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Script.js; sourceTree = "<group>"; };
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
CE0CAFB52D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "AliasVault Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault_Extension.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = background.js; path = "../../../dist/safari-mv2/background.js"; sourceTree = "<group>"; };
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = popup.html; path = "../../../dist/safari-mv2/popup.html"; sourceTree = "<group>"; };
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = chunks; path = "../../../dist/safari-mv2/chunks"; sourceTree = "<group>"; };
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "content-scripts"; path = "../../../dist/safari-mv2/content-scripts"; sourceTree = "<group>"; };
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = "../../../dist/safari-mv2/manifest.json"; sourceTree = "<group>"; };
|
||||
CE0CAFD82D81A9F8006174AB /* icon */ = {isa = PBXFileReference; lastKnownFileType = folder; name = icon; path = "../../../dist/safari-mv2/icon"; sourceTree = "<group>"; };
|
||||
CE0CAFD92D81A9F8006174AB /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "../../../dist/safari-mv2/assets"; sourceTree = "<group>"; };
|
||||
CE0CAFDA2D81A9F8006174AB /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = "../../../dist/safari-mv2/src"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CE0CAF9A2D81A9F7006174AB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */,
|
||||
CE0CAFA42D81A9F7006174AB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA42D81A9F7006174AB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */,
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */,
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */,
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */,
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */,
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */,
|
||||
);
|
||||
path = AliasVault;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */,
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */,
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */,
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */,
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */,
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */,
|
||||
);
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */,
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */,
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */,
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */,
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */,
|
||||
CE0CAFD82D81A9F8006174AB /* icon */,
|
||||
CE0CAFD92D81A9F8006174AB /* assets */,
|
||||
CE0CAFDA2D81A9F8006174AB /* src */,
|
||||
);
|
||||
name = Resources;
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */;
|
||||
buildPhases = (
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */,
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */,
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */,
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */,
|
||||
);
|
||||
name = AliasVault;
|
||||
productName = AliasVault;
|
||||
productReference = CE0CAFA32D81A9F7006174AB /* AliasVault.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */;
|
||||
buildPhases = (
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */,
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */,
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "AliasVault Extension";
|
||||
productName = "AliasVault Extension";
|
||||
productReference = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CE0CAF9B2D81A9F7006174AB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
TargetAttributes = {
|
||||
CE0CAFA22D81A9F7006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CE0CAF9A2D81A9F7006174AB;
|
||||
productRefGroup = CE0CAFA42D81A9F7006174AB /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */,
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */,
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */,
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */,
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */,
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */,
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */,
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */,
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */,
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */,
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */,
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */,
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */,
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */;
|
||||
targetProxy = CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.html;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFB52D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.Extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.15.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFD12D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = AliasVault/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.15.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */,
|
||||
CE0CAFD12D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Override point for customization after application launch.
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 160 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>AliasVault</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
|
||||
<link rel="stylesheet" href="../Style.css">
|
||||
<script src="../Script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<img src="../Icon.png" width="128" height="128" alt="AliasVault Icon">
|
||||
<p class="state-unknown">To enable AliasVault’s browser extension, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-on">AliasVault’s browser extension is currently enabled in Safari. If you wish to turn it off, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-off">AliasVault’s browser extension is currently disabled in Safari. If you wish to turn it on, go to the Safari Extensions preferences.</p>
|
||||
<button class="open-preferences">Open Safari Extensions Preferences…</button>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19085"/>
|
||||
<plugIn identifier="com.apple.WebKit2IBPlugin" version="19085"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="AliasVault" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="AliasVault" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About AliasVault" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Hide AliasVault" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit AliasVault" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="AliasVault Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="76" y="-134"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="AliasVault" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="425" height="325"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<wkWebViewConfiguration key="configuration">
|
||||
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
||||
<wkPreferences key="preferences"/>
|
||||
</wkWebViewConfiguration>
|
||||
</wkWebView>
|
||||
</subviews>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SFSafariWebExtensionConverterVersion</key>
|
||||
<string>15.4</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,22 @@
|
||||
function show(enabled, useSettingsInsteadOfPreferences) {
|
||||
if (useSettingsInsteadOfPreferences) {
|
||||
document.getElementsByClassName('state-on')[0].innerText = "AliasVault's Safari browser extension is succesfully enabled. If you wish to turn it off, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-off')[0].innerText = "AliasVault's Safari browser extension is currently disabled. If you wish to turn it on, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-unknown')[0].innerText = "To enable AliasVault's Safari browser extension, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('open-preferences')[0].innerText = "Open Safari Extensions Preferences…";
|
||||
}
|
||||
|
||||
if (typeof enabled === "boolean") {
|
||||
document.body.classList.toggle(`state-on`, enabled);
|
||||
document.body.classList.toggle(`state-off`, !enabled);
|
||||
} else {
|
||||
document.body.classList.remove(`state-on`);
|
||||
document.body.classList.remove(`state-off`);
|
||||
}
|
||||
}
|
||||
|
||||
function openPreferences() {
|
||||
webkit.messageHandlers.controller.postMessage("open-preferences");
|
||||
}
|
||||
|
||||
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);
|
||||
@@ -0,0 +1,44 @@
|
||||
* {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
--spacing: 20px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing);
|
||||
margin: 0 calc(var(--spacing) * 2);
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font: -apple-system-short-body;
|
||||
font-family: -apple-system-short-body, system-ui;
|
||||
}
|
||||
|
||||
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-on :is(.state-off, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-off :is(.state-on, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SafariServices
|
||||
import WebKit
|
||||
|
||||
let extensionBundleIdentifier = "net.aliasvault.safari.extension"
|
||||
|
||||
class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
|
||||
|
||||
@IBOutlet var webView: WKWebView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
|
||||
self.webView.configuration.userContentController.add(self, name: "controller")
|
||||
|
||||
self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
|
||||
guard let state = state, error == nil else {
|
||||
// Insert code to inform the user that something went wrong.
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if #available(macOS 13, *) {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), true)")
|
||||
} else {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), false)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
if (message.body as! String != "open-preferences") {
|
||||
return;
|
||||
}
|
||||
|
||||
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
// Show manual instructions in case opening the preferences fails due to restricted permissions.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Safari Extensions Settings"
|
||||
alert.informativeText = """
|
||||
Please follow these steps to enable the extension:
|
||||
1. Open Safari
|
||||
2. Click Safari > Settings in the menu bar
|
||||
3. Go to Extensions
|
||||
4. Find and enable "AliasVault"
|
||||
"""
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.runModal()
|
||||
}
|
||||
else {
|
||||
// Close app
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
browser-extension/safari-xcode/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
This folder contains the Xcode project used to publish the Safari version of the AliasVault browser extension to Apple.
|
||||
|
||||
This project was created using the `safari-web-extension-converter` tool. This XCode project is a simple wrapper around the
|
||||
WXT React browser extension, which is required by Apple in order to package and submit a Safari extension.
|
||||
|
||||
For more information see:
|
||||
- https://developer.apple.com/documentation/safariservices/converting-a-web-extension-for-safari
|
||||
- https://developer.apple.com/documentation/safariservices/running-your-safari-web-extension
|
||||
|
||||
To recreate this project, run the following command in the browser-extension root directory:
|
||||
|
||||
```bash
|
||||
# Build the Safari extension via the normal build process (outputs in dist/safari-mv2)
|
||||
npm run build:safari
|
||||
|
||||
# Convert the safari extension to an Xcode project (requires MacOS/XCode command line interface)
|
||||
xcrun safari-web-extension-converter --bundle-identifier net.aliasvault.safari --macos-only dist/safari-mv2 --project-location safari-xcode --force
|
||||
|
||||
# After the Xcode project is opened, you can run the extension by clicking the "Run" button in the top left corner of the Xcode window.
|
||||
# This will install the extension to your Safari browser and allow you to run it.
|
||||
```
|
||||
|
||||
> Note: This project does not need to be recreated when the extension is updated. It loads all extension files from the dist/safari-mv2 directory that is created by the `build:safari` command. To update the extension and/or publish a new version:
|
||||
> 1. Run `npm run build:safari` to rebuild the Safari extension
|
||||
> 2. Open this Xcode project and rebuild it to get the latest version
|
||||
> 3. Submit the extension to Apple for review via Xcode:
|
||||
> - Select the "Archive" option from the Product menu
|
||||
> - Select the newly created archive and click "Distribute App"
|
||||
> - Select "Distribute" and follow the instructions to submit to App Store Connect
|
||||
@@ -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 { 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,11 +12,12 @@ 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)
|
||||
);
|
||||
|
||||
// Listen for messages using webext-bridge
|
||||
onMessage('CHECK_AUTH_STATUS', () => handleCheckAuthStatus());
|
||||
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
@@ -24,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,24 @@ 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.
|
||||
*/
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> {
|
||||
const username = await storage.getItem('local:username');
|
||||
const accessToken = await storage.getItem('local:accessToken');
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData === null;
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the vault in browser storage.
|
||||
@@ -241,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.
|
||||
*/
|
||||
|
||||
@@ -1,83 +1,109 @@
|
||||
import './contentScript/style.css';
|
||||
import { FormDetector } from '../utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupDisabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { canShowPopup, injectIcon } from './contentScript/Form';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { injectIcon, popupDebounceTimeHasPassed } from './contentScript/Form';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
import { BoolResponse as messageBoolResponse } from '../utils/types/messaging/BoolResponse';
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { createShadowRootUi } from 'wxt/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
cssInjectionMode: 'ui',
|
||||
allFrames: true,
|
||||
matchAboutBlank: true,
|
||||
runAt: 'document_start',
|
||||
|
||||
/**
|
||||
* Main entry point for the content script.
|
||||
*/
|
||||
main(ctx) {
|
||||
async main(ctx) {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for input field focus
|
||||
document.addEventListener('focusin', async (e) => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
// Create a shadow root UI for isolation
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'aliasvault-ui',
|
||||
position: 'inline',
|
||||
anchor: 'body',
|
||||
/**
|
||||
* Handle mount.
|
||||
*/
|
||||
onMount(container) {
|
||||
/**
|
||||
* Handle input field focus.
|
||||
*/
|
||||
const handleFocusIn = async (e: FocusEvent) : Promise<void> => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLInputElement;
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url'];
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (target.tagName === 'INPUT' &&
|
||||
textInputTypes.includes(target.type) &&
|
||||
!target.dataset.aliasvaultIgnore) {
|
||||
const formDetector = new FormDetector(document, target);
|
||||
const target = e.target as HTMLInputElement;
|
||||
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url'];
|
||||
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
if (target.tagName === 'INPUT' && textInputTypes.includes(target.type) && !target.dataset.aliasvaultIgnore) {
|
||||
const formDetector = new FormDetector(document, target);
|
||||
if (!formDetector.containsLoginForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
injectIcon(target);
|
||||
injectIcon(target, container);
|
||||
|
||||
const isDisabled = await isAutoShowPopupDisabled();
|
||||
const canShow = canShowPopup();
|
||||
// Only show popup if its enabled and debounce time has passed.
|
||||
if (await isAutoShowPopupEnabled() && popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(target, container);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Only show popup if it's not disabled and the popup can be shown
|
||||
if (!isDisabled && canShow) {
|
||||
openAutofillPopup(target);
|
||||
}
|
||||
}
|
||||
// Listen for input field focus in the main document
|
||||
document.addEventListener('focusin', handleFocusIn);
|
||||
|
||||
// Listen for popstate events (back/forward navigation)
|
||||
window.addEventListener('popstate', () => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeExistingPopup(container);
|
||||
});
|
||||
|
||||
// Listen for messages from the background script
|
||||
onMessage('OPEN_AUTOFILL_POPUP', async (message: { data: { elementIdentifier: string } }) : Promise<messageBoolResponse> => {
|
||||
const { data } = message;
|
||||
const { elementIdentifier } = data;
|
||||
|
||||
if (!elementIdentifier) {
|
||||
return { success: false, error: 'No element identifier provided' };
|
||||
}
|
||||
|
||||
const target = document.getElementById(elementIdentifier) ?? document.getElementsByName(elementIdentifier)[0];
|
||||
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
return { success: false, error: 'Target element is not an input field' };
|
||||
}
|
||||
|
||||
const formDetector = new FormDetector(document, target);
|
||||
|
||||
if (!formDetector.containsLoginForm(true)) {
|
||||
return { success: false, error: 'No form found' };
|
||||
}
|
||||
|
||||
injectIcon(target, container);
|
||||
openAutofillPopup(target, container);
|
||||
return { success: true };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for popstate events (back/forward navigation)
|
||||
window.addEventListener('popstate', () => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeExistingPopup();
|
||||
});
|
||||
|
||||
// Listen for messages from the background script
|
||||
onMessage('OPEN_AUTOFILL_POPUP', async (message: { data: { elementIdentifier: string } }) : Promise<messageBoolResponse> => {
|
||||
const { data } = message;
|
||||
const { elementIdentifier } = data;
|
||||
|
||||
if (!elementIdentifier) {
|
||||
return { success: false, error: 'No element identifier provided' };
|
||||
}
|
||||
|
||||
const target = document.getElementById(elementIdentifier) ?? document.getElementsByName(elementIdentifier)[0];
|
||||
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
return { success: false, error: 'Target element is not an input field' };
|
||||
}
|
||||
|
||||
const formDetector = new FormDetector(document, target);
|
||||
|
||||
if (!formDetector.containsLoginForm(true)) {
|
||||
return { success: false, error: 'No form found' };
|
||||
}
|
||||
|
||||
injectIcon(target);
|
||||
openAutofillPopup(target);
|
||||
return { success: true };
|
||||
});
|
||||
// Mount the UI to create the shadow root
|
||||
ui.autoMount();
|
||||
},
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { FormDetector } from "../../utils/formDetector/FormDetector";
|
||||
import { FormFiller } from "../../utils/formDetector/FormFiller";
|
||||
import { Credential } from "../../utils/types/Credential";
|
||||
import { openAutofillPopup } from "./Popup";
|
||||
|
||||
/**
|
||||
* Global timestamp to track popup debounce time.
|
||||
* This is used to not show the popup again for a specific amount of time.
|
||||
@@ -13,7 +14,7 @@ let popupDebounceTime = 0;
|
||||
/**
|
||||
* Check if popup can be shown based on debounce time.
|
||||
*/
|
||||
export function canShowPopup() : boolean {
|
||||
export function popupDebounceTimeHasPassed() : boolean {
|
||||
if (Date.now() < popupDebounceTime) {
|
||||
return false;
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export function fillCredential(credential: Credential, input: HTMLInputElement)
|
||||
/**
|
||||
* Inject icon for a focused input element
|
||||
*/
|
||||
export function injectIcon(input: HTMLInputElement): void {
|
||||
export function injectIcon(input: HTMLInputElement, container: HTMLElement): void {
|
||||
const aliasvaultIconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
||||
@@ -64,18 +65,7 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
</svg>`;
|
||||
|
||||
const ICON_HTML = `
|
||||
<div class="aliasvault-input-icon" style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
">
|
||||
<div class="av-input-icon">
|
||||
<img src="data:image/svg+xml;base64,${btoa(aliasvaultIconSvg)}" style="width: 100%; height: 100%;" />
|
||||
</div>
|
||||
`;
|
||||
@@ -86,20 +76,12 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
}
|
||||
|
||||
// Create an overlay container at document level if it doesn't exist
|
||||
let overlayContainer = document.getElementById('aliasvault-overlay-container');
|
||||
let overlayContainer = container.querySelector('#aliasvault-overlay-container');
|
||||
if (!overlayContainer) {
|
||||
overlayContainer = document.createElement('div');
|
||||
overlayContainer = document.createElement('div') as HTMLElement;
|
||||
overlayContainer.id = 'aliasvault-overlay-container';
|
||||
overlayContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483640;
|
||||
`;
|
||||
document.body.appendChild(overlayContainer);
|
||||
overlayContainer.className = 'av-overlay-container';
|
||||
container.appendChild(overlayContainer);
|
||||
}
|
||||
|
||||
// Create the icon element from the HTML template
|
||||
@@ -131,7 +113,7 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTimeout(() => input.focus(), 0);
|
||||
openAutofillPopup(input);
|
||||
openAutofillPopup(input, container);
|
||||
});
|
||||
|
||||
// Append the icon to the overlay container
|
||||
@@ -179,51 +161,53 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
* 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 }));
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Check if the current theme is dark.
|
||||
*/
|
||||
export function isDarkMode(): boolean {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
424
browser-extension/src/entrypoints/contentScript/style.css
Normal file
@@ -0,0 +1,424 @@
|
||||
/* AliasVault Content Script Styles */
|
||||
body {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Base Popup Styles */
|
||||
.av-popup {
|
||||
position: absolute;
|
||||
z-index: 2147483646;
|
||||
background-color: rgb(31, 41, 55);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
width: 320px;
|
||||
border: 1px solid rgb(55, 65, 81);
|
||||
border-radius: 4px;
|
||||
max-width: 90vw;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Loading Popup Styles */
|
||||
.av-loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.av-loading-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.av-loading-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* Credential List Styles */
|
||||
.av-credential-list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4b5563 #1f2937;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.av-credential-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.av-credential-item:hover {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
|
||||
.av-credential-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-grow: 1;
|
||||
padding: 10px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.av-credential-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.av-credential-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.av-service-name {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
color: #f3f4f6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-service-details {
|
||||
font-size: 0.85em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #9ca3af;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-popout-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
margin-right: 16px;
|
||||
opacity: 0.6;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
color: #ffffff;
|
||||
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-popout-icon:hover {
|
||||
opacity: 1;
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.av-no-matches {
|
||||
padding-left: 10px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.av-divider {
|
||||
height: 1px;
|
||||
background: #374151;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Action Container */
|
||||
.av-action-container {
|
||||
display: flex;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.av-button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.av-button:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-button-primary {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-button-primary:hover {
|
||||
background-color: #d68338;
|
||||
}
|
||||
|
||||
.av-button-close {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.av-button-close:hover {
|
||||
background-color: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Search Input */
|
||||
.av-search-input {
|
||||
flex: 2;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
color: #e5e7eb;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
border: 1px solid #4b5563;
|
||||
outline: none;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.av-search-input::placeholder {
|
||||
color: #bdbebe;
|
||||
}
|
||||
|
||||
.av-search-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
/* Vault Locked Popup */
|
||||
.av-vault-locked {
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-vault-locked:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-vault-locked-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
width: 100%;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.av-vault-locked-message {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
flex-grow: 1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-vault-locked-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding-right: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #d68338;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.av-vault-locked-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
border: 1px solid #6f6f6f;
|
||||
}
|
||||
|
||||
/* Create Name Popup */
|
||||
.av-create-popup-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2147483647;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.av-create-popup {
|
||||
position: relative;
|
||||
z-index: 1000000000;
|
||||
background: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06),
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
padding: 24px;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-create-popup.show {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.av-create-popup-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 6px;
|
||||
background: #374151;
|
||||
color: #f8f9fa;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.av-create-popup-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.av-create-popup-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.av-create-popup-cancel {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #374151;
|
||||
background: transparent;
|
||||
color: #f8f9fa;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.av-create-popup-cancel:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-save {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: #d68338;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.av-create-popup-save:hover {
|
||||
background: #c97731;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* SVG Icons */
|
||||
.av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.av-icon-lock {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Form Icon Styles */
|
||||
.av-overlay-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483640;
|
||||
}
|
||||
|
||||
.av-input-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
@@ -83,7 +83,8 @@ const App: React.FC = () => {
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: 'calc(100vh - 120px)',
|
||||
height: 'calc(100% - 120px)',
|
||||
maxHeight: '600px',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
|
||||
@@ -135,7 +135,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
href={`https://spamok.com/${email.split('@')[0]}/${mail.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex justify-between items-center p-2 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -152,7 +152,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
<Link
|
||||
key={mail.id}
|
||||
to={`/emails/${mail.id}`}
|
||||
className={`flex justify-between items-center p-2 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -95,7 +95,10 @@ const Header: React.FC<HeaderProps> = ({
|
||||
>
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">AliasVault</h1>
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
|
||||
{!import.meta.env.SAFARI && (
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { TotpCode } from '../../../utils/types/TotpCode';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
|
||||
type TotpViewerProps = {
|
||||
credentialId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows TOTP codes for a credential.
|
||||
*/
|
||||
export const TotpViewer: React.FC<TotpViewerProps> = ({ credentialId }) => {
|
||||
const [totpCodes, setTotpCodes] = useState<TotpCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentCodes, setCurrentCodes] = useState<Record<string, string>>({});
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Gets the remaining seconds for the TOTP code.
|
||||
*/
|
||||
const getRemainingSeconds = (step = 30): number => {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: 'dummy', // We only need this for timing calculations
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: step
|
||||
});
|
||||
return totp.period - (Math.floor(Date.now() / 1000) % totp.period);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the remaining percentage for the TOTP code.
|
||||
*/
|
||||
const getRemainingPercentage = (): number => {
|
||||
const remaining = getRemainingSeconds();
|
||||
// Invert the percentage so it counts down instead of up
|
||||
return Math.floor(((30.0 - remaining) / 30.0) * 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a TOTP code for a given secret key.
|
||||
*/
|
||||
const generateTotpCode = (secretKey: string): string => {
|
||||
try {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: secretKey,
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30
|
||||
});
|
||||
return totp.generate();
|
||||
} catch (error) {
|
||||
console.error('Error generating TOTP code:', error);
|
||||
return 'Error';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies a TOTP code to the clipboard.
|
||||
*/
|
||||
const copyToClipboard = async (code: string, id: string): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopiedId(id);
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopiedId(null);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the TOTP codes for the credential.
|
||||
*/
|
||||
const loadTotpCodes = async (): Promise<void> => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const codes = dbContext.sqliteClient.getTotpCodesForCredential(credentialId);
|
||||
setTotpCodes(codes);
|
||||
} catch (error) {
|
||||
console.error('Error loading TOTP codes:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTotpCodes();
|
||||
}, [credentialId, dbContext?.sqliteClient]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Updates the current TOTP codes.
|
||||
*/
|
||||
const updateTotpCodes = (prevCodes: Record<string, string>): Record<string, string> => {
|
||||
const newCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
const generatedCode = generateTotpCode(code.SecretKey);
|
||||
// Only update if we have a valid code
|
||||
if (generatedCode !== 'Error') {
|
||||
newCodes[code.Id] = generatedCode;
|
||||
} else {
|
||||
// Keep the previous code if there's an error
|
||||
newCodes[code.Id] = prevCodes[code.Id] ?? 'Error';
|
||||
}
|
||||
});
|
||||
return newCodes;
|
||||
};
|
||||
|
||||
// Generate initial codes
|
||||
const initialCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
initialCodes[code.Id] = generateTotpCode(code.SecretKey);
|
||||
});
|
||||
setCurrentCodes(initialCodes);
|
||||
|
||||
// Set up interval to refresh codes
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentCodes(updateTotpCodes);
|
||||
}, 1000);
|
||||
|
||||
// Clean up interval on unmount or when totpCodes change
|
||||
return () : void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [totpCodes]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Two-factor authentication</h2>
|
||||
Loading TOTP codes...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (totpCodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-base font-semibold text-gray-900 dark:text-white">Two-factor authentication</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{totpCodes.map(totpCode => (
|
||||
<button
|
||||
key={totpCode.Id}
|
||||
className={`w-full text-left p-2 ps-3 pe-3 rounded bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700`}
|
||||
onClick={() => copyToClipboard(currentCodes[totpCode.Id], totpCode.Id)}
|
||||
aria-label={`Copy ${totpCode.Name} code`}
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{totpCode.Name}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{currentCodes[totpCode.Id]}
|
||||
</span>
|
||||
<div className="text-xs">
|
||||
{copiedId === totpCode.Id ? (
|
||||
<span className="text-green-600 dark:text-green-400">Copied!</span>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400">{getRemainingSeconds()}s</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1 h-6 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div
|
||||
className="bg-blue-600 rounded-full transition-all"
|
||||
style={{ height: `${getRemainingPercentage()}%`, width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCall
|
||||
import { useDb } from './DbContext';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
type AuthContextType = {
|
||||
isLoggedIn: boolean;
|
||||
@@ -66,6 +67,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
*/
|
||||
const login = useCallback(async () : Promise<void> => {
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
134
browser-extension/src/entrypoints/popup/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { createContext, useContext, useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Theme type.
|
||||
*/
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
/**
|
||||
* Theme preference key in storage.
|
||||
*/
|
||||
const THEME_PREFERENCE_KEY = 'local:theme';
|
||||
|
||||
/**
|
||||
* Theme context type.
|
||||
*/
|
||||
type ThemeContextType = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme context.
|
||||
*/
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Theme provider
|
||||
*/
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
/**
|
||||
* Theme state that can be 'light', 'dark', or 'system'.
|
||||
*/
|
||||
const [theme, setTheme] = useState<Theme>('system');
|
||||
|
||||
/**
|
||||
* Tracks whether dark mode is active (based on theme or system preference).
|
||||
*/
|
||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load theme setting from storage.
|
||||
*/
|
||||
const loadTheme = async () : Promise<void> => {
|
||||
const savedTheme = await getTheme();
|
||||
setTheme(savedTheme);
|
||||
};
|
||||
loadTheme();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set the theme and save to storage.
|
||||
*/
|
||||
const updateTheme = useCallback((newTheme: Theme): void => {
|
||||
setTheme(newTheme);
|
||||
setStoredTheme(newTheme);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get the theme from storage.
|
||||
*/
|
||||
const getTheme = async (): Promise<Theme> => {
|
||||
return (await storage.getItem(THEME_PREFERENCE_KEY) as Theme) || 'system';
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the theme in storage.
|
||||
*/
|
||||
const setStoredTheme = async (theme: Theme): Promise<void> => {
|
||||
await storage.setItem(THEME_PREFERENCE_KEY, theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* Effect to apply theme to document and handle system preference changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Update the dark mode status.
|
||||
*/
|
||||
const updateDarkMode = (): void => {
|
||||
if (theme === 'system') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setIsDarkMode(prefersDark);
|
||||
document.documentElement.classList.toggle('dark', prefersDark);
|
||||
} else {
|
||||
const isDark = theme === 'dark';
|
||||
setIsDarkMode(isDark);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateDarkMode();
|
||||
|
||||
// Listen for system preference changes if using 'system' theme
|
||||
if (theme === 'system') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
/**
|
||||
* Update the dark mode status when the system preference changes.
|
||||
*/
|
||||
const handler = () : void => updateDarkMode();
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () : void => mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme: updateTheme,
|
||||
isDarkMode,
|
||||
}),
|
||||
[theme, isDarkMode, updateTheme]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use theme state
|
||||
*/
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -6,14 +6,6 @@
|
||||
<title>AliasVault</title>
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
<script>
|
||||
// Check if expanded=true is in the URL, which means the popup was opened in expanded mode with unlimited width.
|
||||
// If not, set the width to 350px to force the default popup to a fixed width.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.get('expanded')) {
|
||||
document.documentElement.classList.add('max-w-[350px]');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,6 +4,11 @@ import { AuthProvider } from './context/AuthContext';
|
||||
import { WebApiProvider } from './context/WebApiContext';
|
||||
import { DbProvider } from './context/DbContext';
|
||||
import { LoadingProvider } from './context/LoadingContext';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import { setupExpandedMode } from '../../utils/ExpandedMode';
|
||||
|
||||
// Run before React initializes to ensure the popup is always a fixed width except for when explicitly expanded.
|
||||
setupExpandedMode();
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
@@ -11,7 +16,9 @@ root.render(
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<App />
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { GLOBAL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '../../contentScript/Popup';
|
||||
|
||||
type ApiOption = {
|
||||
label: string;
|
||||
@@ -19,6 +20,7 @@ const AuthSettings: React.FC = () => {
|
||||
const [selectedOption, setSelectedOption] = useState<string>('');
|
||||
const [customUrl, setCustomUrl] = useState<string>('');
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -27,6 +29,15 @@ const AuthSettings: React.FC = () => {
|
||||
const loadStoredSettings = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
const clientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
const globallyEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
|
||||
|
||||
if (dismissUntil) {
|
||||
setIsGloballyEnabled(false);
|
||||
} else {
|
||||
setIsGloballyEnabled(globallyEnabled);
|
||||
}
|
||||
|
||||
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === apiUrl);
|
||||
|
||||
if (matchingOption) {
|
||||
@@ -74,6 +85,23 @@ const AuthSettings: React.FC = () => {
|
||||
await storage.setItem('local:clientUrl', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle global popup.
|
||||
*/
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !isGloballyEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
if (newGloballyEnabled) {
|
||||
// Reset all disabled sites when enabling globally
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}
|
||||
|
||||
setIsGloballyEnabled(newGloballyEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-6">
|
||||
@@ -124,6 +152,23 @@ const AuthSettings: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
isGloballyEnabled
|
||||
? 'bg-green-200 text-green-800 hover:bg-green-300 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
|
||||
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
||||
}`}
|
||||
>
|
||||
{isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Credential } from '../../../utils/types/Credential';
|
||||
import { Buffer } from 'buffer';
|
||||
import { FormInputCopyToClipboard } from '../components/FormInputCopyToClipboard';
|
||||
import { EmailPreview } from '../components/EmailPreview';
|
||||
import { TotpViewer } from '../components/TotpViewer';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
/**
|
||||
@@ -100,7 +101,7 @@ const CredentialDetails: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
@@ -147,14 +148,14 @@ const CredentialDetails: React.FC = () => {
|
||||
{credential.Email && (
|
||||
<>
|
||||
{isEmailDomainSupported(credential.Email) && (
|
||||
<div className="mt-6">
|
||||
<EmailPreview
|
||||
email={credential.Email}
|
||||
/>
|
||||
</div>
|
||||
<EmailPreview
|
||||
email={credential.Email}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<TotpViewer credentialId={credential.Id} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DISABLED_SITES_KEY, GLOBAL_POPUP_ENABLED_KEY } from '../../contentScrip
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from "wxt/storage";
|
||||
import { browser } from 'wxt/browser';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
/**
|
||||
* Popup settings type.
|
||||
@@ -18,6 +19,7 @@ type PopupSettings = {
|
||||
* Settings page component.
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
currentUrl: '',
|
||||
@@ -49,7 +51,7 @@ const Settings: React.FC = () => {
|
||||
disabledUrls,
|
||||
currentUrl,
|
||||
isEnabled: !disabledUrls.includes(currentUrl),
|
||||
isGloballyEnabled
|
||||
isGloballyEnabled,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -106,6 +108,20 @@ const Settings: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set theme preference.
|
||||
*/
|
||||
const setThemePreference = async (newTheme: 'system' | 'light' | 'dark') : Promise<void> => {
|
||||
// Use the ThemeContext to apply the theme
|
||||
setTheme(newTheme);
|
||||
|
||||
// Update local state
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
theme: newTheme
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
@@ -128,11 +144,11 @@ const Settings: React.FC = () => {
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isGloballyEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,18 +164,18 @@ const Settings: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Open popup on: {settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? 'Popup is active' : 'Popup is disabled'}
|
||||
{settings.isEnabled ? 'Enabled for this site' : 'Disabled for this site'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -175,6 +191,53 @@ const Settings: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Appearance</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Theme</p>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="system"
|
||||
checked={theme === 'system'}
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Use default</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="light"
|
||||
checked={theme === 'light'}
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Light</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="dark"
|
||||
checked={theme === 'dark'}
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Dark</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import SrpUtility from '../utils/SrpUtility';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Unlock page
|
||||
@@ -15,6 +18,7 @@ import { useLoading } from '../context/LoadingContext';
|
||||
const Unlock: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
@@ -73,6 +77,9 @@ const Unlock: React.FC = () => {
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
} catch (err) {
|
||||
setError('Failed to unlock vault. Please check your password and try again.');
|
||||
console.error('Unlock error:', err);
|
||||
@@ -81,6 +88,13 @@ const Unlock: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout
|
||||
*/
|
||||
const handleLogout = () : void => {
|
||||
navigate('/logout', { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@@ -116,7 +130,7 @@ const Unlock: React.FC = () => {
|
||||
</Button>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
Switch accounts? <a href="/logout" className="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
|
||||
Switch accounts? <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">Log out</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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.13.0';
|
||||
public static readonly VERSION = '0.15.0';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
20
browser-extension/src/utils/ExpandedMode.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Setup the expanded mode.
|
||||
*/
|
||||
export function setupExpandedMode() : void {
|
||||
/**
|
||||
* This runs once when imported and checks if the popup was opened in expanded mode with unlimited width.
|
||||
* If not, it sets the width to 350px to force the default popup to a fixed width.
|
||||
* This is used to ensure the popup is always a fixed width, even if some content like email preview
|
||||
* is too wide to fit in the default width. Some browsers like Firefox and Safari will then try to
|
||||
* expand the popup to the width of the content, which can cause the popup to become too wide and bad UX.
|
||||
*
|
||||
* You can test this by opening the popup and then clicking on the email preview. If the popup width does
|
||||
* not change, it works. Then if you expand/popout the extension, the content of the page should adjust
|
||||
* to the new width of the resizable popup.
|
||||
*/
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.get('expanded')) {
|
||||
document.documentElement.classList.add('max-w-[350px]');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
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.
|
||||
@@ -279,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
|
||||
@@ -421,6 +450,66 @@ class SqliteClient {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TOTP codes for a credential
|
||||
* @param credentialId - The ID of the credential to get TOTP codes for
|
||||
* @returns Array of TotpCode objects
|
||||
*/
|
||||
public getTotpCodesForCredential(credentialId: string): TotpCode[] {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
/*
|
||||
* Check if TotpCodes table exists (for backward compatibility).
|
||||
* TODO: whenever the browser extension has a minimum client DB version of 1.5.0+,
|
||||
* we can remove this check as the TotpCodes table then is guaranteed to exist.
|
||||
*/
|
||||
if (!this.tableExists('TotpCodes')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
Id,
|
||||
Name,
|
||||
SecretKey,
|
||||
CredentialId
|
||||
FROM TotpCodes
|
||||
WHERE CredentialId = ? AND IsDeleted = 0`;
|
||||
|
||||
return this.executeQuery<TotpCode>(query, [credentialId]);
|
||||
} catch (error) {
|
||||
console.error('Error getting TOTP codes:', error);
|
||||
// Return empty array instead of throwing to be robust
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a table exists in the database
|
||||
* @param tableName - The name of the table to check
|
||||
* @returns True if the table exists, false otherwise
|
||||
*/
|
||||
private tableExists(tableName: string): boolean {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name=?`;
|
||||
|
||||
const results = this.executeQuery(query, [tableName]);
|
||||
return results.length > 0;
|
||||
} catch (error) {
|
||||
console.error(`Error checking if table ${tableName} exists:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SqliteClient;
|
||||
@@ -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.
|
||||
@@ -35,12 +40,12 @@ export class FormFiller {
|
||||
}
|
||||
|
||||
if (this.form.passwordField) {
|
||||
this.form.passwordField.value = credential.Password;
|
||||
this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
this.triggerInputEvents(this.form.passwordField);
|
||||
}
|
||||
|
||||
if (this.form.passwordConfirmField) {
|
||||
this.form.passwordConfirmField.value = credential.Password;
|
||||
this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
this.triggerInputEvents(this.form.passwordConfirmField);
|
||||
}
|
||||
|
||||
@@ -70,6 +75,29 @@ export class FormFiller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the password field with the given password. This uses a small delay between each character to simulate human typing.
|
||||
* 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.
|
||||
*/
|
||||
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
|
||||
// Clear the field first
|
||||
field.value = '';
|
||||
this.triggerInputEvents(field, false);
|
||||
|
||||
// Type each character with a small delay
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the birthdate fields of the form.
|
||||
* @param credential The credential to fill the form with.
|
||||
|
||||
@@ -44,11 +44,14 @@ describe('FormFiller', () => {
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.emailConfirmField)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fill password and confirmation fields', () => {
|
||||
it('should fill password and confirmation fields', async () => {
|
||||
formFields.passwordConfirmField = document.createElement('input');
|
||||
|
||||
formFiller.fillFields(mockCredential);
|
||||
|
||||
// Delay for 150ms to ensure the password field is filled as it uses a small delay between each character.
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
expect(formFields.passwordField?.value).toBe('testpass');
|
||||
expect(formFields.passwordConfirmField?.value).toBe('testpass');
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.passwordField)).toBe(true);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
16
browser-extension/src/utils/types/TotpCode.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* TotpCode SQLite database type.
|
||||
*/
|
||||
export type TotpCode = {
|
||||
/** The ID of the TOTP code */
|
||||
Id: string;
|
||||
|
||||
/** The name of the TOTP code */
|
||||
Name: string;
|
||||
|
||||
/** The secret key for the TOTP code */
|
||||
SecretKey: string;
|
||||
|
||||
/** The credential ID this TOTP code belongs to */
|
||||
CredentialId: string;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PasswordSettings } from "@/utils/types/PasswordSettings";
|
||||
|
||||
export type PasswordSettingsResponse = {
|
||||
success: boolean,
|
||||
error?: string,
|
||||
settings?: PasswordSettings
|
||||
};
|
||||
@@ -1,10 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/entrypoints/*.{js,jsx,ts,tsx}",
|
||||
"./src/entrypoints/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/entrypoints/**/*/*.{html,js}"
|
||||
"./src/**/*.{js,jsx,ts,tsx,html}"
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.13.0",
|
||||
version: "0.15.0",
|
||||
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)
|
||||
|
||||
@@ -11,7 +11,7 @@ In order to install the Firefox Extension, see the options below.
|
||||
## Firefox Add-ons
|
||||
Installing the extension from the Firefox Add-ons is the easiest way to get started. This ensures that you are always using the latest version of the extension.
|
||||
|
||||
1. Go to the (TODO: add Firefox Add-ons link)
|
||||
1. Go to the [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/aliasvault/) page
|
||||
2. Click on the "Add to Firefox" button
|
||||
3. The extension will be installed and added to your browser
|
||||
|
||||
|
||||
53
docs/browser-extensions/safari/build-from-source.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
layout: default
|
||||
title: Build from Source
|
||||
parent: Safari
|
||||
grand_parent: Browser Extensions
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Building Safari Extension from Source
|
||||
|
||||
This guide explains how to build and install the AliasVault Safari extension from source code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js installed on your computer
|
||||
- Git to clone the repository (optional)
|
||||
- MacOS machine with Xcode installed
|
||||
|
||||
## Building the Safari Extension
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/lanedirt/AliasVault.git
|
||||
```
|
||||
|
||||
2. Navigate to the Browser Extension directory:
|
||||
```bash
|
||||
cd AliasVault/browser-extension
|
||||
```
|
||||
|
||||
3. Install the required dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Build the extension:
|
||||
```bash
|
||||
npm run build:safari
|
||||
```
|
||||
|
||||
5. Open Xcode and open the `browser-extension/safari-xcode/AliasVault/AliasVault.xcodeproj` file
|
||||
|
||||
6. Run the project. This will open up the AliasVault MacOS wrapper app and automatically install the extension to your Safari Extensions list.
|
||||
|
||||
## Installing and enabling the extension in Safari
|
||||
|
||||
1. Open Safari and go to menu > Safari > Settings
|
||||
2. Click on the "Extensions" tab
|
||||
3. Enable the AliasVault extension. If the extension is not visible, then you may need to enable developer mode in Safari settings first to allow unsigned extensions to run.
|
||||
|
||||
## Development Mode (Optional)
|
||||
|
||||
If you plan to modify the extension code, see the [browser-extensions](../../misc/dev/browser-extensions.md) developer documentation for more information.
|
||||
19
docs/browser-extensions/safari/index.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
layout: default
|
||||
title: Safari
|
||||
parent: Browser Extensions
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# Safari Extension
|
||||
In order to install the Safari Extension, see the options below.
|
||||
|
||||
## MacOS App Store
|
||||
Installing the extension from the MacOS App Store is the easiest way to get started. This ensures that you are always using the latest version of the extension.
|
||||
|
||||
1. Go to the [MacOS App Store](https://apps.apple.com/app/id6743163173) page
|
||||
2. Click on the "Get" button
|
||||
3. The extension will be installed and added to Safari. Follow the instructions in the AliasVault MacOS app that is shown on screen after installation.
|
||||
|
||||
## Build from Source
|
||||
If you wish to install the extension from source instead, see the [build-from-source](build-from-source.md) documentation. This will allow you to make changes to the extension and/or to use a specific version of the extension.
|
||||
@@ -72,6 +72,9 @@ and then in the prompt choose option 2.
|
||||
|
||||
AliasVault includes a built-in email server that can handle multiple custom domains for your aliases.
|
||||
|
||||
{: .note }
|
||||
Please be aware that if you skip this step, AliasVault will default to use public email domains offered by SpamOK. While this will still work for creating email aliases, it has privacy limitations. For complete privacy and control, we recommend following the setup steps below to use your own private domain. [Learn more about the differences between private and public email domains](../misc/private-vs-public-email.md).
|
||||
|
||||
To set up the email server, you need the following:
|
||||
- Public IPv4 address
|
||||
- Open ports (25 and 587) in server firewall for SMTP traffic
|
||||
|
||||
@@ -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
|
||||
@@ -76,6 +76,17 @@ npm run build:edge
|
||||
- Enable "Developer mode" in the top right corner
|
||||
- Click "Load unpacked" and the folder `./browser-extension/dist/edge-mv3`
|
||||
|
||||
### Safari
|
||||
|
||||
1. Build the extension:
|
||||
```bash
|
||||
npm run build:safari
|
||||
```
|
||||
|
||||
2. Open the Xcode project in the `safari-xcode/AliasVault/AliasVault.xcodeproj` folder and build / run the app.
|
||||
|
||||
3. The extension will be installed automatically in Safari. Follow the on-screen MacOS app instructions to complete the installation.
|
||||
|
||||
## Automatic tests
|
||||
The extension has a suite of automatic tests that are run on every pull request. These tests are located in the `__tests__` directories scattered throughout the browser extension codebase.
|
||||
|
||||
@@ -98,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: 5
|
||||
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
@@ -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
@@ -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
|
||||
56
docs/misc/private-vs-public-email.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
layout: default
|
||||
title: Private vs Public Email Domains
|
||||
parent: Miscellaneous
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# Private vs Public Email Domains
|
||||
AliasVault offers two types of email domains: private and public.
|
||||
|
||||
## Private Email Domains
|
||||
Private email domains come in two forms:
|
||||
|
||||
1. For the official cloud-hosted AliasVault service, users get access to the aliasvault.net domain, which is a private domain managed by AliasVault.
|
||||
|
||||
2. For self-hosted installations, private domains are domains that you control and configure yourself to connect to your AliasVault server instance.
|
||||
|
||||
In both cases, private domains are directly connected to the AliasVault server infrastructure. Any email aliases created using these domains benefit from full end-to-end encryption - emails are encrypted with the receiver's public/private key pair before they are stored on the AliasVault server. These emails can only be decrypted by the receiver's private key that is stored securely in the user's vault. This ensures that no one can read your emails except for you.
|
||||
|
||||
---
|
||||
|
||||
## Public Email Domains
|
||||
For convenience, AliasVault also offers public email domains which are provided through an integration with [SpamOK.com](https://spamok.com), a free service operated by Lanedirt (the author of AliasVault). These domains are suitable for testing and non-critical email aliases and offer convenience for self-hosted users who cannot set up their own private domains.
|
||||
|
||||
## Available Domains
|
||||
The following public email domains are currently available through SpamOK:
|
||||
- spamok.com
|
||||
- solarflarecorp.com
|
||||
- spamok.nl
|
||||
- 3060.nl
|
||||
- landmail.nl
|
||||
- asdasd.nl
|
||||
- spamok.de
|
||||
- spamok.com.ua
|
||||
- spamok.es
|
||||
- spamok.fr
|
||||
|
||||
## Important Disclaimers
|
||||
Public email domains do have limitations, please be aware of them:
|
||||
|
||||
1. **Public Nature**: These are fully public domains - anyone can access any email address as long as they know the name of the alias. The benefit is that this makes these domains fully anonymous because usage cannot be tied back to a specific user. But this also means that there is no privacy guarantee, as your emails can be read by anyone who knows the email address.
|
||||
|
||||
2. **No Service Level Agreement**: SpamOK is provided as a free service without any SLA or warranty. Email delivery and service availability are not guaranteed and can be interrupted at any time without notice.
|
||||
|
||||
### When to Use SpamOK Domains
|
||||
SpamOK domains are suitable for:
|
||||
- Testing AliasVault functionality
|
||||
- Non-critical email aliases
|
||||
- Temporary or disposable email needs
|
||||
|
||||
### When to Set Up Your Own Email Server
|
||||
Consider setting up your own email server if you need:
|
||||
- Complete control over your email domains
|
||||
- Private email addresses where all incoming emails are encrypted before being stored on the AliasVault server. No one can read your emails except for you.
|
||||
- Guaranteed service availability
|
||||
- Professional or business use
|
||||
@@ -14,17 +14,17 @@ 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.
|
||||
- [ ] Update the version `MARKETING_VERSION` and increase the build number `CURRENT_PROJECT_VERSION` in the ./browser-extension/safari-xcode/AliasVault/AliasVault.xcodeproj project file in MacOS Xcode. This is the version that will be shown in the Safari Browser Extension App Store.
|
||||
- [ ] Update ./browser-extension/src/utils/AppInfo.ts with the new version for the extension. This version should be equal to the git release tag.
|
||||
- [ ] Update ./browser-extension/src/utils/AppInfo.ts with the minimum supported server version (in case of required API breaking changes).
|
||||
- [ ] Update ./browser-extension/src/shared/AppInfo.ts with the minimum supported client vault version (in case of required client vault model changes).
|
||||
|
||||
## 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+)
|
||||
@@ -56,3 +56,4 @@ The GitHub Actions workflow `Browser Extension Build` will build the browser ext
|
||||
2. Upload the Chrome archive to the Chrome Web Store.
|
||||
3. Upload the Firefox archive (normal + sources) to the Firefox Add-ons page.
|
||||
4. Upload the Edge archive to the Microsoft Edge Add-ons page.
|
||||
5. Submit the Safari extension to Apple for review by opening the `browser-extension/safari-xcode/AliasVault/AliasVault.xcodeproj` project in Xcode and submitting the extension via the "Distribute App" option.
|
||||
31
install.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# @version 0.12.2
|
||||
# @version 0.15.0
|
||||
|
||||
# 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"
|
||||
@@ -651,7 +652,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"
|
||||
@@ -1544,7 +1545,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"
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||