Compare commits

..

99 Commits

Author SHA1 Message Date
Leendert de Borst
09d931484a Update GitHub workflows (#722) 2025-03-25 13:29:26 +01:00
Leendert de Borst
1678595c13 Bump version to 0.15.0 (#722) 2025-03-25 13:13:05 +01:00
Leendert de Borst
8945b33705 Add install.sh to release artifacts (#722) 2025-03-25 13:13:05 +01:00
Leendert de Borst
4ee044ffb9 Update faviconextractor HtmlAgilityPack call (#715) 2025-03-25 11:53:04 +01:00
dependabot[bot]
5443e147b1 Bump HtmlAgilityPack from 1.11.74 to 1.12.0
Bumps [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack) from 1.11.74 to 1.12.0.
- [Release notes](https://github.com/zzzprojects/html-agility-pack/releases)
- [Commits](https://github.com/zzzprojects/html-agility-pack/compare/v1.11.74...v1.12.0)

---
updated-dependencies:
- dependency-name: HtmlAgilityPack
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-25 11:53:04 +01:00
Leendert de Borst
05edda8b48 Show returning users count in admin dashboard (#720) 2025-03-25 10:48:55 +01:00
Leendert de Borst
179bb62604 Fix bug in search for null credential fields (#718) 2025-03-24 22:21:34 +01:00
Leendert de Borst
1f5863b066 Fix vault dismiss logic when user is not logged in (#718) 2025-03-24 22:21:34 +01:00
Leendert de Borst
ef36a08ef4 Update password autofill to improve compatibility (#718) 2025-03-24 22:21:34 +01:00
dependabot[bot]
4f7212668e Bump Swashbuckle.AspNetCore from 7.3.2 to 8.0.0
Bumps [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 7.3.2 to 8.0.0.
- [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases)
- [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.3.2...v8.0.0)

---
updated-dependencies:
- dependency-name: Swashbuckle.AspNetCore
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 17:52:47 +01:00
dependabot[bot]
41bb7ed701 Bump Microsoft.AspNetCore.Components.WebAssembly.DevServer
Bumps [Microsoft.AspNetCore.Components.WebAssembly.DevServer](https://github.com/dotnet/aspnetcore) from 9.0.2 to 9.0.3.
- [Release notes](https://github.com/dotnet/aspnetcore/releases)
- [Changelog](https://github.com/dotnet/aspnetcore/blob/main/docs/ReleasePlanning.md)
- [Commits](https://github.com/dotnet/aspnetcore/compare/v9.0.2...v9.0.3)

---
updated-dependencies:
- dependency-name: Microsoft.AspNetCore.Components.WebAssembly.DevServer
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 17:51:49 +01:00
dependabot[bot]
78286b1ac1 Bump nokogiri in /docs in the bundler group across 1 directory
Bumps the bundler group with 1 update in the /docs directory: [nokogiri](https://github.com/sparklemotion/nokogiri).


Updates `nokogiri` from 1.18.3 to 1.18.4
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.3...v1.18.4)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-22 10:34:01 +01:00
Leendert de Borst
7bc8bb3fc2 Create FUNDING.yml 2025-03-21 16:36:47 +01:00
Leendert de Borst
c576062025 Fix hyperlinks absolute vs relative address (#711) 2025-03-20 13:55:32 +01:00
Leendert de Borst
1194d54e6f Add E2E test for email claim disable logic (#711) 2025-03-20 13:55:32 +01:00
Leendert de Borst
e782a6a51f Reject emails addressed to disabled email claim (#711) 2025-03-20 13:55:32 +01:00
Leendert de Borst
2071a7c4fe Add email claim enable/disable toggle to admin (#711) 2025-03-20 13:55:32 +01:00
Leendert de Borst
8c1e5a7bf8 Add email claim table disabled boolean (#711) 2025-03-20 13:55:32 +01:00
Leendert de Borst
b8f9e7fa2c Merge pull request #710 from lanedirt/641-add-statistics-graphs-to-admin
Add analytics charts to admin dashboard
2025-03-20 10:04:24 +01:00
Leendert de Borst
a0a541aff9 Update admin tests (#641) 2025-03-19 22:17:59 +01:00
Leendert de Borst
d6932f33ea Update email list page and tweak search fields (#641) 2025-03-19 22:10:13 +01:00
Leendert de Borst
9ea845b497 Add ApexChart service and integrate dark mode (#641) 2025-03-19 19:33:42 +01:00
Leendert de Borst
917d6f6bcc Add charts to admin dashboard (#641) 2025-03-19 17:49:09 +01:00
Leendert de Borst
39a263d157 Update docs (#641) 2025-03-19 15:34:35 +01:00
Leendert de Borst
c7360ee23c Add general log source context to term filter (#705) 2025-03-19 14:23:51 +01:00
Leendert de Borst
d1924f4044 Update header text (#705) 2025-03-19 14:23:51 +01:00
Leendert de Borst
4d86356990 Update users page with credential count column (#705) 2025-03-19 14:23:51 +01:00
Leendert de Borst
505a2445eb Reset page back to 1 when search term changes in admin (#705) 2025-03-19 14:23:51 +01:00
Leendert de Borst
75385c4b5d Remove WASM DevServer package from admin which caused it to not run in debug (#705) 2025-03-19 14:23:51 +01:00
Leendert de Borst
4d4053c7fb Update package-lock.json (#705) 2025-03-19 14:23:51 +01:00
Leendert de Borst
43062d0d93 Update .vscode tasks (#705) 2025-03-19 14:23:51 +01:00
Leendert de Borst
956709da54 Merge pull request #706 from lanedirt/167-allow-customizing-password-generation-options
Make password generation options customizable
2025-03-18 22:15:53 +01:00
Leendert de Borst
496e0ab754 Refactor PasswordGenerator.ts (#167) 2025-03-18 22:04:11 +01:00
Leendert de Borst
ef97aac848 Merge branch 'main' into 167-allow-customizing-password-generation-options 2025-03-18 18:22:09 +01:00
Leendert de Borst
998fa1913f Update dotnet nuget packages to 9.0.3 (#707) 2025-03-18 18:08:32 +01:00
Leendert de Borst
79cd265c3e Add browser extension password settings test (#167) 2025-03-18 17:40:31 +01:00
Leendert de Borst
ed5fd5b861 Disable autofill extension for aliasvault client by default (#167) 2025-03-18 17:12:34 +01:00
Leendert de Borst
5e2dde252d Update tests (#167) 2025-03-18 16:51:49 +01:00
Leendert de Borst
79950ab9fc Add password generator settings awareness to browser extension (#167) 2025-03-18 16:30:41 +01:00
Leendert de Borst
dffa651512 Cleanup (#167) 2025-03-18 14:37:24 +01:00
Leendert de Borst
2dc36cea11 Add password settings to general settings page (#167) 2025-03-18 14:17:49 +01:00
Leendert de Borst
ad4c2c7b41 Add modalwrapper component for keydown detection (#167) 2025-03-18 13:41:43 +01:00
Leendert de Borst
2022cdb58b Improve UX (#167) 2025-03-18 13:08:56 +01:00
Leendert de Borst
5f779ce360 Update UI style (#167) 2025-03-18 12:37:10 +01:00
Leendert de Borst
b9d981f80b Refactor (#167) 2025-03-18 11:30:36 +01:00
Leendert de Borst
65110abf4c Add range binds and sanity checks (#167) 2025-03-18 10:47:06 +01:00
Leendert de Borst
b0e939ef23 Add support for temp or global password settings persist (#167) 2025-03-18 10:19:53 +01:00
Leendert de Borst
607c0da5b4 Make password settings a separate component (#167) 2025-03-18 10:05:10 +01:00
Leendert de Borst
1de7f831b5 Fix recent email refresh duplicate calls (#167) 2025-03-17 22:19:31 +01:00
Leendert de Borst
ef328718cd Refactor password generator and make all use general settings (#167) 2025-03-17 21:28:57 +01:00
Leendert de Borst
465c4cc730 Update username and password button style (#167) 2025-03-17 20:37:26 +01:00
Leendert de Borst
0dceeeffa4 Update docs to include Windows instructions (#703) 2025-03-17 17:56:21 +01:00
Leendert de Borst
af24464a8d Convert install.sh line endings so it works on Windows out of the box (#703) 2025-03-17 17:56:21 +01:00
Leendert de Borst
5aa82d8149 Update username and password edit field GUI (#167) 2025-03-17 15:06:15 +01:00
Leendert de Borst
e848e05cce Cleanup and simplify install.sh (#690) 2025-03-16 15:35:58 +01:00
Leendert de Borst
323be10d03 Tweak password edit component UI (#167) 2025-03-15 18:24:35 +01:00
Leendert de Borst
51b382a739 Add password generation settings GUI scaffolding (#167) 2025-03-15 18:03:45 +01:00
Leendert de Borst
7954104dfc Update README.md 2025-03-14 17:54:51 +01:00
Leendert de Borst
4c7b44c04a Bump version to 0.14.0 (#688) 2025-03-14 14:17:26 +01:00
Leendert de Borst
b41449f892 Remove Microsoft.IdentityModel packages from API which caused method not found bug (#668) 2025-03-14 13:13:36 +01:00
dependabot[bot]
934d0d9e56 Bump Microsoft.IdentityModel.Tokens from 8.6.0 to 8.6.1
Bumps [Microsoft.IdentityModel.Tokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 8.6.0 to 8.6.1.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/8.6.0...8.6.1)

---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.Tokens
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-14 13:13:36 +01:00
Leendert de Borst
99d0da1119 Update docs and README.md (#680) 2025-03-13 15:10:01 +01:00
Leendert de Borst
c74e05d400 Improve create credential popup page title extraction (#686) 2025-03-13 15:09:21 +01:00
dependabot[bot]
844bdab92f Bump MailKit from 4.10.0 to 4.11.0
Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.10.0 to 4.11.0.
- [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MailKit/compare/4.10.0...4.11.0)

---
updated-dependencies:
- dependency-name: MailKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 14:13:02 +01:00
dependabot[bot]
1345e3c657 Bump MimeKit from 4.10.0 to 4.11.0
Bumps [MimeKit](https://github.com/jstedfast/MimeKit) from 4.10.0 to 4.11.0.
- [Changelog](https://github.com/jstedfast/MimeKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MimeKit/compare/4.10.0...4.11.0)

---
updated-dependencies:
- dependency-name: MimeKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-13 14:12:55 +01:00
Leendert de Borst
4fdf7ce92c Show autofill popup dismiss button when vault is locked (#682) 2025-03-13 14:12:43 +01:00
Leendert de Borst
852d9b5e98 Update tests to wait until all password chars have been entered (#684) 2025-03-13 13:47:39 +01:00
Leendert de Borst
3c72fa3fde Update password autofill mechanism to simulate user typing behavior (#684) 2025-03-13 13:47:39 +01:00
Leendert de Borst
b61b747e4b Add default font-family (#680) 2025-03-13 13:29:57 +01:00
Leendert de Borst
1b4389c7d7 Show manual instructions if opening preferences fails (#680) 2025-03-13 13:29:57 +01:00
Leendert de Borst
499d2759ce Add Safari extension docs (#680) 2025-03-13 13:29:57 +01:00
Leendert de Borst
d0140a8ddb Fix MacOS wrapper app links and content (#680) 2025-03-13 13:29:57 +01:00
Leendert de Borst
76dc465032 Refactor (#678) 2025-03-12 22:02:11 +01:00
Leendert de Borst
84420104ee Iframe and position tweaks (#678) 2025-03-12 22:02:11 +01:00
Leendert de Borst
1109bde521 Refactor all inline styles to separate style.css (#678) 2025-03-12 22:02:11 +01:00
Leendert de Borst
134a173148 Import stylesheet for contentScript (#678) 2025-03-12 22:02:11 +01:00
Leendert de Borst
83be492b3a Refactor injectIcon (#678) 2025-03-12 22:02:11 +01:00
Leendert de Borst
fac72e5a11 Refactor content script to use shadowroot UI (#678) 2025-03-12 22:02:11 +01:00
Leendert de Borst
5eb885da20 Refactor (#661) 2025-03-12 16:07:16 +01:00
Leendert de Borst
da4f286757 Add download links for Firefox, Edge, Safari and Brave (#661) 2025-03-12 16:07:16 +01:00
Leendert de Borst
f6db447ad4 Add Safari extension XCode project scaffolding (#661) 2025-03-12 16:07:16 +01:00
Leendert de Borst
b472ba749c Fix padding issue with search field in Safari (#661) 2025-03-12 16:07:16 +01:00
Leendert de Borst
ef68b3b265 Fix scroll issue for Safari browser (#661) 2025-03-12 16:07:16 +01:00
Leendert de Borst
08d4a8b656 Add light/dark mode toggle to browser extension settings (#661) 2025-03-12 16:07:16 +01:00
Leendert de Borst
93ac131508 Refactor expanded mode check to be called from React (#661) 2025-03-12 16:07:16 +01:00
Leendert de Borst
a7d1536140 Refactor and tweak UI (#672) 2025-03-11 16:59:12 +01:00
Leendert de Borst
4fa3fedea2 Add TotpViewer component (#672) 2025-03-11 16:59:12 +01:00
Leendert de Borst
038e8babb1 Update TotpViewer.razor (#672) 2025-03-11 16:59:12 +01:00
Leendert de Borst
0845477041 Add private vs public email domain documentation (#673) 2025-03-11 11:17:23 +01:00
Leendert de Borst
90156dd1f8 Refactor (#181) 2025-03-11 10:29:25 +01:00
Leendert de Borst
fe4b11cf4d Add TOTP E2E tests (#181) 2025-03-11 10:29:25 +01:00
Leendert de Borst
2cbf234d05 Refactor (#181) 2025-03-11 10:29:25 +01:00
Leendert de Borst
a53575b4bf Add click to copy and form validation (#181) 2025-03-11 10:29:25 +01:00
Leendert de Borst
697abc6828 Refactor TOTP code to work view AddEdit/View mode (#181) 2025-03-11 10:29:25 +01:00
Leendert de Borst
e96cfa3940 Update UX (#181) 2025-03-11 10:29:25 +01:00
Leendert de Borst
61a88e6715 Add credentials TOTP code scaffolding (#181) 2025-03-11 10:29:25 +01:00
Leendert de Borst
e07a35b214 Add firefox addon link to docs (#665) 2025-03-11 09:59:07 +01:00
Leendert de Borst
4a79fafbb9 Update README.md 2025-03-09 21:32:02 +01:00
Leendert de Borst
02b9bff64e Update browser-extension-build.yml (#665) 2025-03-09 20:46:50 +01:00
187 changed files with 9497 additions and 1875 deletions

31
.gitattributes vendored
View File

@@ -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
View File

@@ -0,0 +1,2 @@
# These are supported funding model platforms
buy_me_a_coffee: lanedirt

View File

@@ -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 }}

View File

@@ -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>'"

View File

@@ -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"

View File

@@ -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
View File

@@ -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
View 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"
}
}
]
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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}

View File

@@ -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.
```

View File

@@ -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": {

View File

@@ -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",

View 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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -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"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,20 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -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 AliasVaults browser extension, go to the Safari Extensions preferences.</p>
<p class="state-on">AliasVaults 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">AliasVaults 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>

View File

@@ -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>

View File

@@ -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>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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)
}
}
}
}
}

View 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

View File

@@ -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));

View File

@@ -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.
*/

View File

@@ -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();
},
});

View File

@@ -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 }));

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
/**
* Check if the current theme is dark.
*/
export function isDarkMode(): boolean {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

View 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); }
}

View File

@@ -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">

View File

@@ -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' : ''
}`}
>

View File

@@ -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>
)}

View File

@@ -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>
);
};

View File

@@ -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);
}, []);
/**

View 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;
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View 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]');
}
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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);

View File

@@ -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);
}
}

View 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;
}

View 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;
};

View File

@@ -0,0 +1,7 @@
import { PasswordSettings } from "@/utils/types/PasswordSettings";
export type PasswordSettingsResponse = {
success: boolean,
error?: string,
settings?: PasswordSettings
};

View File

@@ -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: {

View File

@@ -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';"
},

View File

@@ -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)

View File

@@ -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

View 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.

View 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.

View File

@@ -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

View File

@@ -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
```
---

View File

@@ -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 |

View File

@@ -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.
```

View File

@@ -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.

View File

@@ -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)

View 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

View File

@@ -1,6 +1,6 @@
---
layout: default
title: Run GitHub Actions Locally
title: Run GitHub actions locally
parent: Development
grand_parent: Miscellaneous
nav_order: 9

View 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

View 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

View File

@@ -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.

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

Some files were not shown because too many files have changed in this diff Show More