Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d59117112 | ||
|
|
ccb66af1ca | ||
|
|
f4093a9199 | ||
|
|
290601ccfb | ||
|
|
77be2a339e | ||
|
|
c0b23c15e7 | ||
|
|
4af158b35d | ||
|
|
abfabc2a4a | ||
|
|
a0036da781 | ||
|
|
99f084558d | ||
|
|
d7be5fc308 | ||
|
|
485e867c50 | ||
|
|
d2e5f3c715 | ||
|
|
0cbe5fec93 | ||
|
|
7f7c729e82 | ||
|
|
35cc29e751 | ||
|
|
8a16a29727 | ||
|
|
708cffc49e | ||
|
|
74c0ace2b5 | ||
|
|
55175a7db6 | ||
|
|
7e1f33e4e1 | ||
|
|
81362b165b | ||
|
|
41d6511eb2 | ||
|
|
60ba96cb86 | ||
|
|
fdd8c8b37e | ||
|
|
53fcb2f2e4 | ||
|
|
b1848320d9 | ||
|
|
610be7e30b | ||
|
|
933e458776 | ||
|
|
b460e6ec20 | ||
|
|
80cd371ee3 | ||
|
|
915e12d541 | ||
|
|
c8d78e0b02 | ||
|
|
199941a837 | ||
|
|
1e0c586dba | ||
|
|
37e59dcd4e | ||
|
|
e665130ea7 | ||
|
|
c0aac4ef72 | ||
|
|
8319ddcce4 | ||
|
|
adc6293f4b | ||
|
|
418bfed663 | ||
|
|
7074113cbf | ||
|
|
ddb610051a | ||
|
|
188b7a4062 | ||
|
|
989d17708f | ||
|
|
77a4b4fcba | ||
|
|
0462e3522b | ||
|
|
f6bddf730f | ||
|
|
035403e3e3 | ||
|
|
33ebbf0fd5 | ||
|
|
55c75ec094 | ||
|
|
6e244e611c | ||
|
|
e1dc9eb447 | ||
|
|
7a8b31a98a | ||
|
|
9baa70f022 | ||
|
|
24106475f9 | ||
|
|
c50178967a | ||
|
|
a69a6a91e2 | ||
|
|
1dca845731 | ||
|
|
9bec5a3ae5 | ||
|
|
1a8dae44ec | ||
|
|
ec15c76001 | ||
|
|
e0c11ba0f6 | ||
|
|
a72f1139f9 | ||
|
|
a3a3d39664 | ||
|
|
014a705a5e | ||
|
|
6dfb922292 | ||
|
|
cb78d8a636 | ||
|
|
a4c4a9c8ec | ||
|
|
6f5ae7c17e | ||
|
|
43f5e0c647 | ||
|
|
0e5f611670 | ||
|
|
70b7ac6f9f | ||
|
|
14ee466bec | ||
|
|
ea9c3c5683 | ||
|
|
30b812e8a3 | ||
|
|
27ba14ee34 | ||
|
|
2e851701f9 | ||
|
|
a2c2caed79 | ||
|
|
c00e6c6a4d | ||
|
|
09dda0147b | ||
|
|
ef7398b47a | ||
|
|
dc769bb5d4 | ||
|
|
634fc281a2 | ||
|
|
e93b0575ff | ||
|
|
3f6575dfe5 | ||
|
|
390877f8f3 | ||
|
|
55ee3bfd4a | ||
|
|
423fe00692 | ||
|
|
f8e0d6a293 | ||
|
|
a20b0ed83a | ||
|
|
ca043954ec | ||
|
|
4f0104e8f9 | ||
|
|
ea37c4d8c6 | ||
|
|
95be4beb13 | ||
|
|
716ef0b30c | ||
|
|
fc0eb0e7e7 | ||
|
|
9670178aec | ||
|
|
8503be4d52 | ||
|
|
9eadcaa2ed | ||
|
|
e0ed8fd285 | ||
|
|
61748c3d03 | ||
|
|
faff4844f5 | ||
|
|
09d931484a | ||
|
|
1678595c13 | ||
|
|
8945b33705 | ||
|
|
4ee044ffb9 | ||
|
|
5443e147b1 | ||
|
|
05edda8b48 | ||
|
|
179bb62604 | ||
|
|
1f5863b066 | ||
|
|
ef36a08ef4 | ||
|
|
4f7212668e | ||
|
|
41bb7ed701 | ||
|
|
78286b1ac1 | ||
|
|
7bc8bb3fc2 | ||
|
|
c576062025 | ||
|
|
1194d54e6f | ||
|
|
e782a6a51f | ||
|
|
2071a7c4fe | ||
|
|
8c1e5a7bf8 | ||
|
|
b8f9e7fa2c | ||
|
|
a0a541aff9 | ||
|
|
d6932f33ea | ||
|
|
9ea845b497 | ||
|
|
917d6f6bcc | ||
|
|
39a263d157 | ||
|
|
c7360ee23c | ||
|
|
d1924f4044 | ||
|
|
4d86356990 | ||
|
|
505a2445eb | ||
|
|
75385c4b5d | ||
|
|
4d4053c7fb | ||
|
|
43062d0d93 | ||
|
|
956709da54 | ||
|
|
496e0ab754 | ||
|
|
ef97aac848 | ||
|
|
998fa1913f | ||
|
|
79cd265c3e | ||
|
|
ed5fd5b861 | ||
|
|
5e2dde252d | ||
|
|
79950ab9fc | ||
|
|
dffa651512 | ||
|
|
2dc36cea11 | ||
|
|
ad4c2c7b41 | ||
|
|
2022cdb58b | ||
|
|
5f779ce360 | ||
|
|
b9d981f80b | ||
|
|
65110abf4c | ||
|
|
b0e939ef23 | ||
|
|
607c0da5b4 | ||
|
|
1de7f831b5 | ||
|
|
ef328718cd | ||
|
|
465c4cc730 | ||
|
|
0dceeeffa4 | ||
|
|
af24464a8d | ||
|
|
5aa82d8149 | ||
|
|
e848e05cce | ||
|
|
323be10d03 | ||
|
|
51b382a739 | ||
|
|
7954104dfc | ||
|
|
4c7b44c04a | ||
|
|
b41449f892 | ||
|
|
934d0d9e56 | ||
|
|
99d0da1119 | ||
|
|
c74e05d400 | ||
|
|
844bdab92f | ||
|
|
1345e3c657 | ||
|
|
4fdf7ce92c | ||
|
|
852d9b5e98 | ||
|
|
3c72fa3fde | ||
|
|
b61b747e4b | ||
|
|
1b4389c7d7 | ||
|
|
499d2759ce | ||
|
|
d0140a8ddb | ||
|
|
76dc465032 | ||
|
|
84420104ee | ||
|
|
1109bde521 | ||
|
|
134a173148 | ||
|
|
83be492b3a | ||
|
|
fac72e5a11 | ||
|
|
5eb885da20 | ||
|
|
da4f286757 | ||
|
|
f6db447ad4 | ||
|
|
b472ba749c | ||
|
|
ef68b3b265 | ||
|
|
08d4a8b656 | ||
|
|
93ac131508 | ||
|
|
a7d1536140 | ||
|
|
4fa3fedea2 | ||
|
|
038e8babb1 | ||
|
|
0845477041 | ||
|
|
90156dd1f8 | ||
|
|
fe4b11cf4d | ||
|
|
2cbf234d05 | ||
|
|
a53575b4bf | ||
|
|
697abc6828 | ||
|
|
e96cfa3940 | ||
|
|
61a88e6715 | ||
|
|
e07a35b214 | ||
|
|
4a79fafbb9 | ||
|
|
02b9bff64e |
106
.env.example
@@ -1,10 +1,106 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
# AliasVault configuration file.
|
||||
#
|
||||
# Note: we recommend using the provided install.sh script to install and
|
||||
# configure AliasVault, as this will automatically set all of the following
|
||||
# variables for you and allow you to easily change them later via the CLI.
|
||||
# It also allows for easily updating AliasVault to a newer version in the
|
||||
# future.
|
||||
#
|
||||
# However if you still wish to manually install or configure AliasVault,
|
||||
# you can do so below.
|
||||
#
|
||||
# After changing settings here, make sure to restart all AliasVault
|
||||
# Docker containers to apply the changes.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Set the ports that your AliasVault will be accessible at.
|
||||
# These are the default ports that will be used by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these to any other ports that are available on your system.
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
SMTP_TLS_PORT=587
|
||||
|
||||
# Set the hostname that your AliasVault will be accessible at.
|
||||
# E.g. `aliasvault.mydomain.com` or if you're running it on your local machine, choose `localhost`.
|
||||
HOSTNAME=
|
||||
|
||||
# Set a random 32 character string for the JWT key.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
JWT_KEY=
|
||||
|
||||
# Set the password for the data protection certificate.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
DATA_PROTECTION_CERT_PASS=
|
||||
ADMIN_PASSWORD_HASH=
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
PRIVATE_EMAIL_DOMAINS=
|
||||
SMTP_TLS_ENABLED=false
|
||||
LETSENCRYPT_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Database configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# These are the credentials that are used by the PostgreSQL container
|
||||
# on startup to create the database and user, and for the application to
|
||||
# connect to the database.
|
||||
POSTGRES_DB=aliasvault
|
||||
POSTGRES_USER=aliasvault
|
||||
|
||||
# Set the password for the database user.
|
||||
# This can be generated using the following command:
|
||||
# $ openssl rand -base64 32
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Note: in order to change the password for an existing installation
|
||||
# refer to https://docs.aliasvault.net/misc/dev/database-operations.html
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Admin user configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set the password for the admin user. This is an encrypted hash that needs
|
||||
# to be generated using the `aliasvault-cli` tool. This allows you to login
|
||||
# to the admin panel at https://your-hostname/admin.
|
||||
#
|
||||
# For example:
|
||||
# docker run --rm ghcr.io/lanedirt/aliasvault-installcli:latest hash-password "my-password"
|
||||
#
|
||||
# Then copy the output and paste it into the ADMIN_PASSWORD_HASH variable below.
|
||||
# When changing the hash, update the ADMIN_PASSWORD_GENERATED variable to the current date and time
|
||||
# and then restart the AliasVault docker containers to apply the changes.
|
||||
ADMIN_PASSWORD_HASH=
|
||||
|
||||
# Set the date and time the admin password was last generated. When changing the
|
||||
# admin password hash manually, make sure to increase this value so the system
|
||||
# knows that the password has been changed and should be overwritten with the new hash.
|
||||
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Email server configuration for email aliases
|
||||
# ----------------------------------------------------------------------------
|
||||
# In order to use AliasVault's private email domains feature, you need to configure
|
||||
# your DNS. Please refer to the full documentation for more instructions on DNS:
|
||||
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup
|
||||
#
|
||||
# Set the private email domains below that are allowed to be used (comma separated values).
|
||||
# Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
|
||||
# To disable the private email domains feature, set this to "DISABLED.TLD"
|
||||
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
|
||||
|
||||
# Set whether TLS is enabled for SMTP.
|
||||
SMTP_TLS_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Let's Encrypt configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Set whether Let's Encrypt is enabled. This is only supported through
|
||||
# the install.sh script.
|
||||
LETSENCRYPT_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Optional configuration settings
|
||||
# ----------------------------------------------------------------------------
|
||||
PUBLIC_REGISTRATION_ENABLED=true
|
||||
IP_LOGGING_ENABLED=true
|
||||
|
||||
# Set the support email address which is shown to users in the main web app.
|
||||
# Keep this blank if you don't want to show a support email.
|
||||
SUPPORT_EMAIL=
|
||||
|
||||
31
.gitattributes
vendored
@@ -1,2 +1,31 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
# Set default behavior to automatically normalize line endings
|
||||
* text=auto
|
||||
|
||||
# Common files should always use LF (Unix-style) line endings
|
||||
*.sh text eol=lf
|
||||
*.cs text eol=lf
|
||||
*.razor text eol=lf
|
||||
*.css text eol=lf
|
||||
*.html text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# Docker files should use LF
|
||||
Dockerfile text eol=lf
|
||||
docker-compose*.yml text eol=lf
|
||||
|
||||
# Config files should use LF
|
||||
*.conf text eol=lf
|
||||
*.config text eol=lf
|
||||
.env* text eol=lf
|
||||
|
||||
# Batch scripts should always use CRLF (Windows-style) line endings
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Documentation should be normalized
|
||||
*.md text
|
||||
*.txt text
|
||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# These are supported funding model platforms
|
||||
buy_me_a_coffee: lanedirt
|
||||
10
.github/hooks/commit-msg
vendored
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Commit-msg hook to check commit messages for issue number in format "(#123)"
|
||||
|
||||
commit_message=$(cat "$1")
|
||||
|
||||
if ! grep -q "(\#[0-9]\+)" <<< "$commit_message"; then
|
||||
echo "Error: Commit message must contain an issue number in the format \"(#123)\""
|
||||
exit 1
|
||||
fi
|
||||
59
.github/workflows/browser-extension-build.yml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -164,60 +162,3 @@ jobs:
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
|
||||
upload-chrome-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-chrome-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-chrome-extension.outputs.sha_short) || needs.build-chrome-extension.outputs.sha_short) }}-chrome
|
||||
path: browser-extension/dist
|
||||
|
||||
- name: Upload Chrome Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*-chrome.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-firefox-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-firefox-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact Firefox
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-firefox-extension.outputs.sha_short) || needs.build-firefox-extension.outputs.sha_short) }}-firefox
|
||||
path: browser-extension/dist
|
||||
|
||||
- name: Download built artifact Firefox sources
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-firefox-extension.outputs.sha_short) || needs.build-firefox-extension.outputs.sha_short) }}-sources
|
||||
path: browser-extension/dist/aliasvault-browser-extension-*-sources.zip
|
||||
|
||||
- name: Upload Firefox Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*{-firefox,-sources}.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-edge-release-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-edge-extension]
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
steps:
|
||||
- name: Download built artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', needs.build-edge-extension.outputs.sha_short) || needs.build-edge-extension.outputs.sha_short) }}-edge
|
||||
path: browser-extension/dist
|
||||
|
||||
- name: Upload Edge Extension ZIP to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: browser-extension/dist/aliasvault-browser-extension-*-edge.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.github/workflows/docker-compose-build.yml
vendored
@@ -92,9 +92,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
- name: Test install.sh reset-admin-password output
|
||||
run: |
|
||||
output=$(./install.sh reset-password)
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
echo "Password reset output format is incorrect"
|
||||
echo "Expected: 'New admin password: <at least 8 base64 chars>'"
|
||||
|
||||
69
.github/workflows/docker-compose-pull.yml
vendored
@@ -43,44 +43,54 @@ jobs:
|
||||
|
||||
- name: Set permissions and run install.sh
|
||||
id: install_script
|
||||
continue-on-error: true
|
||||
run: |
|
||||
chmod +x install.sh
|
||||
./install.sh install --verbose
|
||||
|
||||
- name: Check if failure was due to version mismatch
|
||||
if: steps.install_script.outcome == 'failure'
|
||||
run: |
|
||||
if grep -q "Install script needs updating to match version" <<< "$(./install.sh install --verbose 2>&1)"; then
|
||||
echo "Test skipped: Install script version is newer than latest release version. This is expected behavior if the install script is run on a branch that is ahead of the latest release."
|
||||
exit 0
|
||||
else
|
||||
echo "Test failed due to an unexpected error"
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
./install.sh install --verbose
|
||||
exit_code=$?
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "Test skipped: Install script version is newer than latest release version. This is expected behavior if the install script is run on a branch that is ahead of the latest release."
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true # Force success exit code
|
||||
elif [ $exit_code -ne 0 ]; then
|
||||
false # Propagate failure
|
||||
fi
|
||||
} || {
|
||||
if [ $exit_code -eq 2 ]; then
|
||||
echo "skip_remaining=true" >> $GITHUB_OUTPUT
|
||||
true # Version mismatch is okay
|
||||
else
|
||||
exit $exit_code # Propagate other failures
|
||||
fi
|
||||
}
|
||||
|
||||
- name: Set up Docker Compose
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: docker compose -f docker-compose.yml up -d
|
||||
|
||||
- name: Wait for services to be up
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
# Wait for a few seconds
|
||||
sleep 10
|
||||
- name: Test if localhost:443 (WASM app) responds
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with 200 OK"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443 (WASM app) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
http_code=$(curl -k -s -o /dev/null -w "%{http_code}" https://localhost:443)
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with 200 OK. Check if client app and/or nginx is configured correctly."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with 200 OK"
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/api (WebApi) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -95,6 +105,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test if localhost:443/admin (Admin) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -109,6 +120,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test if localhost:2525 (SmtpService) responds
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
@@ -121,9 +133,10 @@ jobs:
|
||||
echo "SmtpService responded on port 2525"
|
||||
fi
|
||||
|
||||
- name: Test install.sh reset-password output
|
||||
- name: Test install.sh reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
output=$(./install.sh reset-password)
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
echo "Password reset output format is incorrect. Expected format: 'New admin password: <at least 8 base64 chars>'"
|
||||
echo "Actual output: $output"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# This workflow will publish new Docker images to the GitHub Container Registry when a new release is published.
|
||||
name: Publish Docker Images
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -11,7 +10,56 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
upload-install-script:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload install.sh to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: install.sh
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
package-browser-extensions:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: browser-extension
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
npm run zip:chrome
|
||||
npm run zip:firefox
|
||||
npm run zip:edge
|
||||
|
||||
- name: Upload extensions to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
browser-extension/dist/aliasvault-browser-extension-*-chrome.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-firefox.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-edge.zip
|
||||
browser-extension/dist/aliasvault-browser-extension-*-sources.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-and-push-docker:
|
||||
needs: [upload-install-script, package-browser-extensions]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -114,4 +162,4 @@ jobs:
|
||||
file: src/Utilities/AliasVault.InstallCli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-installcli:${{ github.ref_name }}
|
||||
4
.vscode/launch.json
vendored
@@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
116
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build and watch API",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Api"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Client"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Admin",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client CSS",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "build:client-css"],
|
||||
"problemMatcher": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Client"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Admin CSS",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "build:admin-css"],
|
||||
"problemMatcher": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/AliasVault.Admin"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client (API + Client + CSS)",
|
||||
"dependsOn": [
|
||||
"Build and watch API",
|
||||
"Build and watch Client",
|
||||
"Build and watch Client CSS"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run Unit Tests",
|
||||
"type": "shell",
|
||||
"command": "dotnet",
|
||||
"args": ["test"],
|
||||
"problemMatcher": "$msCompile",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/src/Tests/AliasVault.UnitTests"
|
||||
},
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run Browser Extension (Chrome Dev)",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "dev:chrome"],
|
||||
"problemMatcher": [],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/browser-extension"
|
||||
},
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
75
README.md
@@ -9,14 +9,17 @@
|
||||
|
||||
> 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.
|
||||
- **Built-in email server**: AliasVault includes its own email server that allows you to generate real working email addresses for each alias. Emails sent to these addresses are instantly visible in the AliasVault app and browser extension.
|
||||
- **Alias generation**: Generate aliases and assign them to a website, allowing you to use different email addresses and usernames for each website. Keeping your online identities separate and secure, making it harder for bad actors to link your accounts.
|
||||
- **Open-source**: The source code is available on GitHub and AliasVault can be self-hosted on your own server via an easy install script.
|
||||
- **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.
|
||||
- **Built-in email server**:
|
||||
- AliasVault includes its own email server that allows you to generate real working email addresses for each alias. Emails sent to these addresses are instantly visible in the AliasVault app and browser extension.
|
||||
- **Alias generation**:
|
||||
- Generate aliases and assign them to a website, allowing you to use different email addresses and usernames for each website. Keeping your online identities separate and secure, making it harder for bad actors to link your accounts.
|
||||
- **Open-source & Self-hostable**:
|
||||
- The source code is available on GitHub and AliasVault can be self-hosted on your own server via an easy install script.
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -67,7 +70,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 -L -o install.sh https://github.com/lanedirt/AliasVault/releases/latest/download/install.sh
|
||||
|
||||
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
|
||||
chmod +x install.sh
|
||||
@@ -81,7 +84,7 @@ The install script will output the URL where the app is available. By default th
|
||||
> Note: If you want to change the default AliasVault ports you can do so in the `.env` file.
|
||||
|
||||
## Documentation
|
||||
For more detailed information about the installation process and other topics, please see the official documentation website:
|
||||
For more information about the installation process, manual setup instructions and other topics, please see the official documentation website:
|
||||
- [Documentation website (docs.aliasvault.net) 📚](https://docs.aliasvault.net)
|
||||
|
||||
## Security Architecture
|
||||
@@ -98,44 +101,34 @@ For detailed information about our encryption implementation and security archit
|
||||
- [Security Architecture Diagram](https://docs.aliasvault.net/architecture)
|
||||
|
||||
## Roadmap
|
||||
AliasVault is under active development with new features being added regularly. We believe in transparency and want to share our vision for the future of the platform. Here's what we've accomplished and what we're working on next:
|
||||
|
||||
AliasVault is under active development, with a strong focus on usability, security, and cross-platform support.
|
||||
The main focus is on ensuring robust usability for everyday tasks, including comprehensive autofill capabilities across all platforms.
|
||||
|
||||
🛠️ Incremental releases are published every 2–3 weeks, with a strong emphasis on real-world testing and user feedback.
|
||||
During this phase, AliasVault can safely be used in production as it maintains strict data integrity and automatic migration guarantees.
|
||||
|
||||
Core features that are being worked on:
|
||||
|
||||
- [x] Core password & alias management
|
||||
- [x] End-to-end encryption
|
||||
- [x] Full end-to-end encryption
|
||||
- [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)
|
||||
- [ ] Import passwords from existing password managers (https://github.com/lanedirt/AliasVault/issues/542)
|
||||
- [x] Easy self-hosted installer
|
||||
- [x] Browser extensions with autofill feature (Chrome, Firefox, Edge, Safari, Brave)
|
||||
- [x] Built-in TOTP authenticator
|
||||
- [x] Import passwords from traditional password managers
|
||||
- [ ] iOS and Android native apps
|
||||
- [ ] Data model improvements to support reusable identities in combination with aliases
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
### Future Plans
|
||||
- [ ] Mobile apps (iOS, Android)
|
||||
- [ ] Team / organization features (sharing passwords/aliases)
|
||||
- [ ] Disposable phone number service
|
||||
👉 [View the full AliasVault roadmap here](https://github.com/lanedirt/AliasVault/issues/731)
|
||||
|
||||
Want to suggest a feature? Join our [Discord](https://discord.gg/DsaXMTEtpF) or create an issue on GitHub.
|
||||
### Got feedback or ideas?
|
||||
Feel free to open an issue or join our [Discord](https://discord.gg/DsaXMTEtpF)! Contributions are warmly welcomed—whether in feature development, testing, or spreading the word. Get in touch on Discord if you're interested in contributing.
|
||||
|
||||
## Tech Stack & Security
|
||||
### Support the mission
|
||||
Your donation helps me dedicate more time and resources to improving AliasVault, making the internet safer for everyone!
|
||||
|
||||
AliasVault is built with a modern, secure, and scalable technology stack, ensuring robust encryption and privacy protection.
|
||||
<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>
|
||||
|
||||
### Core Technologies
|
||||
- **C# & ASP.NET Core** – Reliable, high-performance backend for Web API.
|
||||
- **Blazor WASM** – Secure, interactive web UI.
|
||||
- **PostgreSQL & SQLite** – Database solutions, with SQLite powering encrypted user vaults.
|
||||
- **Docker** – Containerized deployment for scalability.
|
||||
- **Next.JS & React & Typescript** - Powering the AliasVault website and browser extensions
|
||||
|
||||
### Security & Cryptography
|
||||
- **Argon2id (Konscious.Security.Cryptography)** – Industry-leading password hashing.
|
||||
- **SRP** – Secure Remote Password (SRP-6a) protocol for authentication.
|
||||
- **MimeKit & SmtpServer** – Secure email processing and virtual addresses.
|
||||
|
||||
### Additional Tools
|
||||
- **Tailwind CSS & Flowbite** – Modern UI design.
|
||||
- **Playwright** – Automated end-to-end testing.
|
||||
- **SonarCloud** – Continuous code quality monitoring.
|
||||
|
||||
AliasVault prioritizes security, performance, and user privacy with a technology stack trusted by the industry.
|
||||
|
||||
@@ -29,7 +29,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.E2ETests.Client.
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{607945F3-9896-4544-99EC-F3496CF4D36B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.CsvImportExport", "src\Utilities\AliasVault.CsvImportExport\AliasVault.CsvImportExport.csproj", "{A9C9A606-C87E-4298-AB32-09B1884D7487}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.ImportExport", "src\Utilities\AliasVault.ImportExport\AliasVault.ImportExport.csproj", "{A9C9A606-C87E-4298-AB32-09B1884D7487}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{8A477241-B96C-4174-968D-D40CB77F1ECD}"
|
||||
EndProject
|
||||
@@ -59,8 +59,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Cryptography.Cli
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generators", "Generators", "{03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Generators.Password", "src\Generators\AliasVault.Generators.Password\AliasVault.Generators.Password.csproj", "{47F47A1B-49E0-406A-81C8-31FF2E4C339B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AliasVault.Generators.Identity", "src\Generators\AliasVault.Generators.Identity\AliasVault.Generators.Identity.csproj", "{80E74FBC-4EC8-45FB-B210-473337C484B5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DD359F0A-0180-4F8F-9E48-46213386BA4D}"
|
||||
@@ -161,10 +159,6 @@ Global
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -188,6 +182,7 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{ED328644-A152-403D-86EB-81201AA07744} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{8E6A418A-B305-465D-857D-49953605C18E} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{15EFE0D0-F41B-47D7-86B7-8F840335CB82} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
@@ -198,16 +193,14 @@ Global
|
||||
{1C7C8DE9-5F2A-43DB-A25E-33319E80A509} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{857BCD0E-753F-437A-AF75-B995B4D9A5FE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{FF0B0E64-1AE2-415C-A404-0EB78010821A} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{951C3DF8-DF22-4B2B-839F-FBA26DDD8ABD} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{E8D9C551-67D2-4651-8EDF-4262DF7375CE} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{DA175274-0FF7-4436-9266-742F96C2D1ED} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{BB7E701E-B1C6-453E-800A-E12CE256318D} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{341EC443-0B6B-4E8C-AF46-D6156573CEA5} = {BB7E701E-B1C6-453E-800A-E12CE256318D}
|
||||
{542C7B7D-C2B4-4AE3-9B2C-C62FCF4DFF8E} = {BB7E701E-B1C6-453E-800A-E12CE256318D}
|
||||
{47F47A1B-49E0-406A-81C8-31FF2E4C339B} = {03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}
|
||||
{80E74FBC-4EC8-45FB-B210-473337C484B5} = {03D55CA4-20B3-4FEA-9ADD-3C7B5B10E0FE}
|
||||
{59642CEF-D90A-4A6B-AD3F-9C6300D1E3FC} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{15EFE0D0-F41B-47D7-86B7-8F840335CB82} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{40CA41BF-9E67-4D0A-A3F8-38B94992E4CA} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
{D631A936-DD1C-40CC-B735-BD0A5D4F46A1} = {8A477241-B96C-4174-968D-D40CB77F1ECD}
|
||||
{34FADEB6-4B56-463B-B359-F844B43D76D9} = {DD359F0A-0180-4F8F-9E48-46213386BA4D}
|
||||
|
||||
@@ -18,4 +18,8 @@ npm run zip:firefox
|
||||
|
||||
# Build the Edge extension (saves in dist/edge-mv3)
|
||||
npm run zip:edge
|
||||
|
||||
# Build the Safari extension (saves in dist/safari-mv2 which is referenced by the dist/safari-xcode/AliasVault.xcodeproj project)
|
||||
npm run build:safari
|
||||
# Open the dist/safari-xcode/AliasVault.xcodeproj project in MacOS Xcode and run the project. This will install the extension to your Safari browser locally.
|
||||
```
|
||||
|
||||
40
browser-extension/package-lock.json
generated
@@ -9,9 +9,11 @@
|
||||
"version": "0.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
@@ -1473,6 +1475,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz",
|
||||
"integrity": "sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2056,10 +2070,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
|
||||
"devOptional": true,
|
||||
"version": "22.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@@ -9114,6 +9127,18 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/otpauth": {
|
||||
"version": "9.3.6",
|
||||
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.6.tgz",
|
||||
"integrity": "sha512-eIcCvuEvcAAPHxUKC9Q4uCe0Fh/yRc5jv9z+f/kvyIF2LPrhgAOuLB7J9CssGYhND/BL8M9hlHBTFmffpoQlMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.6.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/hectorm/otpauth?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -12223,7 +12248,6 @@
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unimport": {
|
||||
@@ -12425,9 +12449,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
|
||||
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
|
||||
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"build:chrome": "wxt build -b chrome",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"build:edge": "wxt build -b edge",
|
||||
"build:safari": "wxt build -b safari",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint src",
|
||||
@@ -24,9 +25,11 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^22.13.10",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"otpauth": "^9.3.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.4",
|
||||
|
||||
62
browser-extension/safari-xcode/.gitignore
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
|
||||
## App packaging
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
# Package.resolved
|
||||
# *.xcodeproj
|
||||
#
|
||||
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
|
||||
# hence it is not needed unless you have added a package configuration file to your project
|
||||
# .swiftpm
|
||||
|
||||
.build/
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from the Xcode workspace
|
||||
# *.xcworkspace
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build/
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo.
|
||||
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots/**/*.png
|
||||
fastlane/test_output
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.Safari.web-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SafariWebExtensionHandler.swift
|
||||
// AliasVault Extension
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import SafariServices
|
||||
import os.log
|
||||
|
||||
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
|
||||
|
||||
func beginRequest(with context: NSExtensionContext) {
|
||||
let request = context.inputItems.first as? NSExtensionItem
|
||||
|
||||
let profile: UUID?
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
|
||||
} else {
|
||||
profile = request?.userInfo?["profile"] as? UUID
|
||||
}
|
||||
|
||||
let message: Any?
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
message = request?.userInfo?[SFExtensionMessageKey]
|
||||
} else {
|
||||
message = request?.userInfo?["message"]
|
||||
}
|
||||
|
||||
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
|
||||
|
||||
let response = NSExtensionItem()
|
||||
if #available(iOS 15.0, macOS 11.0, *) {
|
||||
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
|
||||
} else {
|
||||
response.userInfo = [ "message": [ "echo": message ] ]
|
||||
}
|
||||
|
||||
context.completeRequest(returningItems: [ response ], completionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */; };
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAA2D81A9F7006174AB /* Base */; };
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAC2D81A9F7006174AB /* Icon.png */; };
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFAE2D81A9F7006174AB /* Style.css */; };
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB02D81A9F7006174AB /* Script.js */; };
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFB22D81A9F7006174AB /* ViewController.swift */; };
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB52D81A9F7006174AB /* Base */; };
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFB72D81A9F8006174AB /* Assets.xcassets */; };
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */; };
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD32D81A9F8006174AB /* background.js */; };
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD42D81A9F8006174AB /* popup.html */; };
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD52D81A9F8006174AB /* chunks */; };
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD62D81A9F8006174AB /* content-scripts */; };
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD72D81A9F8006174AB /* manifest.json */; };
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD82D81A9F8006174AB /* icon */; };
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFD92D81A9F8006174AB /* assets */; };
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */ = {isa = PBXBuildFile; fileRef = CE0CAFDA2D81A9F8006174AB /* src */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = CE0CAFBF2D81A9F8006174AB;
|
||||
remoteInfo = "AliasVault Extension";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
CE0CAFC12D81A9F8006174AB /* AliasVault Extension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AliasVault.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = ../Base.lproj/Main.html; sourceTree = "<group>"; };
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = "<group>"; };
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = Style.css; sourceTree = "<group>"; };
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = Script.js; sourceTree = "<group>"; };
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
CE0CAFB52D81A9F7006174AB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "AliasVault Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AliasVault_Extension.entitlements; sourceTree = "<group>"; };
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = background.js; path = "../../../dist/safari-mv2/background.js"; sourceTree = "<group>"; };
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = popup.html; path = "../../../dist/safari-mv2/popup.html"; sourceTree = "<group>"; };
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = chunks; path = "../../../dist/safari-mv2/chunks"; sourceTree = "<group>"; };
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "content-scripts"; path = "../../../dist/safari-mv2/content-scripts"; sourceTree = "<group>"; };
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = "../../../dist/safari-mv2/manifest.json"; sourceTree = "<group>"; };
|
||||
CE0CAFD82D81A9F8006174AB /* icon */ = {isa = PBXFileReference; lastKnownFileType = folder; name = icon; path = "../../../dist/safari-mv2/icon"; sourceTree = "<group>"; };
|
||||
CE0CAFD92D81A9F8006174AB /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = "../../../dist/safari-mv2/assets"; sourceTree = "<group>"; };
|
||||
CE0CAFDA2D81A9F8006174AB /* src */ = {isa = PBXFileReference; lastKnownFileType = folder; name = src; path = "../../../dist/safari-mv2/src"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CE0CAF9A2D81A9F7006174AB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */,
|
||||
CE0CAFA42D81A9F7006174AB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA42D81A9F7006174AB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA32D81A9F7006174AB /* AliasVault.app */,
|
||||
CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA52D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA62D81A9F7006174AB /* AppDelegate.swift */,
|
||||
CE0CAFB22D81A9F7006174AB /* ViewController.swift */,
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */,
|
||||
CE0CAFB72D81A9F8006174AB /* Assets.xcassets */,
|
||||
CE0CAFB92D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFBA2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFBB2D81A9F8006174AB /* AliasVault.entitlements */,
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */,
|
||||
);
|
||||
path = AliasVault;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFA82D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */,
|
||||
CE0CAFAC2D81A9F7006174AB /* Icon.png */,
|
||||
CE0CAFAE2D81A9F7006174AB /* Style.css */,
|
||||
CE0CAFB02D81A9F7006174AB /* Script.js */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFC42D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */,
|
||||
CE0CAFC52D81A9F8006174AB /* SafariWebExtensionHandler.swift */,
|
||||
CE0CAFC72D81A9F8006174AB /* Info.plist */,
|
||||
CE0CAFC82D81A9F8006174AB /* AliasVault_Extension.entitlements */,
|
||||
);
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFD22D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0CAFD32D81A9F8006174AB /* background.js */,
|
||||
CE0CAFD42D81A9F8006174AB /* popup.html */,
|
||||
CE0CAFD52D81A9F8006174AB /* chunks */,
|
||||
CE0CAFD62D81A9F8006174AB /* content-scripts */,
|
||||
CE0CAFD72D81A9F8006174AB /* manifest.json */,
|
||||
CE0CAFD82D81A9F8006174AB /* icon */,
|
||||
CE0CAFD92D81A9F8006174AB /* assets */,
|
||||
CE0CAFDA2D81A9F8006174AB /* src */,
|
||||
);
|
||||
name = Resources;
|
||||
path = "AliasVault Extension";
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */;
|
||||
buildPhases = (
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */,
|
||||
CE0CAFA02D81A9F7006174AB /* Frameworks */,
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */,
|
||||
CE0CAFCE2D81A9F8006174AB /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */,
|
||||
);
|
||||
name = AliasVault;
|
||||
productName = AliasVault;
|
||||
productReference = CE0CAFA32D81A9F7006174AB /* AliasVault.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */;
|
||||
buildPhases = (
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */,
|
||||
CE0CAFBD2D81A9F8006174AB /* Frameworks */,
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "AliasVault Extension";
|
||||
productName = "AliasVault Extension";
|
||||
productReference = CE0CAFC02D81A9F8006174AB /* AliasVault Extension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CE0CAF9B2D81A9F7006174AB /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1540;
|
||||
TargetAttributes = {
|
||||
CE0CAFA22D81A9F7006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
CE0CAFBF2D81A9F8006174AB = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */;
|
||||
compatibilityVersion = "Xcode 14.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CE0CAF9A2D81A9F7006174AB;
|
||||
productRefGroup = CE0CAFA42D81A9F7006174AB /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
CE0CAFA22D81A9F7006174AB /* AliasVault */,
|
||||
CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
CE0CAFA12D81A9F7006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFAD2D81A9F7006174AB /* Icon.png in Resources */,
|
||||
CE0CAFB12D81A9F7006174AB /* Script.js in Resources */,
|
||||
CE0CAFAB2D81A9F7006174AB /* Base in Resources */,
|
||||
CE0CAFAF2D81A9F7006174AB /* Style.css in Resources */,
|
||||
CE0CAFB82D81A9F8006174AB /* Assets.xcassets in Resources */,
|
||||
CE0CAFB62D81A9F7006174AB /* Base in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBE2D81A9F8006174AB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFDD2D81A9F8006174AB /* chunks in Resources */,
|
||||
CE0CAFE02D81A9F8006174AB /* icon in Resources */,
|
||||
CE0CAFE12D81A9F8006174AB /* assets in Resources */,
|
||||
CE0CAFE22D81A9F8006174AB /* src in Resources */,
|
||||
CE0CAFDB2D81A9F8006174AB /* background.js in Resources */,
|
||||
CE0CAFDF2D81A9F8006174AB /* manifest.json in Resources */,
|
||||
CE0CAFDC2D81A9F8006174AB /* popup.html in Resources */,
|
||||
CE0CAFDE2D81A9F8006174AB /* content-scripts in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
CE0CAF9F2D81A9F7006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFB32D81A9F7006174AB /* ViewController.swift in Sources */,
|
||||
CE0CAFA72D81A9F7006174AB /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CE0CAFBC2D81A9F8006174AB /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE0CAFC62D81A9F8006174AB /* SafariWebExtensionHandler.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
CE0CAFC32D81A9F8006174AB /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = CE0CAFBF2D81A9F8006174AB /* AliasVault Extension */;
|
||||
targetProxy = CE0CAFC22D81A9F8006174AB /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
CE0CAFA92D81A9F7006174AB /* Main.html */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFAA2D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.html;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE0CAFB42D81A9F7006174AB /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE0CAFB52D81A9F7006174AB /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.5;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "AliasVault Extension/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "AliasVault Extension";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari.extension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 12;
|
||||
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.16.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 = 12;
|
||||
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.16.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
"-framework",
|
||||
WebKit,
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.safari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CE0CAF9E2D81A9F7006174AB /* Build configuration list for PBXProject "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFC92D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCA2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCB2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault Extension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFCC2D81A9F8006174AB /* Debug */,
|
||||
CE0CAFCD2D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE0CAFCF2D81A9F8006174AB /* Build configuration list for PBXNativeTarget "AliasVault" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE0CAFD02D81A9F8006174AB /* Debug */,
|
||||
CE0CAFD12D81A9F8006174AB /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = CE0CAF9B2D81A9F7006174AB /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
@main
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Override point for customization after application launch.
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-16@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-32@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-128@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-256@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "mac-icon-512@2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 160 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>AliasVault</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
|
||||
<link rel="stylesheet" href="../Style.css">
|
||||
<script src="../Script.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<img src="../Icon.png" width="128" height="128" alt="AliasVault Icon">
|
||||
<p class="state-unknown">To enable AliasVault’s browser extension, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-on">AliasVault’s browser extension is currently enabled in Safari. If you wish to turn it off, go to the Safari Extensions preferences.</p>
|
||||
<p class="state-off">AliasVault’s browser extension is currently disabled in Safari. If you wish to turn it on, go to the Safari Extensions preferences.</p>
|
||||
<button class="open-preferences">Open Safari Extensions Preferences…</button>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="19085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19085"/>
|
||||
<plugIn identifier="com.apple.WebKit2IBPlugin" version="19085"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="AliasVault" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="AliasVault" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About AliasVault" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Hide AliasVault" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit AliasVault" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="AliasVault Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="76" y="-134"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="AliasVault" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="425" height="325"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
|
||||
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<wkWebViewConfiguration key="configuration">
|
||||
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
|
||||
<wkPreferences key="preferences"/>
|
||||
</wkWebViewConfiguration>
|
||||
</wkWebView>
|
||||
</subviews>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SFSafariWebExtensionConverterVersion</key>
|
||||
<string>15.4</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,22 @@
|
||||
function show(enabled, useSettingsInsteadOfPreferences) {
|
||||
if (useSettingsInsteadOfPreferences) {
|
||||
document.getElementsByClassName('state-on')[0].innerText = "AliasVault's Safari browser extension is succesfully enabled. If you wish to turn it off, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-off')[0].innerText = "AliasVault's Safari browser extension is currently disabled. If you wish to turn it on, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('state-unknown')[0].innerText = "To enable AliasVault's Safari browser extension, go to the Safari Extensions preferences.";
|
||||
document.getElementsByClassName('open-preferences')[0].innerText = "Open Safari Extensions Preferences…";
|
||||
}
|
||||
|
||||
if (typeof enabled === "boolean") {
|
||||
document.body.classList.toggle(`state-on`, enabled);
|
||||
document.body.classList.toggle(`state-off`, !enabled);
|
||||
} else {
|
||||
document.body.classList.remove(`state-on`);
|
||||
document.body.classList.remove(`state-off`);
|
||||
}
|
||||
}
|
||||
|
||||
function openPreferences() {
|
||||
webkit.messageHandlers.controller.postMessage("open-preferences");
|
||||
}
|
||||
|
||||
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);
|
||||
@@ -0,0 +1,44 @@
|
||||
* {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
--spacing: 20px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing);
|
||||
margin: 0 calc(var(--spacing) * 2);
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font: -apple-system-short-body;
|
||||
font-family: -apple-system-short-body, system-ui;
|
||||
}
|
||||
|
||||
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-on :is(.state-off, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.state-off :is(.state-on, .state-unknown) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AliasVault
|
||||
//
|
||||
// Created by Leendert de Borst on 12/03/2025.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SafariServices
|
||||
import WebKit
|
||||
|
||||
let extensionBundleIdentifier = "net.aliasvault.safari.extension"
|
||||
|
||||
class ViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
|
||||
|
||||
@IBOutlet var webView: WKWebView!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.webView.navigationDelegate = self
|
||||
|
||||
self.webView.configuration.userContentController.add(self, name: "controller")
|
||||
|
||||
self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
|
||||
guard let state = state, error == nil else {
|
||||
// Insert code to inform the user that something went wrong.
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if #available(macOS 13, *) {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), true)")
|
||||
} else {
|
||||
webView.evaluateJavaScript("show(\(state.isEnabled), false)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
if (message.body as! String != "open-preferences") {
|
||||
return;
|
||||
}
|
||||
|
||||
SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
// Show manual instructions in case opening the preferences fails due to restricted permissions.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Safari Extensions Settings"
|
||||
alert.informativeText = """
|
||||
Please follow these steps to enable the extension:
|
||||
1. Open Safari
|
||||
2. Click Safari > Settings in the menu bar
|
||||
3. Go to Extensions
|
||||
4. Find and enable "AliasVault"
|
||||
"""
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.runModal()
|
||||
}
|
||||
else {
|
||||
// Close app
|
||||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
browser-extension/safari-xcode/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
This folder contains the Xcode project used to publish the Safari version of the AliasVault browser extension to Apple.
|
||||
|
||||
This project was created using the `safari-web-extension-converter` tool. This XCode project is a simple wrapper around the
|
||||
WXT React browser extension, which is required by Apple in order to package and submit a Safari extension.
|
||||
|
||||
For more information see:
|
||||
- https://developer.apple.com/documentation/safariservices/converting-a-web-extension-for-safari
|
||||
- https://developer.apple.com/documentation/safariservices/running-your-safari-web-extension
|
||||
|
||||
To recreate this project, run the following command in the browser-extension root directory:
|
||||
|
||||
```bash
|
||||
# Build the Safari extension via the normal build process (outputs in dist/safari-mv2)
|
||||
npm run build:safari
|
||||
|
||||
# Convert the safari extension to an Xcode project (requires MacOS/XCode command line interface)
|
||||
xcrun safari-web-extension-converter --bundle-identifier net.aliasvault.safari --macos-only dist/safari-mv2 --project-location safari-xcode --force
|
||||
|
||||
# After the Xcode project is opened, you can run the extension by clicking the "Run" button in the top left corner of the Xcode window.
|
||||
# This will install the extension to your Safari browser and allow you to run it.
|
||||
```
|
||||
|
||||
> Note: This project does not need to be recreated when the extension is updated. It loads all extension files from the dist/safari-mv2 directory that is created by the `build:safari` command. To update the extension and/or publish a new version:
|
||||
> 1. Run `npm run build:safari` to rebuild the Safari extension
|
||||
> 2. Open this Xcode project and rebuild it to get the latest version
|
||||
> 3. Submit the extension to Apple for review via Xcode:
|
||||
> - Select the "Archive" option from the Product menu
|
||||
> - Select the newly created archive and click "Distribute App"
|
||||
> - Select "Distribute" and follow the instructions to submit to App Store Connect
|
||||
@@ -2,7 +2,7 @@ import { browser } from "wxt/browser";
|
||||
import { defineBackground } from 'wxt/sandbox';
|
||||
import { onMessage } from "webext-bridge/background";
|
||||
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
|
||||
import { handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, 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,8 @@ export default defineBackground({
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DEFAULT_IDENTITY_LANGUAGE', () => handleGetDefaultIdentityLanguage());
|
||||
onMessage('GET_PASSWORD_SETTINGS', () => handleGetPasswordSettings());
|
||||
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
|
||||
@@ -9,7 +9,25 @@ import { storage } from 'wxt/storage';
|
||||
import { BoolResponse as messageBoolResponse } from '../../utils/types/messaging/BoolResponse';
|
||||
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 { StringResponse as stringResponse } from '../../utils/types/messaging/StringResponse';
|
||||
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.
|
||||
@@ -179,13 +197,13 @@ export async function getEmailAddressesForVault(
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
|
||||
const emailAddresses = credentials
|
||||
.filter(cred => cred.Email != null)
|
||||
.map(cred => cred.Email)
|
||||
.filter(cred => cred.Alias?.Email != null)
|
||||
.map(cred => cred.Alias.Email ?? '')
|
||||
.filter((email, index, self) => self.indexOf(email) === index);
|
||||
|
||||
return emailAddresses.filter(email => {
|
||||
const domain = email.split('@')[1];
|
||||
return privateEmailDomains.includes(domain);
|
||||
const domain = email?.split('@')[1];
|
||||
return domain && privateEmailDomains.includes(domain);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,8 +211,8 @@ export async function getEmailAddressesForVault(
|
||||
* Get default email domain for a vault.
|
||||
*/
|
||||
export function handleGetDefaultEmailDomain(
|
||||
) : Promise<messageDefaultEmailDomainResponse> {
|
||||
return (async () : Promise<messageDefaultEmailDomainResponse> => {
|
||||
) : Promise<stringResponse> {
|
||||
return (async () : Promise<stringResponse> => {
|
||||
try {
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
@@ -215,21 +233,21 @@ export function handleGetDefaultEmailDomain(
|
||||
|
||||
// First check if the default domain that is configured in the vault is still valid.
|
||||
if (defaultEmailDomain && isValidDomain(defaultEmailDomain)) {
|
||||
return { success: true, domain: defaultEmailDomain };
|
||||
return { success: true, value: defaultEmailDomain };
|
||||
}
|
||||
|
||||
// If default domain is not valid, fall back to first available private domain.
|
||||
const firstPrivate = privateEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPrivate) {
|
||||
return { success: true, domain: firstPrivate };
|
||||
return { success: true, value: firstPrivate };
|
||||
}
|
||||
|
||||
// Return first valid public domain if no private domains are available.
|
||||
const firstPublic = publicEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPublic) {
|
||||
return { success: true, domain: firstPublic };
|
||||
return { success: true, value: firstPublic };
|
||||
}
|
||||
|
||||
// Return null if no valid domains are found
|
||||
@@ -241,6 +259,38 @@ export function handleGetDefaultEmailDomain(
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity language.
|
||||
*/
|
||||
export async function handleGetDefaultIdentityLanguage(
|
||||
) : Promise<stringResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const settingValue = sqliteClient.getDefaultIdentityLanguage();
|
||||
|
||||
return { success: true, value: settingValue };
|
||||
} catch (error) {
|
||||
console.error('Error getting default identity language:', error);
|
||||
return { success: false, error: 'Failed to get default identity language' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password settings.
|
||||
*/
|
||||
export async function handleGetPasswordSettings(
|
||||
) : Promise<messagePasswordSettingsResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const passwordSettings = sqliteClient.getPasswordSettings();
|
||||
|
||||
return { success: true, settings: passwordSettings };
|
||||
} catch (error) {
|
||||
console.error('Error getting password settings:', error);
|
||||
return { success: false, error: 'Failed to get password settings' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the derived key for the encrypted vault.
|
||||
*/
|
||||
|
||||
@@ -1,83 +1,109 @@
|
||||
import './contentScript/style.css';
|
||||
import { FormDetector } from '../utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupDisabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { canShowPopup, injectIcon } from './contentScript/Form';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
|
||||
import { injectIcon, popupDebounceTimeHasPassed } from './contentScript/Form';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
import { BoolResponse as messageBoolResponse } from '../utils/types/messaging/BoolResponse';
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { createShadowRootUi } from 'wxt/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
cssInjectionMode: 'ui',
|
||||
allFrames: true,
|
||||
matchAboutBlank: true,
|
||||
runAt: 'document_start',
|
||||
|
||||
/**
|
||||
* Main entry point for the content script.
|
||||
*/
|
||||
main(ctx) {
|
||||
async main(ctx) {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for input field focus
|
||||
document.addEventListener('focusin', async (e) => {
|
||||
if (ctx.isInvalid) {
|
||||
return;
|
||||
}
|
||||
// Create a shadow root UI for isolation
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'aliasvault-ui',
|
||||
position: 'inline',
|
||||
anchor: 'html',
|
||||
/**
|
||||
* 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()) {
|
||||
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();
|
||||
},
|
||||
});
|
||||
@@ -27,7 +27,7 @@ export function filterCredentials(credentials: Credential[], currentUrl: string,
|
||||
const credDomainParts = credUrlObject.hostname.toLowerCase().split('.');
|
||||
const currentDomainParts = currentUrlObject.hostname.toLowerCase().split('.');
|
||||
|
||||
// Get root domain (last two parts, e.g., 'aliasvaul.net')
|
||||
// Get root domain (last two parts, e.g., 'aliasvault.net')
|
||||
const credRootDomain = credDomainParts.slice(-2).join('.');
|
||||
const currentRootDomain = currentDomainParts.slice(-2).join('.');
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -35,8 +36,8 @@ export function hidePopupFor(ms: number) : void {
|
||||
* @param input - The input element that triggered the popup. Required when filling credentials to know which form to fill.
|
||||
*/
|
||||
export function fillCredential(credential: Credential, input: HTMLInputElement) : void {
|
||||
// Set debounce time to 800ms to prevent the popup from being shown again within 800ms because of autofill events.
|
||||
hidePopupFor(800);
|
||||
// Set debounce time to 300ms to prevent the popup from being shown again within 300ms because of autofill events.
|
||||
hidePopupFor(300);
|
||||
|
||||
const formDetector = new FormDetector(document, input);
|
||||
const form = formDetector.getForm();
|
||||
@@ -53,7 +54,7 @@ export function fillCredential(credential: Credential, input: HTMLInputElement)
|
||||
/**
|
||||
* Inject icon for a focused input element
|
||||
*/
|
||||
export function injectIcon(input: HTMLInputElement): void {
|
||||
export function injectIcon(input: HTMLInputElement, container: HTMLElement): void {
|
||||
const aliasvaultIconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
|
||||
@@ -64,18 +65,7 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
</svg>`;
|
||||
|
||||
const ICON_HTML = `
|
||||
<div class="aliasvault-input-icon" style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
">
|
||||
<div class="av-input-icon">
|
||||
<img src="data:image/svg+xml;base64,${btoa(aliasvaultIconSvg)}" style="width: 100%; height: 100%;" />
|
||||
</div>
|
||||
`;
|
||||
@@ -86,20 +76,12 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
}
|
||||
|
||||
// Create an overlay container at document level if it doesn't exist
|
||||
let overlayContainer = document.getElementById('aliasvault-overlay-container');
|
||||
let overlayContainer = container.querySelector('#aliasvault-overlay-container');
|
||||
if (!overlayContainer) {
|
||||
overlayContainer = document.createElement('div');
|
||||
overlayContainer = document.createElement('div') as HTMLElement;
|
||||
overlayContainer.id = 'aliasvault-overlay-container';
|
||||
overlayContainer.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483640;
|
||||
`;
|
||||
document.body.appendChild(overlayContainer);
|
||||
overlayContainer.className = 'av-overlay-container';
|
||||
container.appendChild(overlayContainer);
|
||||
}
|
||||
|
||||
// Create the icon element from the HTML template
|
||||
@@ -131,7 +113,7 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTimeout(() => input.focus(), 0);
|
||||
openAutofillPopup(input);
|
||||
openAutofillPopup(input, container);
|
||||
});
|
||||
|
||||
// Append the icon to the overlay container
|
||||
@@ -179,51 +161,53 @@ export function injectIcon(input: HTMLInputElement): void {
|
||||
* Trigger input events for an element to trigger form validation
|
||||
* which some websites require before the "continue" button is enabled.
|
||||
*/
|
||||
function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement) : void {
|
||||
// Create an overlay div that will show the highlight effect
|
||||
const overlay = document.createElement('div');
|
||||
function triggerInputEvents(element: HTMLInputElement | HTMLSelectElement, animate: boolean = true) : void {
|
||||
// Add keyframe animation if animation is requested
|
||||
if (animate) {
|
||||
// Create an overlay div that will show the highlight effect
|
||||
const overlay = document.createElement('div');
|
||||
|
||||
/**
|
||||
* Update position of the overlay.
|
||||
*/
|
||||
const updatePosition = () : void => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 999999991;
|
||||
pointer-events: none;
|
||||
top: ${rect.top}px;
|
||||
left: ${rect.left}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
background-color: rgba(244, 149, 65, 0.3);
|
||||
border-radius: ${getComputedStyle(element).borderRadius};
|
||||
animation: fadeOut 1.4s ease-out forwards;
|
||||
/**
|
||||
* Update position of the overlay.
|
||||
*/
|
||||
const updatePosition = () : void => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 999999991;
|
||||
pointer-events: none;
|
||||
top: ${rect.top}px;
|
||||
left: ${rect.left}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
background-color: rgba(244, 149, 65, 0.3);
|
||||
border-radius: ${getComputedStyle(element).borderRadius};
|
||||
animation: fadeOut 1.4s ease-out forwards;
|
||||
`;
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
|
||||
// Add scroll event listener
|
||||
window.addEventListener('scroll', updatePosition);
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
};
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
updatePosition();
|
||||
|
||||
// Add scroll event listener
|
||||
window.addEventListener('scroll', updatePosition);
|
||||
|
||||
// Add keyframe animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Remove overlay and cleanup after animation
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('scroll', updatePosition);
|
||||
overlay.remove();
|
||||
style.remove();
|
||||
}, 1400);
|
||||
// Remove overlay and cleanup after animation
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('scroll', updatePosition);
|
||||
overlay.remove();
|
||||
style.remove();
|
||||
}, 1400);
|
||||
}
|
||||
|
||||
// Trigger events
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Check if the current theme is dark.
|
||||
*/
|
||||
export function isDarkMode(): boolean {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
768
browser-extension/src/entrypoints/contentScript/style.css
Normal file
@@ -0,0 +1,768 @@
|
||||
/* 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: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.av-loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 50%;
|
||||
border-top-color: transparent;
|
||||
animation: av-loading-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes av-loading-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.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: 16px 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;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-help-text {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-modes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
background: #374151;
|
||||
border: 1px solid #4b5563;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-btn:hover {
|
||||
background: #4b5563;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.av-create-popup-mode-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-icon .av-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-content h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.av-create-popup-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
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-input-default {
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
/* Custom Credential UI Styles */
|
||||
.av-create-popup-custom-toggle {
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.av-create-popup-toggle-text {
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.av-create-popup-custom-fields {
|
||||
margin: 16px 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-field-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.av-create-popup-field-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #eee;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.av-create-popup-input-error {
|
||||
border-color: #ef4444 !important;
|
||||
box-shadow: 0 0 0 1px #ef4444 !important;
|
||||
}
|
||||
|
||||
.av-create-popup-error-text {
|
||||
color: #ef4444;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.av-create-popup-password-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.av-create-popup-password-preview input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 9px;
|
||||
background: #374151;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #e5e7eb;
|
||||
transition: background-color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-create-popup-regenerate-btn .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.av-create-popup-error {
|
||||
margin-top: 16px;
|
||||
padding: 8px 12px;
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.av-create-popup-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.av-create-popup-back {
|
||||
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-back:hover {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.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 fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; transform: scale(1.02); }
|
||||
100% { opacity: 0; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Create Popup Styles */
|
||||
.av-create-popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-create-popup-mode {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.av-create-popup-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper .av-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.av-create-popup-title-wrapper .av-create-popup-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-title-container:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown .av-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown-menu {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
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);
|
||||
z-index: 1000;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-dropdown-menu::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #1f2937;
|
||||
border-left: 1px solid #374151;
|
||||
border-top: 1px solid #374151;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background-color 0.2s;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #374151;
|
||||
border-radius: 8px;
|
||||
color: #d68338;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-content h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.av-create-popup-mode-option .av-create-popup-mode-content p {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
type ButtonProps = {
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
variant?: 'primary' | 'secondary';
|
||||
|
||||
@@ -135,7 +135,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
href={`https://spamok.com/${email.split('@')[0]}/${mail.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex justify-between items-center p-2 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -152,7 +152,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
<Link
|
||||
key={mail.id}
|
||||
to={`/emails/${mail.id}`}
|
||||
className={`flex justify-between items-center p-2 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
className={`flex justify-between items-center p-2 ps-3 pe-3 rounded cursor-pointer bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 ${
|
||||
mail.id > lastEmailId ? 'bg-yellow-50 dark:bg-yellow-900/30' : ''
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -95,7 +95,10 @@ const Header: React.FC<HeaderProps> = ({
|
||||
>
|
||||
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
|
||||
<h1 className="text-gray-900 dark:text-white text-xl font-bold">AliasVault</h1>
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
|
||||
{!import.meta.env.SAFARI && (
|
||||
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { TotpCode } from '../../../utils/types/TotpCode';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
|
||||
type TotpViewerProps = {
|
||||
credentialId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows TOTP codes for a credential.
|
||||
*/
|
||||
export const TotpViewer: React.FC<TotpViewerProps> = ({ credentialId }) => {
|
||||
const [totpCodes, setTotpCodes] = useState<TotpCode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentCodes, setCurrentCodes] = useState<Record<string, string>>({});
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Gets the remaining seconds for the TOTP code.
|
||||
*/
|
||||
const getRemainingSeconds = (step = 30): number => {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: 'dummy', // We only need this for timing calculations
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: step
|
||||
});
|
||||
return totp.period - (Math.floor(Date.now() / 1000) % totp.period);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the remaining percentage for the TOTP code.
|
||||
*/
|
||||
const getRemainingPercentage = (): number => {
|
||||
const remaining = getRemainingSeconds();
|
||||
// Invert the percentage so it counts down instead of up
|
||||
return Math.floor(((30.0 - remaining) / 30.0) * 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a TOTP code for a given secret key.
|
||||
*/
|
||||
const generateTotpCode = (secretKey: string): string => {
|
||||
try {
|
||||
const totp = new OTPAuth.TOTP({
|
||||
secret: secretKey,
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30
|
||||
});
|
||||
return totp.generate();
|
||||
} catch (error) {
|
||||
console.error('Error generating TOTP code:', error);
|
||||
return 'Error';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies a TOTP code to the clipboard.
|
||||
*/
|
||||
const copyToClipboard = async (code: string, id: string): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopiedId(id);
|
||||
|
||||
// Reset copied state after 2 seconds
|
||||
setTimeout(() => {
|
||||
setCopiedId(null);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the TOTP codes for the credential.
|
||||
*/
|
||||
const loadTotpCodes = async (): Promise<void> => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const codes = dbContext.sqliteClient.getTotpCodesForCredential(credentialId);
|
||||
setTotpCodes(codes);
|
||||
} catch (error) {
|
||||
console.error('Error loading TOTP codes:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTotpCodes();
|
||||
}, [credentialId, dbContext?.sqliteClient]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Updates the current TOTP codes.
|
||||
*/
|
||||
const updateTotpCodes = (prevCodes: Record<string, string>): Record<string, string> => {
|
||||
const newCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
const generatedCode = generateTotpCode(code.SecretKey);
|
||||
// Only update if we have a valid code
|
||||
if (generatedCode !== 'Error') {
|
||||
newCodes[code.Id] = generatedCode;
|
||||
} else {
|
||||
// Keep the previous code if there's an error
|
||||
newCodes[code.Id] = prevCodes[code.Id] ?? 'Error';
|
||||
}
|
||||
});
|
||||
return newCodes;
|
||||
};
|
||||
|
||||
// Generate initial codes
|
||||
const initialCodes: Record<string, string> = {};
|
||||
totpCodes.forEach(code => {
|
||||
initialCodes[code.Id] = generateTotpCode(code.SecretKey);
|
||||
});
|
||||
setCurrentCodes(initialCodes);
|
||||
|
||||
// Set up interval to refresh codes
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentCodes(updateTotpCodes);
|
||||
}, 1000);
|
||||
|
||||
// Clean up interval on unmount or when totpCodes change
|
||||
return () : void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [totpCodes]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">Two-factor authentication</h2>
|
||||
Loading TOTP codes...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (totpCodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-base font-semibold text-gray-900 dark:text-white">Two-factor authentication</h2>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{totpCodes.map(totpCode => (
|
||||
<button
|
||||
key={totpCode.Id}
|
||||
className={`w-full text-left p-2 ps-3 pe-3 rounded bg-white dark:bg-gray-800 shadow hover:shadow-md transition-all border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700`}
|
||||
onClick={() => copyToClipboard(currentCodes[totpCode.Id], totpCode.Id)}
|
||||
aria-label={`Copy ${totpCode.Name} code`}
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">{totpCode.Name}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{currentCodes[totpCode.Id]}
|
||||
</span>
|
||||
<div className="text-xs">
|
||||
{copiedId === totpCode.Id ? (
|
||||
<span className="text-green-600 dark:text-green-400">Copied!</span>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400">{getRemainingSeconds()}s</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1 h-6 bg-gray-200 rounded-full dark:bg-gray-600">
|
||||
<div
|
||||
className="bg-blue-600 rounded-full transition-all"
|
||||
style={{ height: `${getRemainingPercentage()}%`, width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCall
|
||||
import { useDb } from './DbContext';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
type AuthContextType = {
|
||||
isLoggedIn: boolean;
|
||||
@@ -66,6 +67,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
*/
|
||||
const login = useCallback(async () : Promise<void> => {
|
||||
setIsLoggedIn(true);
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
134
browser-extension/src/entrypoints/popup/context/ThemeContext.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { createContext, useContext, useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Theme type.
|
||||
*/
|
||||
type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
/**
|
||||
* Theme preference key in storage.
|
||||
*/
|
||||
const THEME_PREFERENCE_KEY = 'local:theme';
|
||||
|
||||
/**
|
||||
* Theme context type.
|
||||
*/
|
||||
type ThemeContextType = {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme context.
|
||||
*/
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Theme provider
|
||||
*/
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
/**
|
||||
* Theme state that can be 'light', 'dark', or 'system'.
|
||||
*/
|
||||
const [theme, setTheme] = useState<Theme>('system');
|
||||
|
||||
/**
|
||||
* Tracks whether dark mode is active (based on theme or system preference).
|
||||
*/
|
||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load theme setting from storage.
|
||||
*/
|
||||
const loadTheme = async () : Promise<void> => {
|
||||
const savedTheme = await getTheme();
|
||||
setTheme(savedTheme);
|
||||
};
|
||||
loadTheme();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set the theme and save to storage.
|
||||
*/
|
||||
const updateTheme = useCallback((newTheme: Theme): void => {
|
||||
setTheme(newTheme);
|
||||
setStoredTheme(newTheme);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get the theme from storage.
|
||||
*/
|
||||
const getTheme = async (): Promise<Theme> => {
|
||||
return (await storage.getItem(THEME_PREFERENCE_KEY) as Theme) || 'system';
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the theme in storage.
|
||||
*/
|
||||
const setStoredTheme = async (theme: Theme): Promise<void> => {
|
||||
await storage.setItem(THEME_PREFERENCE_KEY, theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* Effect to apply theme to document and handle system preference changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Update the dark mode status.
|
||||
*/
|
||||
const updateDarkMode = (): void => {
|
||||
if (theme === 'system') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setIsDarkMode(prefersDark);
|
||||
document.documentElement.classList.toggle('dark', prefersDark);
|
||||
} else {
|
||||
const isDark = theme === 'dark';
|
||||
setIsDarkMode(isDark);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateDarkMode();
|
||||
|
||||
// Listen for system preference changes if using 'system' theme
|
||||
if (theme === 'system') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
/**
|
||||
* Update the dark mode status when the system preference changes.
|
||||
*/
|
||||
const handler = () : void => updateDarkMode();
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () : void => mediaQuery.removeEventListener('change', handler);
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
theme,
|
||||
setTheme: updateTheme,
|
||||
isDarkMode,
|
||||
}),
|
||||
[theme, isDarkMode, updateTheme]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use theme state
|
||||
*/
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -6,14 +6,6 @@
|
||||
<title>AliasVault</title>
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
<script>
|
||||
// Check if expanded=true is in the URL, which means the popup was opened in expanded mode with unlimited width.
|
||||
// If not, set the width to 350px to force the default popup to a fixed width.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.get('expanded')) {
|
||||
document.documentElement.classList.add('max-w-[350px]');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-900">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,6 +4,11 @@ import { AuthProvider } from './context/AuthContext';
|
||||
import { WebApiProvider } from './context/WebApiContext';
|
||||
import { DbProvider } from './context/DbContext';
|
||||
import { LoadingProvider } from './context/LoadingContext';
|
||||
import { ThemeProvider } from './context/ThemeContext';
|
||||
import { setupExpandedMode } from '../../utils/ExpandedMode';
|
||||
|
||||
// Run before React initializes to ensure the popup is always a fixed width except for when explicitly expanded.
|
||||
setupExpandedMode();
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
@@ -11,7 +16,9 @@ root.render(
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<App />
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { GLOBAL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '../../contentScript/Popup';
|
||||
|
||||
type ApiOption = {
|
||||
label: string;
|
||||
@@ -19,6 +20,7 @@ const AuthSettings: React.FC = () => {
|
||||
const [selectedOption, setSelectedOption] = useState<string>('');
|
||||
const [customUrl, setCustomUrl] = useState<string>('');
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -27,6 +29,15 @@ const AuthSettings: React.FC = () => {
|
||||
const loadStoredSettings = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
const clientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
const globallyEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
|
||||
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
|
||||
|
||||
if (dismissUntil) {
|
||||
setIsGloballyEnabled(false);
|
||||
} else {
|
||||
setIsGloballyEnabled(globallyEnabled);
|
||||
}
|
||||
|
||||
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === apiUrl);
|
||||
|
||||
if (matchingOption) {
|
||||
@@ -74,6 +85,23 @@ const AuthSettings: React.FC = () => {
|
||||
await storage.setItem('local:clientUrl', value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle global popup.
|
||||
*/
|
||||
const toggleGlobalPopup = async () : Promise<void> => {
|
||||
const newGloballyEnabled = !isGloballyEnabled;
|
||||
|
||||
await storage.setItem(GLOBAL_POPUP_ENABLED_KEY, newGloballyEnabled);
|
||||
|
||||
if (newGloballyEnabled) {
|
||||
// Reset all disabled sites when enabling globally
|
||||
await storage.setItem(DISABLED_SITES_KEY, []);
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
}
|
||||
|
||||
setIsGloballyEnabled(newGloballyEnabled);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="mb-6">
|
||||
@@ -124,6 +152,23 @@ const AuthSettings: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Autofill Popup Settings Section */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
|
||||
<button
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
isGloballyEnabled
|
||||
? 'bg-green-200 text-green-800 hover:bg-green-300 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
|
||||
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
|
||||
}`}
|
||||
>
|
||||
{isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,213 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useDb } from '../context/DbContext';
|
||||
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';
|
||||
import SqliteClient from '../../../utils/SqliteClient';
|
||||
|
||||
type BlockProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a block.
|
||||
*/
|
||||
const Block: React.FC<BlockProps> = ({ children, className = '' }) => (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the header block.
|
||||
*/
|
||||
const HeaderBlock: React.FC<{ credential: Credential; onOpenNewPopup: () => void }> = ({ credential, onOpenNewPopup }) => (
|
||||
<Block className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={SqliteClient.imgSrcFromBytes(credential.Logo)}
|
||||
alt={credential.ServiceName}
|
||||
className="w-12 h-12 rounded-lg mr-4"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{credential.ServiceName}</h1>
|
||||
{credential.ServiceUrl && (
|
||||
<a
|
||||
href={credential.ServiceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onOpenNewPopup}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
title="Open in new window"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the email block.
|
||||
*/
|
||||
const EmailBlock: React.FC<{ email: string; isSupported: boolean }> = ({ email, isSupported }) => (
|
||||
<Block>
|
||||
{isSupported && <EmailPreview email={email} />}
|
||||
</Block>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the TOTP viewer block.
|
||||
*/
|
||||
const TotpBlock: React.FC<{ credentialId: string }> = ({ credentialId }) => (
|
||||
<Block>
|
||||
<TotpViewer credentialId={credentialId} />
|
||||
</Block>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render the login credentials block.
|
||||
*/
|
||||
const LoginCredentialsBlock: React.FC<{ credential: Credential }> = ({ credential }) => {
|
||||
const email = credential.Alias?.Email?.trim();
|
||||
const username = credential.Username?.trim();
|
||||
const password = credential.Password?.trim();
|
||||
|
||||
if (!email && !username && !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Login credentials</h2>
|
||||
{email && (
|
||||
<FormInputCopyToClipboard
|
||||
id="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
/>
|
||||
)}
|
||||
{username && (
|
||||
<FormInputCopyToClipboard
|
||||
id="username"
|
||||
label="Username"
|
||||
value={username}
|
||||
/>
|
||||
)}
|
||||
{password && (
|
||||
<FormInputCopyToClipboard
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
type="password"
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the alias block.
|
||||
*/
|
||||
const AliasBlock: React.FC<{ credential: Credential; isValidDate: (date: string | null | undefined) => boolean }> = ({
|
||||
credential,
|
||||
isValidDate
|
||||
}) => {
|
||||
const hasFirstName = Boolean(credential.Alias?.FirstName?.trim());
|
||||
const hasLastName = Boolean(credential.Alias?.LastName?.trim());
|
||||
const hasNickName = Boolean(credential.Alias?.NickName?.trim());
|
||||
const hasBirthDate = isValidDate(credential.Alias?.BirthDate);
|
||||
|
||||
if (!hasFirstName && !hasLastName && !hasNickName && !hasBirthDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Alias</h2>
|
||||
{(hasFirstName || hasLastName) && (
|
||||
<FormInputCopyToClipboard
|
||||
id="fullName"
|
||||
label="Full Name"
|
||||
value={[credential.Alias?.FirstName, credential.Alias?.LastName].filter(Boolean).join(' ')}
|
||||
/>
|
||||
)}
|
||||
{hasFirstName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="firstName"
|
||||
label="First Name"
|
||||
value={credential.Alias?.FirstName}
|
||||
/>
|
||||
)}
|
||||
{hasLastName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
value={credential.Alias?.LastName}
|
||||
/>
|
||||
)}
|
||||
{hasBirthDate && (
|
||||
<FormInputCopyToClipboard
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
value={new Date(credential.Alias?.BirthDate).toISOString().split('T')[0]}
|
||||
/>
|
||||
)}
|
||||
{hasNickName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="nickName"
|
||||
label="Nickname"
|
||||
value={credential.Alias?.NickName ?? ''}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the notes block.
|
||||
*/
|
||||
const NotesBlock: React.FC<{ notes: string | undefined }> = ({ notes }) => {
|
||||
if (!notes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Notes</h2>
|
||||
<div className="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
|
||||
<p className="text-gray-900 dark:text-gray-100 whitespace-pre-wrap">
|
||||
{notes}
|
||||
</p>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Credential details page.
|
||||
@@ -20,7 +222,7 @@ const CredentialDetails: React.FC = () => {
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = () : boolean => {
|
||||
const isPopup = (): boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
@@ -28,7 +230,7 @@ const CredentialDetails: React.FC = () => {
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = () : void => {
|
||||
const openInNewPopup = (): void => {
|
||||
const width = 380;
|
||||
const height = 600;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
@@ -40,38 +242,39 @@ const CredentialDetails: React.FC = () => {
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
// Close the current tab
|
||||
window.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the email domain is supported for email preview.
|
||||
*
|
||||
* @param email The email address to check
|
||||
* @returns True if the domain is supported, false otherwise
|
||||
* Check if the email domain is supported.
|
||||
*/
|
||||
const isEmailDomainSupported = (email: string): boolean => {
|
||||
// Extract domain from email
|
||||
const domain = email.split('@')[1]?.toLowerCase();
|
||||
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if domain is in public or private domains
|
||||
const publicDomains = dbContext.publicEmailDomains ?? [];
|
||||
const privateDomains = dbContext.privateEmailDomains ?? [];
|
||||
|
||||
// Check if the domain ends with any of the supported domains
|
||||
return [...publicDomains, ...privateDomains].some(supportedDomain =>
|
||||
domain === supportedDomain || domain.endsWith(`.${supportedDomain}`)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a date is valid.
|
||||
*/
|
||||
const isValidDate = useCallback((date: string | null | undefined): boolean => {
|
||||
if (!date || date === '0001-01-01 00:00:00') {
|
||||
return false;
|
||||
}
|
||||
const dateObj = new Date(date);
|
||||
return !isNaN(dateObj.getTime());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// For popup windows, ensure we have proper history state for navigation
|
||||
if (isPopup()) {
|
||||
// Clear existing history and create fresh entries
|
||||
window.history.replaceState({}, '', `popup.html#/credentials`);
|
||||
window.history.pushState({}, '', `popup.html#/credentials/${id}`);
|
||||
}
|
||||
@@ -99,127 +302,26 @@ const CredentialDetails: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={credential.Logo ? `data:image/x-icon;base64,${Buffer.from(credential.Logo).toString('base64')}` : '/assets/images/service-placeholder.webp'}
|
||||
alt={credential.ServiceName}
|
||||
className="w-12 h-12 rounded-lg mr-4"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{credential.ServiceName}</h1>
|
||||
{credential.ServiceUrl && (
|
||||
<a
|
||||
href={credential.ServiceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
|
||||
>
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={openInNewPopup}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
title="Open in new window"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{credential.Email && (
|
||||
<>
|
||||
{isEmailDomainSupported(credential.Email) && (
|
||||
<div className="mt-6">
|
||||
<EmailPreview
|
||||
email={credential.Email}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
<div className="space-y-4 lg:col-span-2 xl:col-span-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Login credentials</h2>
|
||||
<FormInputCopyToClipboard
|
||||
id="email"
|
||||
label="Email"
|
||||
value={credential.Email ?? ''}
|
||||
/>
|
||||
<FormInputCopyToClipboard
|
||||
id="username"
|
||||
label="Username"
|
||||
value={credential.Username}
|
||||
/>
|
||||
<FormInputCopyToClipboard
|
||||
id="password"
|
||||
label="Password"
|
||||
value={credential.Password}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Alias</h2>
|
||||
<FormInputCopyToClipboard
|
||||
id="fullName"
|
||||
label="Full Name"
|
||||
value={`${credential.Alias.FirstName} ${credential.Alias.LastName}`}
|
||||
/>
|
||||
<FormInputCopyToClipboard
|
||||
id="firstName"
|
||||
label="First Name"
|
||||
value={credential.Alias.FirstName}
|
||||
/>
|
||||
<FormInputCopyToClipboard
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
value={credential.Alias.LastName}
|
||||
/>
|
||||
<FormInputCopyToClipboard
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
value={credential.Alias.BirthDate ? new Date(credential.Alias.BirthDate).toISOString().split('T')[0] : ''}
|
||||
/>
|
||||
{credential.Alias.NickName && (
|
||||
<FormInputCopyToClipboard
|
||||
id="nickName"
|
||||
label="Nickname"
|
||||
value={credential.Alias.NickName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{credential.Notes && (
|
||||
<div className="space-y-4 lg:col-span-2 xl:col-span-1">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Notes</h2>
|
||||
<div className="p-4 bg-gray-50 rounded-lg dark:bg-gray-700">
|
||||
<p className="text-gray-900 dark:text-gray-100 whitespace-pre-wrap">
|
||||
{credential.Notes}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<HeaderBlock credential={credential} onOpenNewPopup={openInNewPopup} />
|
||||
|
||||
{credential.Alias?.Email && (
|
||||
<EmailBlock
|
||||
email={credential.Alias.Email}
|
||||
isSupported={isEmailDomainSupported(credential.Alias.Email)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TotpBlock credentialId={credential.Id} />
|
||||
|
||||
<LoginCredentialsBlock credential={credential} />
|
||||
|
||||
<AliasBlock
|
||||
credential={credential}
|
||||
isValidDate={isValidDate}
|
||||
/>
|
||||
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useDb } from '../context/DbContext';
|
||||
import { Credential } from '../../../utils/types/Credential';
|
||||
import { Buffer } from 'buffer';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { useWebApi } from '../context/WebApiContext';
|
||||
@@ -10,7 +9,7 @@ import ReloadButton from '../components/ReloadButton';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '../../../hooks/useMinDurationLoading';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import SqliteClient from '../../../utils/SqliteClient';
|
||||
/**
|
||||
* Credentials list page.
|
||||
*/
|
||||
@@ -107,11 +106,12 @@ const CredentialsList: React.FC = () => {
|
||||
// Add this function to filter credentials
|
||||
const filteredCredentials = credentials.filter(cred => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return (
|
||||
cred.ServiceName.toLowerCase().includes(searchLower) ||
|
||||
cred.Username.toLowerCase().includes(searchLower) ||
|
||||
(cred.Email?.toLowerCase().includes(searchLower))
|
||||
);
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase()
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchLower));
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
@@ -163,7 +163,7 @@ const CredentialsList: React.FC = () => {
|
||||
className="w-full p-2 border dark:border-gray-600 rounded flex items-center bg-white dark:bg-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<img
|
||||
src={cred.Logo ? `data:image/x-icon;base64,${Buffer.from(cred.Logo).toString('base64')}` : '/assets/images/service-placeholder.webp'}
|
||||
src={SqliteClient.imgSrcFromBytes(cred.Logo)}
|
||||
alt={cred.ServiceName}
|
||||
className="w-8 h-8 mr-2 flex-shrink-0"
|
||||
onError={(e) => {
|
||||
|
||||
@@ -12,6 +12,8 @@ import { LoginResponse } from '../../../utils/types/webapi/Login';
|
||||
import LoginServerInfo from '../components/LoginServerInfo';
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from 'wxt/storage';
|
||||
import { ApiAuthError } from '../../../utils/types/errors/ApiAuthError';
|
||||
|
||||
/**
|
||||
* Login page
|
||||
*/
|
||||
@@ -108,7 +110,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
const vaultResponseJson = await webApi.fetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
@@ -130,8 +132,13 @@ const Login: React.FC = () => {
|
||||
|
||||
// Show app.
|
||||
hideLoading();
|
||||
} catch {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
} catch (err) {
|
||||
// Show API authentication errors as-is.
|
||||
if (err instanceof ApiAuthError) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
@@ -143,13 +150,19 @@ const Login: React.FC = () => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!passwordHashString || !passwordHashBase64 || !loginResponse) {
|
||||
throw new Error('Required login data not found');
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
if (!passwordHashString || !passwordHashBase64 || !loginResponse) {
|
||||
throw new Error('Required login data not found');
|
||||
}
|
||||
|
||||
// Validate that 2FA code is a 6-digit number
|
||||
const code = twoFactorCode.trim();
|
||||
if (!/^\d{6}$/.test(code)) {
|
||||
throw new ApiAuthError('Please enter a valid 6-digit authentication code.');
|
||||
}
|
||||
|
||||
const validationResponse = await srpUtil.validateLogin2Fa(
|
||||
credentials.username,
|
||||
passwordHashString,
|
||||
@@ -164,7 +177,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
// Try to get latest vault manually providing auth token.
|
||||
const vaultResponseJson = await webApi.fetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
|
||||
'Authorization': `Bearer ${validationResponse.token.token}`
|
||||
} });
|
||||
|
||||
@@ -192,8 +205,13 @@ const Login: React.FC = () => {
|
||||
setLoginResponse(null);
|
||||
hideLoading();
|
||||
} catch (err) {
|
||||
setError('Invalid authentication code. Please try again.');
|
||||
// Show API authentication errors as-is.
|
||||
console.error('2FA error:', err);
|
||||
if (err instanceof ApiAuthError) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('Could not reach AliasVault server. Please try again later or contact support if the problem persists.');
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DISABLED_SITES_KEY, GLOBAL_POPUP_ENABLED_KEY } from '../../contentScrip
|
||||
import { AppInfo } from '../../../utils/AppInfo';
|
||||
import { storage } from "wxt/storage";
|
||||
import { browser } from 'wxt/browser';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
|
||||
/**
|
||||
* Popup settings type.
|
||||
@@ -18,6 +19,7 @@ type PopupSettings = {
|
||||
* Settings page component.
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
currentUrl: '',
|
||||
@@ -49,7 +51,7 @@ const Settings: React.FC = () => {
|
||||
disabledUrls,
|
||||
currentUrl,
|
||||
isEnabled: !disabledUrls.includes(currentUrl),
|
||||
isGloballyEnabled
|
||||
isGloballyEnabled,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -106,6 +108,20 @@ const Settings: React.FC = () => {
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set theme preference.
|
||||
*/
|
||||
const setThemePreference = async (newTheme: 'system' | 'light' | 'dark') : Promise<void> => {
|
||||
// Use the ThemeContext to apply the theme
|
||||
setTheme(newTheme);
|
||||
|
||||
// Update local state
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
theme: newTheme
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
@@ -128,11 +144,11 @@ const Settings: React.FC = () => {
|
||||
onClick={toggleGlobalPopup}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isGloballyEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isGloballyEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,18 +164,18 @@ const Settings: React.FC = () => {
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">Open popup on: {settings.currentUrl}</p>
|
||||
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
|
||||
{settings.isEnabled ? 'Popup is active' : 'Popup is disabled'}
|
||||
{settings.isEnabled ? 'Enabled for this site' : 'Disabled for this site'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-green-500 hover:bg-green-600 text-white'
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? 'Disable' : 'Enable'}
|
||||
{settings.isEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -175,6 +191,53 @@ const Settings: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Appearance</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">Theme</p>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="system"
|
||||
checked={theme === 'system'}
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Use default</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="light"
|
||||
checked={theme === 'light'}
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Light</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="dark"
|
||||
checked={theme === 'dark'}
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Dark</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ import EncryptionUtility from '../../../utils/EncryptionUtility';
|
||||
import SrpUtility from '../utils/SrpUtility';
|
||||
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
|
||||
import { storage } from 'wxt/storage';
|
||||
|
||||
/**
|
||||
* Unlock page
|
||||
@@ -15,6 +18,7 @@ import { useLoading } from '../context/LoadingContext';
|
||||
const Unlock: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
@@ -73,6 +77,9 @@ const Unlock: React.FC = () => {
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
} catch (err) {
|
||||
setError('Failed to unlock vault. Please check your password and try again.');
|
||||
console.error('Unlock error:', err);
|
||||
@@ -81,6 +88,13 @@ const Unlock: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout
|
||||
*/
|
||||
const handleLogout = () : void => {
|
||||
navigate('/logout', { replace: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
@@ -116,7 +130,7 @@ const Unlock: React.FC = () => {
|
||||
</Button>
|
||||
|
||||
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
|
||||
Switch accounts? <a href="/logout" className="text-primary-700 hover:underline dark:text-primary-500">Log out</a>
|
||||
Switch accounts? <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">Log out</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import srp from 'secure-remote-password/client'
|
||||
import { WebApiService } from '../../../utils/WebApiService';
|
||||
import { LoginRequest, LoginResponse } from '../../../utils/types/webapi/Login';
|
||||
import { ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse } from '../../../utils/types/webapi/ValidateLogin';
|
||||
import BadRequestResponse from '@/utils/types/webapi/BadRequestResponse';
|
||||
import { ApiAuthError } from '../../../utils/types/errors/ApiAuthError';
|
||||
|
||||
/**
|
||||
* Utility class for SRP authentication operations.
|
||||
@@ -22,9 +24,27 @@ class SrpUtility {
|
||||
* Initiate login with server.
|
||||
*/
|
||||
public async initiateLogin(username: string): Promise<LoginResponse> {
|
||||
return this.webApiService.post<LoginRequest, LoginResponse>('Auth/login', {
|
||||
username: username.toLowerCase().trim()
|
||||
const model: LoginRequest = {
|
||||
username: username.toLowerCase().trim(),
|
||||
};
|
||||
|
||||
const response = await this.webApiService.rawFetch('Auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(model),
|
||||
});
|
||||
|
||||
// Check if response is a bad request (400)
|
||||
if (response.status === 400) {
|
||||
const badRequestResponse = await response.json() as BadRequestResponse;
|
||||
throw new ApiAuthError(badRequestResponse.title);
|
||||
}
|
||||
|
||||
// For other responses, try to parse as LoginResponse
|
||||
const loginResponse = await response.json() as LoginResponse;
|
||||
return loginResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,12 +71,30 @@ class SrpUtility {
|
||||
privateKey
|
||||
);
|
||||
|
||||
return this.webApiService.post<ValidateLoginRequest, ValidateLoginResponse>('Auth/validate', {
|
||||
const model: ValidateLoginRequest = {
|
||||
username: username.toLowerCase().trim(),
|
||||
rememberMe: rememberMe,
|
||||
clientPublicEphemeral: clientEphemeral.public,
|
||||
clientSessionProof: sessionProof.proof,
|
||||
};
|
||||
|
||||
const response = await this.webApiService.rawFetch('Auth/validate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(model),
|
||||
});
|
||||
|
||||
// Check if response is a bad request (400)
|
||||
if (response.status === 400) {
|
||||
const badRequestResponse = await response.json() as BadRequestResponse;
|
||||
throw new ApiAuthError(badRequestResponse.title);
|
||||
}
|
||||
|
||||
// For other responses, try to parse as ValidateLoginResponse
|
||||
const validateLoginResponse = await response.json() as ValidateLoginResponse;
|
||||
return validateLoginResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,14 +121,31 @@ class SrpUtility {
|
||||
username,
|
||||
privateKey
|
||||
);
|
||||
|
||||
return this.webApiService.post<ValidateLoginRequest2Fa, ValidateLoginResponse>('Auth/validate-2fa', {
|
||||
const model: ValidateLoginRequest2Fa = {
|
||||
username: username.toLowerCase().trim(),
|
||||
rememberMe: rememberMe,
|
||||
rememberMe,
|
||||
clientPublicEphemeral: clientEphemeral.public,
|
||||
clientSessionProof: sessionProof.proof,
|
||||
code2Fa: code2Fa,
|
||||
code2Fa,
|
||||
};
|
||||
|
||||
const response = await this.webApiService.rawFetch('Auth/validate-2fa', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(model),
|
||||
});
|
||||
|
||||
// Check if response is a bad request (400)
|
||||
if (response.status === 400) {
|
||||
const badRequestResponse = await response.json() as BadRequestResponse;
|
||||
throw new ApiAuthError(badRequestResponse.title);
|
||||
}
|
||||
|
||||
// For other responses, try to parse as ValidateLoginResponse
|
||||
const validateLoginResponse = await response.json() as ValidateLoginResponse;
|
||||
return validateLoginResponse;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.16.0';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
20
browser-extension/src/utils/ExpandedMode.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Setup the expanded mode.
|
||||
*/
|
||||
export function setupExpandedMode() : void {
|
||||
/**
|
||||
* This runs once when imported and checks if the popup was opened in expanded mode with unlimited width.
|
||||
* If not, it sets the width to 350px to force the default popup to a fixed width.
|
||||
* This is used to ensure the popup is always a fixed width, even if some content like email preview
|
||||
* is too wide to fit in the default width. Some browsers like Firefox and Safari will then try to
|
||||
* expand the popup to the width of the content, which can cause the popup to become too wide and bad UX.
|
||||
*
|
||||
* You can test this by opening the popup and then clicking on the email preview. If the popup width does
|
||||
* not change, it works. Then if you expand/popout the extension, the content of the page should adjust
|
||||
* to the new width of the resizable popup.
|
||||
*/
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (!urlParams.get('expanded')) {
|
||||
document.documentElement.classList.add('max-w-[350px]');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* Placeholder base64 image for credentials without a logo.
|
||||
*/
|
||||
const placeholderBase64 = 'UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp5IraLASCWUA0eb/0s56RrLtCnYfLPiBshdXWMx8j1Ez65f169iA4xUDBTEV6ylMQeCIj2b7RngGi7gKZ9WjKdSoy9R8JcgOmjCMlDmLG20KhNo/i/Dc/Ah5GAvGfm8kfniV3AkR6fxN6eKwjDc6xrDgSfS48G5uGV6WzQt24YAVlLSK9BMwndzfHnePK1KFchFrL7O3ulB8cGNCeomu4o+l0SrS/JKblJ4WTzj0DAD++lCUEouSfgRKdiV2TiYCD+H+l3tANKSPQFPQuzi7rbvxqGeRmXB9kDwURaoSTTpYjA9REMUi9uA6aV7PWtBNXgUzMLowYMZeos6Xvyhb34GmufswMHA5ZyYpxzjTphOak4ZjNOiz8aScO5ygiTx99SqwX/uL+HSeVOSraHw8IymrMwm+jLxqN8BS8dGcItLlm/ioulqH2j4V8glDgSut+ExkxiD7m8TGPrrjCQNJbRDzpOFsyCyfBZupvp8QjGKW2KGziSZeIWes4aTB9tRmeEBhnUrmTDZQuXcc67Fg82KHrSfaeeOEq6jjuUjQ8wUnzM4Zz3dhrwSyslVz/WvnKqYkr4V/TTXPFF5EjF4rM1bHZ8bK63EfTnK41+n3n4gEFoYP4mXkNH0hntnYcdTqiE7Gn+q0BpRRxnkpBSZlA6Wa70jpW0FGqkw5e591A5/H+OV+60WAo+4Mi+NlsKrvLZ9EiVaPnoEFZlJQx1fA777AJ2MjXJ4KSsrWDWJi1lE8yPs8V6XvcC0chDTYt8456sKXAagCZyY+fzQriFMaddXyKQdG8qBqcdYjAsiIcjzaRFBBoOK9sU+sFY7N6B6+xtrlu3c37rQKkI3O2EoiJOris54EjJ5OFuumA0M6riNUuBf/MEPFBVx1JRcUEs+upEBsCnwYski7FT3TTqHrx7v5AjgFN97xhPTkmVpu6sxRnWBi1fxIRp8eWZeFM6mUcGgVk1WeVb1yhdV9hoMo2TsNEPE0tHo/wvuSJSzbZo7wibeXM9v/rRfKcx7X93rfiXVnyQ9f/5CaAQ4lxedPp/6uzLtOS4FyL0bCNeZ6L5w+AiuyWCTDFIYaUzhwfG+/YTQpWyeZCdQIKzhV+3GeXI2cxoP0ER/DlOKymf1gm+zRU3sqf1lBVQ0y+mK/Awl9bS3uaaQmI0FUyUwHUKP7PKuXnO+LcwDv4OfPT6hph8smc1EtMe5ib/apar/qZ9dyaEaElALJ1KKxnHziuvVl8atk1fINSQh7OtXDyqbPw9o/nGIpTnv5iFmwmWJLis2oyEgPkJqyx0vYI8rjkVEzKc8eQavAJBYSpjMwM193Swt+yJyjvaGYWPnqExxKiNarpB2WSO7soCAZXhS1uEYHryrK47BH6W1dRiruqT0xpLih3MXiwU3VDwAAAA==';
|
||||
|
||||
/**
|
||||
* Client for interacting with the SQLite database.
|
||||
@@ -162,7 +169,6 @@ class SqliteClient {
|
||||
Id: row.Id,
|
||||
Username: row.Username,
|
||||
Password: row.Password,
|
||||
Email: row.Email,
|
||||
ServiceName: row.ServiceName,
|
||||
ServiceUrl: row.ServiceUrl,
|
||||
Logo: row.Logo,
|
||||
@@ -213,7 +219,6 @@ class SqliteClient {
|
||||
Id: row.Id,
|
||||
Username: row.Username,
|
||||
Password: row.Password,
|
||||
Email: row.Email,
|
||||
ServiceName: row.ServiceName,
|
||||
ServiceUrl: row.ServiceUrl,
|
||||
Logo: row.Logo,
|
||||
@@ -261,15 +266,15 @@ class SqliteClient {
|
||||
|
||||
/**
|
||||
* Get setting from database for a given key.
|
||||
* Returns empty string if setting is not found.
|
||||
* Returns default value (empty string by default) if setting is not found.
|
||||
*/
|
||||
public getSetting(key: string): string {
|
||||
public getSetting(key: string, defaultValue: string = ''): string {
|
||||
const results = this.executeQuery<{ Value: string }>(`SELECT
|
||||
s.Value
|
||||
FROM Settings s
|
||||
WHERE s.Key = ?`, [key]);
|
||||
|
||||
return results.length > 0 ? results[0].Value : '';
|
||||
return results.length > 0 ? results[0].Value : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +284,40 @@ class SqliteClient {
|
||||
return this.getSetting('DefaultEmailDomain');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity language from the database.
|
||||
*/
|
||||
public getDefaultIdentityLanguage(): string {
|
||||
return this.getSetting('DefaultIdentityLanguage', 'en');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -351,7 +390,7 @@ class SqliteClient {
|
||||
const credentialId = crypto.randomUUID().toUpperCase();
|
||||
this.executeUpdate(credentialQuery, [
|
||||
credentialId,
|
||||
credential.Username,
|
||||
credential.Username ?? null,
|
||||
credential.Notes ?? null,
|
||||
serviceId,
|
||||
aliasId,
|
||||
@@ -421,6 +460,160 @@ 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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary data to a base64 encoded image source.
|
||||
*/
|
||||
public static imgSrcFromBytes(bytes: Uint8Array<ArrayBufferLike> | number[] | undefined): string {
|
||||
// Handle base64 image data
|
||||
if (bytes) {
|
||||
try {
|
||||
const logoBytes = this.toUint8Array(bytes);
|
||||
const base64Logo = this.base64Encode(logoBytes);
|
||||
// Detect image type from first few bytes
|
||||
const mimeType = this.detectMimeType(logoBytes);
|
||||
return `data:${mimeType};base64,${base64Logo}`;
|
||||
} catch (error) {
|
||||
console.error('Error setting logo:', error);
|
||||
return `data:image/x-icon;base64,${placeholderBase64}`;
|
||||
}
|
||||
} else {
|
||||
return `data:image/x-icon;base64,${placeholderBase64}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect MIME type from file signature (magic numbers)
|
||||
*/
|
||||
private static detectMimeType(bytes: Uint8Array): string {
|
||||
/**
|
||||
* Check if the file is an SVG file.
|
||||
*/
|
||||
const isSvg = () : boolean => {
|
||||
const header = new TextDecoder().decode(bytes.slice(0, 5)).toLowerCase();
|
||||
return header.includes('<?xml') || header.includes('<svg');
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the file is an ICO file.
|
||||
*/
|
||||
const isIco = () : boolean => {
|
||||
return bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the file is an PNG file.
|
||||
*/
|
||||
const isPng = () : boolean => {
|
||||
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47;
|
||||
};
|
||||
|
||||
if (isSvg()) {
|
||||
return 'image/svg+xml';
|
||||
}
|
||||
if (isIco()) {
|
||||
return 'image/x-icon';
|
||||
}
|
||||
if (isPng()) {
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
return 'image/x-icon';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert various binary data formats to Uint8Array
|
||||
*/
|
||||
private static toUint8Array(buffer: Uint8Array | number[] | {[key: number]: number}): Uint8Array {
|
||||
if (buffer instanceof Uint8Array) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
if (Array.isArray(buffer)) {
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
const length = Object.keys(buffer).length;
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = buffer[i];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encode binary data.
|
||||
*/
|
||||
private static base64Encode(buffer: Uint8Array | number[] | {[key: number]: number}): string | null {
|
||||
try {
|
||||
const arr = Array.from(this.toUint8Array(buffer));
|
||||
return btoa(arr.reduce((data, byte) => data + String.fromCharCode(byte), ''));
|
||||
} catch (error) {
|
||||
console.error('Error encoding to base64:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -37,15 +37,13 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data from the API.
|
||||
* Fetch data from the API with authentication headers and access token refresh retry.
|
||||
*/
|
||||
public async fetch<T>(
|
||||
public async authFetch<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
parseJson: boolean = true
|
||||
): Promise<T> {
|
||||
const baseUrl = await this.getBaseUrl();
|
||||
const url = baseUrl + endpoint;
|
||||
const headers = new Headers(options.headers ?? {});
|
||||
|
||||
// Add authorization header if we have an access token
|
||||
@@ -54,22 +52,19 @@ export class WebApiService {
|
||||
headers.set('Authorization', `Bearer ${accessToken}`);
|
||||
}
|
||||
|
||||
// Add client version header
|
||||
headers.set('X-AliasVault-Client', `${AppInfo.CLIENT_NAME}-${AppInfo.VERSION}`);
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
...options,
|
||||
headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, requestOptions);
|
||||
const response = await this.rawFetch(endpoint, requestOptions);
|
||||
|
||||
if (response.status === 401) {
|
||||
const newToken = await this.refreshAccessToken();
|
||||
if (newToken) {
|
||||
headers.set('Authorization', `Bearer ${newToken}`);
|
||||
const retryResponse = await fetch(url, {
|
||||
const retryResponse = await this.rawFetch(endpoint, {
|
||||
...requestOptions,
|
||||
headers,
|
||||
});
|
||||
@@ -96,6 +91,34 @@ export class WebApiService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data from the API without authentication headers and without access token refresh retry.
|
||||
*/
|
||||
public async rawFetch(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<Response> {
|
||||
const baseUrl = await this.getBaseUrl();
|
||||
const url = baseUrl + endpoint;
|
||||
const headers = new Headers(options.headers ?? {});
|
||||
|
||||
// Add client version header
|
||||
headers.set('X-AliasVault-Client', `${AppInfo.CLIENT_NAME}-${AppInfo.VERSION}`);
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
...options,
|
||||
headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, requestOptions);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('API request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the access token.
|
||||
*/
|
||||
@@ -106,14 +129,11 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = await this.getBaseUrl();
|
||||
|
||||
const response = await fetch(`${baseUrl}Auth/refresh`, {
|
||||
const response = await this.rawFetch('Auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Ignore-Failure': 'true',
|
||||
'X-AliasVault-Client': `${AppInfo.CLIENT_NAME}-${AppInfo.VERSION}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: await this.getAccessToken(),
|
||||
@@ -138,7 +158,7 @@ export class WebApiService {
|
||||
* Issue GET request to the API.
|
||||
*/
|
||||
public async get<T>(endpoint: string): Promise<T> {
|
||||
return this.fetch<T>(endpoint, { method: 'GET' });
|
||||
return this.authFetch<T>(endpoint, { method: 'GET' });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,7 +166,7 @@ export class WebApiService {
|
||||
*/
|
||||
public async downloadBlobAndConvertToBase64(endpoint: string): Promise<string> {
|
||||
try {
|
||||
const response = await this.fetch<Response>(endpoint, {
|
||||
const response = await this.authFetch<Response>(endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/octet-stream',
|
||||
@@ -170,7 +190,7 @@ export class WebApiService {
|
||||
data: TRequest,
|
||||
parseJson: boolean = true
|
||||
): Promise<TResponse> {
|
||||
return this.fetch<TResponse>(endpoint, {
|
||||
return this.authFetch<TResponse>(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -183,7 +203,7 @@ export class WebApiService {
|
||||
* Issue PUT request to the API.
|
||||
*/
|
||||
public async put<TRequest, TResponse>(endpoint: string, data: TRequest): Promise<TResponse> {
|
||||
return this.fetch<TResponse>(endpoint, {
|
||||
return this.authFetch<TResponse>(endpoint, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -196,7 +216,7 @@ export class WebApiService {
|
||||
* Issue DELETE request to the API.
|
||||
*/
|
||||
public async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.fetch<T>(endpoint, { method: 'DELETE' }, false);
|
||||
return this.authFetch<T>(endpoint, { method: 'DELETE' }, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,7 @@ export const EnglishFieldPatterns: FieldPatterns = {
|
||||
firstName: ['firstname', 'first-name', 'first_name', 'fname', 'name', 'given-name'],
|
||||
lastName: ['lastname', 'last-name', 'last_name', 'lname', 'surname', 'family-name'],
|
||||
email: ['email', 'mail', 'emailaddress'],
|
||||
emailConfirm: ['confirm', 'verification', 'repeat', 'retype', 'verify'],
|
||||
emailConfirm: ['confirm', 'verification', 'repeat', 'retype', 'verify', 'email2'],
|
||||
password: ['password', 'pwd', 'pass'],
|
||||
birthdate: ['birthdate', 'birth-date', 'dob', 'date-of-birth'],
|
||||
gender: ['gender', 'sex'],
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CombinedFieldPatterns, CombinedGenderOptionPatterns } from "./FieldPatt
|
||||
export class FormDetector {
|
||||
private readonly document: Document;
|
||||
private readonly clickedElement: HTMLElement | null;
|
||||
private readonly visibilityCache: Map<HTMLElement, boolean>;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -14,30 +15,106 @@ export class FormDetector {
|
||||
public constructor(document: Document, clickedElement?: HTMLElement) {
|
||||
this.document = document;
|
||||
this.clickedElement = clickedElement ?? null;
|
||||
this.visibilityCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element and all its parents are visible.
|
||||
* This checks for display:none, visibility:hidden, and opacity:0
|
||||
* Uses a cache to avoid redundant checks of the same elements.
|
||||
*/
|
||||
private isElementVisible(element: HTMLElement | null): boolean {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (this.visibilityCache.has(element)) {
|
||||
return this.visibilityCache.get(element)!;
|
||||
}
|
||||
|
||||
let current: HTMLElement | null = element;
|
||||
while (current) {
|
||||
try {
|
||||
const style = this.document.defaultView?.getComputedStyle(current);
|
||||
if (!style) {
|
||||
// Cache and return true for this element and all its parents
|
||||
let parent: HTMLElement | null = current;
|
||||
while (parent) {
|
||||
this.visibilityCache.set(parent, true);
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for display:none
|
||||
if (style.display === 'none') {
|
||||
// Cache and return false for this element and all its parents
|
||||
let parent: HTMLElement | null = current;
|
||||
while (parent) {
|
||||
this.visibilityCache.set(parent, false);
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for visibility:hidden
|
||||
if (style.visibility === 'hidden') {
|
||||
// Cache and return false for this element and all its parents
|
||||
let parent: HTMLElement | null = current;
|
||||
while (parent) {
|
||||
this.visibilityCache.set(parent, false);
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for opacity:0
|
||||
if (parseFloat(style.opacity) === 0) {
|
||||
// Cache and return false for this element and all its parents
|
||||
let parent: HTMLElement | null = current;
|
||||
while (parent) {
|
||||
this.visibilityCache.set(parent, false);
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
// If we can't get computed style, cache and return true for this element and all its parents
|
||||
let parent: HTMLElement | null = current;
|
||||
while (parent) {
|
||||
this.visibilityCache.set(parent, true);
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
// Cache and return true for the original element
|
||||
this.visibilityCache.set(element, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*
|
||||
* @param force - Force the detection of forms, skipping checks such as if the element contains autocomplete="off".
|
||||
*/
|
||||
public containsLoginForm(force: boolean = false): boolean {
|
||||
if (this.clickedElement) {
|
||||
const formWrapper = this.clickedElement.closest('form') ?? this.document.body;
|
||||
public containsLoginForm(): boolean {
|
||||
const formWrapper = this.clickedElement?.closest('form') ?? this.document.body;
|
||||
|
||||
/**
|
||||
* Sanity check: if form contains more than 150 inputs, don't process as this is likely not a login form.
|
||||
* This is a simple way to prevent processing large forms that are not login forms and making the browser page unresponsive.
|
||||
*/
|
||||
const inputCount = formWrapper.querySelectorAll('input').length;
|
||||
if (inputCount > 200) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Sanity check: if form contains more than 150 inputs, don't process as this is likely not a login form.
|
||||
* This is a simple way to prevent processing large forms that are not login forms and making the browser page unresponsive.
|
||||
*/
|
||||
const inputCount = formWrapper.querySelectorAll('input').length;
|
||||
if (inputCount > 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the wrapper contains a password or likely username field before processing.
|
||||
if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper, force)) {
|
||||
return true;
|
||||
}
|
||||
// Check if the wrapper contains a password or likely username field before processing.
|
||||
if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -45,8 +122,6 @@ export class FormDetector {
|
||||
|
||||
/**
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*
|
||||
* @param force - Force the detection of forms, skipping checks such as if the element contains autocomplete="off".
|
||||
*/
|
||||
public getForm(): FormFields | null {
|
||||
if (!this.clickedElement) {
|
||||
@@ -80,12 +155,22 @@ export class FormDetector {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if element is not visible
|
||||
if (!this.isElementVisible(input)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle both input and select elements
|
||||
const type = input.tagName.toLowerCase() === 'select' ? 'select' : input.type.toLowerCase();
|
||||
if (!types.includes(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for exact type match if types contains email, as that most likely is the email field.
|
||||
if (types.includes('email') && input.type.toLowerCase() === 'email') {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Collect all text attributes to check
|
||||
const attributes = [
|
||||
input.id,
|
||||
@@ -103,11 +188,16 @@ export class FormDetector {
|
||||
|
||||
// Check for parent label and table cell structure
|
||||
let currentElement = input;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Check for parent label
|
||||
const parentLabel = currentElement.closest('label');
|
||||
if (parentLabel) {
|
||||
attributes.push(parentLabel.textContent?.toLowerCase() ?? '');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// Stop if we have too many child elements (near body)
|
||||
if (currentElement.children.length > 15) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for label - search both parent and child elements
|
||||
const childLabel = currentElement.querySelector('label');
|
||||
if (childLabel) {
|
||||
attributes.push(childLabel.textContent?.toLowerCase() ?? '');
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -165,12 +255,16 @@ export class FormDetector {
|
||||
['text', 'email']
|
||||
);
|
||||
|
||||
// Find confirmation email field if primary exists
|
||||
/*
|
||||
* Find confirmation email field if primary exists
|
||||
* and ensure it's not the same as the primary email field.
|
||||
*/
|
||||
const confirmEmail = primaryEmail
|
||||
? this.findInputField(
|
||||
form,
|
||||
CombinedFieldPatterns.emailConfirm,
|
||||
['text', 'email']
|
||||
['text', 'email'],
|
||||
[primaryEmail]
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -336,11 +430,11 @@ export class FormDetector {
|
||||
? form.querySelectorAll<HTMLInputElement>('input[type="password"]')
|
||||
: this.document.querySelectorAll<HTMLInputElement>('input[type="password"]');
|
||||
|
||||
const candidateArray = Array.from(candidates);
|
||||
const visibleCandidates = Array.from(candidates).filter(input => this.isElementVisible(input));
|
||||
|
||||
return {
|
||||
primary: candidateArray[0] ?? null,
|
||||
confirm: candidateArray[1] ?? null
|
||||
primary: visibleCandidates[0] ?? null,
|
||||
confirm: visibleCandidates[1] ?? null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -349,7 +443,7 @@ export class FormDetector {
|
||||
*/
|
||||
private containsPasswordField(wrapper: HTMLElement): boolean {
|
||||
const passwordFields = this.findPasswordField(wrapper as HTMLFormElement | null);
|
||||
if (passwordFields.primary) {
|
||||
if (passwordFields.primary && this.isElementVisible(passwordFields.primary)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -359,41 +453,29 @@ export class FormDetector {
|
||||
/**
|
||||
* Check if a form contains a likely username or email field.
|
||||
*/
|
||||
private containsLikelyUsernameOrEmailField(wrapper: HTMLElement, force: boolean = false): boolean {
|
||||
private containsLikelyUsernameOrEmailField(wrapper: HTMLElement): boolean {
|
||||
// Check if the form contains an email field.
|
||||
const emailFields = this.findEmailField(wrapper as HTMLFormElement | null);
|
||||
if (emailFields.primary) {
|
||||
const isValid = force || emailFields.primary.getAttribute('autocomplete') !== 'off';
|
||||
if (isValid) {
|
||||
return true;
|
||||
}
|
||||
if (emailFields.primary && this.isElementVisible(emailFields.primary)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the form contains a username field.
|
||||
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text'], []);
|
||||
if (usernameField) {
|
||||
const isValid = force || usernameField.getAttribute('autocomplete') !== 'off';
|
||||
if (isValid) {
|
||||
return true;
|
||||
}
|
||||
if (usernameField && this.isElementVisible(usernameField)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the form contains a first name field.
|
||||
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.firstName, ['text'], []);
|
||||
if (firstNameField) {
|
||||
const isValid = force || firstNameField.getAttribute('autocomplete') !== 'off';
|
||||
if (isValid) {
|
||||
return true;
|
||||
}
|
||||
if (firstNameField && this.isElementVisible(firstNameField)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the form contains a last name field.
|
||||
const lastNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.lastName, ['text'], []);
|
||||
if (lastNameField) {
|
||||
const isValid = force || lastNameField.getAttribute('autocomplete') !== 'off';
|
||||
if (isValid) {
|
||||
return true;
|
||||
}
|
||||
if (lastNameField && this.isElementVisible(lastNameField)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -433,16 +515,16 @@ export class FormDetector {
|
||||
detectedFields.push(fullNameField);
|
||||
}
|
||||
|
||||
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.firstName, ['text'], detectedFields);
|
||||
if (firstNameField) {
|
||||
detectedFields.push(firstNameField);
|
||||
}
|
||||
|
||||
const lastNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.lastName, ['text'], detectedFields);
|
||||
if (lastNameField) {
|
||||
detectedFields.push(lastNameField);
|
||||
}
|
||||
|
||||
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.firstName, ['text'], detectedFields);
|
||||
if (firstNameField) {
|
||||
detectedFields.push(firstNameField);
|
||||
}
|
||||
|
||||
const birthdateField = this.findBirthdateFields(wrapper as HTMLFormElement | null, detectedFields);
|
||||
if (birthdateField.single) {
|
||||
detectedFields.push(birthdateField.single);
|
||||
|
||||
@@ -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.
|
||||
@@ -29,53 +34,91 @@ export class FormFiller {
|
||||
* @param credential The credential to fill the form with.
|
||||
*/
|
||||
private fillBasicFields(credential: Credential): void {
|
||||
if (this.form.usernameField) {
|
||||
if (this.form.usernameField && credential.Username) {
|
||||
this.form.usernameField.value = credential.Username;
|
||||
this.triggerInputEvents(this.form.usernameField);
|
||||
}
|
||||
|
||||
if (this.form.passwordField) {
|
||||
this.form.passwordField.value = credential.Password;
|
||||
this.triggerInputEvents(this.form.passwordField);
|
||||
if (this.form.passwordField && credential.Password) {
|
||||
this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
}
|
||||
|
||||
if (this.form.passwordConfirmField) {
|
||||
this.form.passwordConfirmField.value = credential.Password;
|
||||
this.triggerInputEvents(this.form.passwordConfirmField);
|
||||
if (this.form.passwordConfirmField && credential.Password) {
|
||||
this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
}
|
||||
|
||||
if (this.form.emailField) {
|
||||
this.form.emailField.value = credential.Email;
|
||||
this.triggerInputEvents(this.form.emailField);
|
||||
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
|
||||
if (credential.Alias?.Email) {
|
||||
this.form.emailField.value = credential.Alias.Email;
|
||||
this.triggerInputEvents(this.form.emailField);
|
||||
} else if (credential.Username && !this.form.usernameField) {
|
||||
/*
|
||||
* If current form has no username field AND the credential has a username
|
||||
* then we can assume the username should be used as the email.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This applies to the usecase where the AliasVault credential was imported
|
||||
* from a previous password manager that only had username/password fields
|
||||
* or where the user manually created a credential with only a username/password.
|
||||
*/
|
||||
this.form.emailField.value = credential.Username;
|
||||
this.triggerInputEvents(this.form.emailField);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.form.emailConfirmField) {
|
||||
this.form.emailConfirmField.value = credential.Email;
|
||||
if (this.form.emailConfirmField && credential.Alias?.Email) {
|
||||
this.form.emailConfirmField.value = credential.Alias.Email;
|
||||
this.triggerInputEvents(this.form.emailConfirmField);
|
||||
}
|
||||
|
||||
if (this.form.fullNameField) {
|
||||
if (this.form.fullNameField && credential.Alias?.FirstName && credential.Alias?.LastName) {
|
||||
this.form.fullNameField.value = `${credential.Alias.FirstName} ${credential.Alias.LastName}`;
|
||||
this.triggerInputEvents(this.form.fullNameField);
|
||||
}
|
||||
|
||||
if (this.form.firstNameField) {
|
||||
if (this.form.firstNameField && credential.Alias?.FirstName) {
|
||||
this.form.firstNameField.value = credential.Alias.FirstName;
|
||||
this.triggerInputEvents(this.form.firstNameField);
|
||||
}
|
||||
|
||||
if (this.form.lastNameField) {
|
||||
if (this.form.lastNameField && credential.Alias?.LastName) {
|
||||
this.form.lastNameField.value = credential.Alias.LastName;
|
||||
this.triggerInputEvents(this.form.lastNameField);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, true);
|
||||
|
||||
// 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
|
||||
this.triggerInputEvents(field, false);
|
||||
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.
|
||||
*/
|
||||
private fillBirthdateFields(credential: Credential): void {
|
||||
if (!credential.Alias.BirthDate) {
|
||||
// TODO: when birth date is made optional in datamodel, we can remove this mindate check here.
|
||||
if (!credential.Alias.BirthDate || credential.Alias.BirthDate === '0001-01-01 00:00:00') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,12 @@ describe('FormDetector English tests', () => {
|
||||
describe('English email form 1 detection', () => {
|
||||
const htmlFile = 'en-email-form1.html';
|
||||
|
||||
// Assert that this test fails, because the autocomplete=off for the specified element.
|
||||
testField(FormField.Email, 'P0-0', htmlFile);
|
||||
});
|
||||
|
||||
describe('English login form 1 detection', () => {
|
||||
const htmlFile = 'en-login-form1.html';
|
||||
|
||||
testField(FormField.Email, 'resolving_input', htmlFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,10 +30,46 @@ describe('FormDetector generic tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form with autocomplete="off" not detected', () => {
|
||||
describe('Form with autocomplete="off" still detected', () => {
|
||||
const htmlFile = 'autocomplete-off.html';
|
||||
|
||||
it('should not detect form with autocomplete="off" on email field', () => {
|
||||
it('should still detect form with autocomplete="off" on email field', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
const formDetector = new FormDetector(document);
|
||||
const form = formDetector.containsLoginForm();
|
||||
expect(form).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form with display:none not detected', () => {
|
||||
const htmlFile = 'display-none.html';
|
||||
|
||||
it('should not detect form with display:none', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
const formDetector = new FormDetector(document);
|
||||
const form = formDetector.containsLoginForm();
|
||||
expect(form).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form with visibility:hidden not detected', () => {
|
||||
const htmlFile = 'visibility-hidden.html';
|
||||
|
||||
it('should not detect form with visibility:hidden', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
const formDetector = new FormDetector(document);
|
||||
const form = formDetector.containsLoginForm();
|
||||
expect(form).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Form with opacity:0 not detected', () => {
|
||||
const htmlFile = 'opacity-zero.html';
|
||||
|
||||
it('should not detect form with opacity:0', () => {
|
||||
const dom = createTestDom(htmlFile);
|
||||
const document = dom.window.document;
|
||||
const formDetector = new FormDetector(document);
|
||||
|
||||
@@ -105,4 +105,14 @@ describe('FormDetector Dutch tests', () => {
|
||||
testField(FormField.Password, 'user_password', htmlFile);
|
||||
testField(FormField.PasswordConfirm, 'user_password_confirmation', htmlFile);
|
||||
});
|
||||
|
||||
describe('Dutch registration form 10 detection', () => {
|
||||
const htmlFile = 'nl-registration-form10.html';
|
||||
|
||||
testField(FormField.Email, 'tbxEmail1', htmlFile);
|
||||
testField(FormField.EmailConfirm, 'tbxEmail2', htmlFile);
|
||||
testField(FormField.FirstName, 'Field645', htmlFile);
|
||||
testField(FormField.LastName, 'Field642', htmlFile);
|
||||
testField(FormField.BirthDate, 'Field675', htmlFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,11 +44,25 @@ describe('FormFiller', () => {
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.emailConfirmField)).toBe(true);
|
||||
});
|
||||
|
||||
it('should fill password and confirmation fields', () => {
|
||||
it('should use username as email when no email is provided and no username field exists', () => {
|
||||
// Create a credential with an empty email string
|
||||
const credentialWithoutEmail = { ...mockCredential, Alias: { ...mockCredential.Alias, Email: '' } };
|
||||
formFields.usernameField = null;
|
||||
|
||||
formFiller.fillFields(credentialWithoutEmail);
|
||||
|
||||
expect(formFields.emailField?.value).toBe('testuser');
|
||||
expect(wasTriggerCalledFor(mockTriggerInputEvents, formFields.emailField)).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -55,27 +55,27 @@ export const testField = (fieldName: FormField, elementId: string, htmlFile: str
|
||||
|
||||
// Handle birthdate fields differently
|
||||
if (fieldName === FormField.BirthDate) {
|
||||
expect(result.birthdateField.single).toBe(expectedElement);
|
||||
expect(result?.birthdateField.single).toBe(expectedElement);
|
||||
} else if (fieldName === FormField.BirthDay) {
|
||||
expect(result.birthdateField.day).toBe(expectedElement);
|
||||
expect(result?.birthdateField.day).toBe(expectedElement);
|
||||
} else if (fieldName === FormField.BirthMonth) {
|
||||
expect(result.birthdateField.month).toBe(expectedElement);
|
||||
expect(result?.birthdateField.month).toBe(expectedElement);
|
||||
} else if (fieldName === FormField.BirthYear) {
|
||||
expect(result.birthdateField.year).toBe(expectedElement);
|
||||
expect(result?.birthdateField.year).toBe(expectedElement);
|
||||
// Handle gender field differently
|
||||
} else if (fieldName === FormField.Gender) {
|
||||
expect(result.genderField.field).toBe(expectedElement);
|
||||
expect(result?.genderField.field).toBe(expectedElement);
|
||||
} else if (fieldName === FormField.GenderMale) {
|
||||
expect(result.genderField.radioButtons?.male).toBe(expectedElement);
|
||||
expect(result?.genderField.radioButtons?.male).toBe(expectedElement);
|
||||
} else if (fieldName === FormField.GenderFemale) {
|
||||
expect(result.genderField.radioButtons?.female).toBe(expectedElement);
|
||||
expect(result?.genderField.radioButtons?.female).toBe(expectedElement);
|
||||
} else if (fieldName === FormField.GenderOther) {
|
||||
expect(result.genderField.radioButtons?.other).toBe(expectedElement);
|
||||
expect(result?.genderField.radioButtons?.other).toBe(expectedElement);
|
||||
// Handle default fields
|
||||
} else {
|
||||
const fieldKey = `${fieldName}Field` as keyof typeof result;
|
||||
expect(result[fieldKey]).toBeDefined();
|
||||
expect(result[fieldKey]).toBe(expectedElement);
|
||||
expect(result?.[fieldKey]).toBeDefined();
|
||||
expect(result?.[fieldKey]).toBe(expectedElement);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -86,7 +86,7 @@ export const testField = (fieldName: FormField, elementId: string, htmlFile: str
|
||||
export const testBirthdateFormat = (expectedFormat: string, htmlFile: string, focusedElementId: string) : void => {
|
||||
it('should detect correct birthdate format', () => {
|
||||
const { result } = setupFormTest(htmlFile, focusedElementId);
|
||||
expect(result.birthdateField.format).toBe(expectedFormat);
|
||||
expect(result?.birthdateField.format).toBe(expectedFormat);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -179,13 +179,13 @@ export const createMockCredential = (): Credential => ({
|
||||
Id: '123',
|
||||
Username: 'testuser',
|
||||
Password: 'testpass',
|
||||
Email: 'test@example.com',
|
||||
ServiceName: 'Test Service',
|
||||
Alias: {
|
||||
FirstName: 'John',
|
||||
LastName: 'Doe',
|
||||
BirthDate: '1991-02-03',
|
||||
Gender: Gender.Male
|
||||
Gender: Gender.Male,
|
||||
Email: 'test@example.com',
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Display None Test Form</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: none;">
|
||||
<form id="hidden-login-form" action="/login" method="post">
|
||||
<div>
|
||||
<label for="hidden-username">Username:</label>
|
||||
<input type="text" id="hidden-username" name="username" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="hidden-password">Password:</label>
|
||||
<input type="password" id="hidden-password" name="password" />
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Opacity Zero Test Form</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="opacity: 0;">
|
||||
<form id="hidden-login-form" action="/login" method="post">
|
||||
<div>
|
||||
<label for="hidden-username">Username:</label>
|
||||
<input type="text" id="hidden-username" name="username" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="hidden-password">Password:</label>
|
||||
<input type="password" id="hidden-password" name="password" />
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Visibility Hidden Test Form</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="visibility: hidden;">
|
||||
<form id="hidden-login-form" action="/login" method="post">
|
||||
<div>
|
||||
<label for="hidden-username">Username:</label>
|
||||
<input type="text" id="hidden-username" name="username" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="hidden-password">Password:</label>
|
||||
<input type="password" id="hidden-password" name="password" />
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,15 @@ import { Gender } from "../generators/Identity/types/Gender";
|
||||
*/
|
||||
export type Credential = {
|
||||
Id: string;
|
||||
Username: string;
|
||||
Username?: string;
|
||||
Password: string;
|
||||
Email: string;
|
||||
ServiceName: string;
|
||||
ServiceUrl?: string;
|
||||
Logo?: Uint8Array | number[];
|
||||
Notes?: string;
|
||||
Alias: {
|
||||
FirstName: string;
|
||||
LastName: string;
|
||||
FirstName?: string;
|
||||
LastName?: string;
|
||||
NickName?: string;
|
||||
BirthDate: string;
|
||||
Gender?: Gender;
|
||||
|
||||
34
browser-extension/src/utils/types/PasswordSettings.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Settings for password generation stored in SQLite database settings table as string.
|
||||
*/
|
||||
export type PasswordSettings = {
|
||||
/**
|
||||
* The length of the password.
|
||||
*/
|
||||
Length: number;
|
||||
|
||||
/**
|
||||
* Whether to use lowercase letters.
|
||||
*/
|
||||
UseLowercase: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use uppercase letters.
|
||||
*/
|
||||
UseUppercase: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use numbers.
|
||||
*/
|
||||
UseNumbers: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use special characters.
|
||||
*/
|
||||
UseSpecialChars: boolean;
|
||||
|
||||
/**
|
||||
* Whether to use non-ambiguous characters.
|
||||
*/
|
||||
UseNonAmbiguousChars: boolean;
|
||||
}
|
||||
16
browser-extension/src/utils/types/TotpCode.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* TotpCode SQLite database type.
|
||||
*/
|
||||
export type TotpCode = {
|
||||
/** The ID of the TOTP code */
|
||||
Id: string;
|
||||
|
||||
/** The name of the TOTP code */
|
||||
Name: string;
|
||||
|
||||
/** The secret key for the TOTP code */
|
||||
SecretKey: string;
|
||||
|
||||
/** The credential ID this TOTP code belongs to */
|
||||
CredentialId: string;
|
||||
};
|
||||
14
browser-extension/src/utils/types/errors/ApiAuthError.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Custom error class for API authentication-related errors.
|
||||
*/
|
||||
export class ApiAuthError extends Error {
|
||||
/**
|
||||
* Creates a new instance of ApiAuthError.
|
||||
*
|
||||
* @param message - The error message.
|
||||
*/
|
||||
public constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ApiAuthError';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export type DefaultEmailDomainResponse = {
|
||||
success: boolean,
|
||||
error?: string,
|
||||
domain?: string
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PasswordSettings } from "@/utils/types/PasswordSettings";
|
||||
|
||||
export type PasswordSettingsResponse = {
|
||||
success: boolean,
|
||||
error?: string,
|
||||
settings?: PasswordSettings
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export type StringResponse = {
|
||||
success: boolean,
|
||||
error?: string,
|
||||
value?: string
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
type BadRequestResponse = {
|
||||
type: string;
|
||||
title: string;
|
||||
status: number;
|
||||
errors: Record<string, string[]>;
|
||||
traceId: string;
|
||||
};
|
||||
|
||||
export default BadRequestResponse;
|
||||
@@ -1,10 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/entrypoints/*.{js,jsx,ts,tsx}",
|
||||
"./src/entrypoints/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/entrypoints/**/*/*.{html,js}"
|
||||
"./src/**/*.{js,jsx,ts,tsx,html}"
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
manifest: {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.13.0",
|
||||
version: "0.16.0",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||