mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
250 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83d9eadeea | ||
|
|
1cdd8f456e | ||
|
|
395f881bd0 | ||
|
|
293ae102c5 | ||
|
|
8f5852bb86 | ||
|
|
9ccaff74cd | ||
|
|
ee6b40dd3d | ||
|
|
3ca4c0a78d | ||
|
|
b246def212 | ||
|
|
1eecb8be38 | ||
|
|
9a7fbe7d2a | ||
|
|
7776fb6d82 | ||
|
|
0eebaddf04 | ||
|
|
8b145e66b5 | ||
|
|
4e3c992c24 | ||
|
|
65944b1523 | ||
|
|
d05114fddc | ||
|
|
8e0fef4b16 | ||
|
|
1bf8b7ee04 | ||
|
|
8545b2c1fd | ||
|
|
2f22e4db56 | ||
|
|
54bbbb0647 | ||
|
|
0b127a4a3e | ||
|
|
241f17868b | ||
|
|
be536741c5 | ||
|
|
7638879aa9 | ||
|
|
499f6e451e | ||
|
|
73ad8f6acd | ||
|
|
c5ea7d0143 | ||
|
|
0473ec21bf | ||
|
|
0eb7e97383 | ||
|
|
7d35777c93 | ||
|
|
08e39ef3e9 | ||
|
|
fe10acb925 | ||
|
|
061f846b66 | ||
|
|
eb64d86c78 | ||
|
|
ef2a58f784 | ||
|
|
a43d50f047 | ||
|
|
0d5fd55133 | ||
|
|
d9942844e2 | ||
|
|
15a1276d42 | ||
|
|
37d6ead41d | ||
|
|
fa99cb77d7 | ||
|
|
f9987b5e2a | ||
|
|
ec11ab0817 | ||
|
|
ecd592e74f | ||
|
|
a3208e72bf | ||
|
|
d66dee3583 | ||
|
|
68471b7c88 | ||
|
|
3d8c2b7086 | ||
|
|
a93a7f7fff | ||
|
|
1b84fd1dad | ||
|
|
c673a20fd1 | ||
|
|
7e81e70ec4 | ||
|
|
c688764831 | ||
|
|
3da40f42c9 | ||
|
|
fd74b7b056 | ||
|
|
0ccbeb683d | ||
|
|
34d00dc7d6 | ||
|
|
ffe1a36df3 | ||
|
|
0f9c2d1f7c | ||
|
|
19499f02d6 | ||
|
|
330a92fbb3 | ||
|
|
5ca29a33d0 | ||
|
|
ab6191ac62 | ||
|
|
f8bf575ab5 | ||
|
|
3576b32821 | ||
|
|
4619fe615c | ||
|
|
e8ba964064 | ||
|
|
4af1a127cf | ||
|
|
22acea0e35 | ||
|
|
c6d7d16b27 | ||
|
|
aba377ac65 | ||
|
|
5a0d1eabb7 | ||
|
|
eb2c4c1cd3 | ||
|
|
62224c86cd | ||
|
|
6ab20501e9 | ||
|
|
dd82803f87 | ||
|
|
27d19759c8 | ||
|
|
c6faa4db97 | ||
|
|
f35d46256f | ||
|
|
4683d6bea6 | ||
|
|
566d4259bd | ||
|
|
afee07885d | ||
|
|
8e8ef8fd5d | ||
|
|
5589042606 | ||
|
|
cbe8b2c471 | ||
|
|
4c7bef2a5a | ||
|
|
bc6479bf5e | ||
|
|
845f780707 | ||
|
|
1089e8299f | ||
|
|
ce9b37d299 | ||
|
|
538675f391 | ||
|
|
260aec34ce | ||
|
|
a7ffc33d56 | ||
|
|
89a57b6047 | ||
|
|
a66e8b6b0d | ||
|
|
5de0806bcc | ||
|
|
a1d2bcbe3b | ||
|
|
fbc085439c | ||
|
|
4a35a1a7d3 | ||
|
|
bd82037d8c | ||
|
|
9615634bf9 | ||
|
|
dfd2b534e6 | ||
|
|
314c757fe6 | ||
|
|
771abe9cc1 | ||
|
|
22aaf17cd1 | ||
|
|
2134b61a78 | ||
|
|
0059e31892 | ||
|
|
2f7a4370b7 | ||
|
|
5fc2889a03 | ||
|
|
f43bc402ba | ||
|
|
2e6d4fbe20 | ||
|
|
38db3c5054 | ||
|
|
971a21a16a | ||
|
|
8058912eee | ||
|
|
8a9e1dc9a3 | ||
|
|
cde78650b9 | ||
|
|
4ef9e58665 | ||
|
|
b6b1d9dec9 | ||
|
|
fa2dedb05a | ||
|
|
f148ccdeba | ||
|
|
9b038cb76c | ||
|
|
aa726706a4 | ||
|
|
d0017d9207 | ||
|
|
cde4b87371 | ||
|
|
431d8d4fca | ||
|
|
9fddb5f450 | ||
|
|
dbb6cf5b94 | ||
|
|
bd41507ef9 | ||
|
|
ebb0e7cf68 | ||
|
|
4603051a91 | ||
|
|
f66fb53706 | ||
|
|
b603160d99 | ||
|
|
096b0277f3 | ||
|
|
f271040ff4 | ||
|
|
f313950112 | ||
|
|
ef1ad127e3 | ||
|
|
cac691a43d | ||
|
|
4efe201224 | ||
|
|
ca477c310c | ||
|
|
77189373ba | ||
|
|
1aaa5c2d55 | ||
|
|
163e5c51c2 | ||
|
|
29895f375f | ||
|
|
2803dcf02c | ||
|
|
a8e075d932 | ||
|
|
49ba704135 | ||
|
|
9669307480 | ||
|
|
343ced5b38 | ||
|
|
8f66670804 | ||
|
|
c2d1fcfcd4 | ||
|
|
e5a340b67d | ||
|
|
6a0e8909a8 | ||
|
|
5a90b4271c | ||
|
|
f0bd837d5e | ||
|
|
de45c286b1 | ||
|
|
fac0fd5f32 | ||
|
|
5a8b6b7f29 | ||
|
|
c864bfcab5 | ||
|
|
c9c692ce6e | ||
|
|
a640e4d280 | ||
|
|
2f03db7951 | ||
|
|
9e5b733c8a | ||
|
|
09c380afdd | ||
|
|
7d9cc6118e | ||
|
|
c7ab42e9f2 | ||
|
|
1b07c5de9f | ||
|
|
84df5b7d98 | ||
|
|
347721a575 | ||
|
|
463c31641d | ||
|
|
67759a814e | ||
|
|
763a859e22 | ||
|
|
d7db5a4e76 | ||
|
|
85bb5cf944 | ||
|
|
cdc59e43a9 | ||
|
|
9d0a003b2d | ||
|
|
e430ae9f4f | ||
|
|
41ba1260d7 | ||
|
|
c7572ac3f7 | ||
|
|
fe5c50b3c4 | ||
|
|
2a8ed28ff9 | ||
|
|
f6764b2f33 | ||
|
|
1afa153381 | ||
|
|
ac59273161 | ||
|
|
551fc42de1 | ||
|
|
4b844189bc | ||
|
|
5c277e747f | ||
|
|
8cbd275134 | ||
|
|
765625b163 | ||
|
|
b3df153128 | ||
|
|
604cffc622 | ||
|
|
3b114445a3 | ||
|
|
e8942c9833 | ||
|
|
b1da32ceae | ||
|
|
ef58217ed3 | ||
|
|
e0dd04263c | ||
|
|
29c52c844f | ||
|
|
b99025c48a | ||
|
|
8ba8eb684e | ||
|
|
b736edbb68 | ||
|
|
1fa0d275cc | ||
|
|
4a05cd00e3 | ||
|
|
574b5ff693 | ||
|
|
e6b7d1afa1 | ||
|
|
cbe224385d | ||
|
|
adb2f9a3d6 | ||
|
|
6790391d37 | ||
|
|
2a7855e1dc | ||
|
|
f3e47d7e67 | ||
|
|
bc76e85a9c | ||
|
|
890025cd49 | ||
|
|
1868370d8f | ||
|
|
9a4fc7fb37 | ||
|
|
199fdebd5d | ||
|
|
d5f17ef99c | ||
|
|
3b1e039d75 | ||
|
|
01cdd28e32 | ||
|
|
95a71f6ab2 | ||
|
|
41cb92befd | ||
|
|
2cfd1a922f | ||
|
|
511ec31d17 | ||
|
|
080e505991 | ||
|
|
461c1a042d | ||
|
|
f30fcf4624 | ||
|
|
522eeefda4 | ||
|
|
94656c4d14 | ||
|
|
bbba8d1393 | ||
|
|
680f5ba926 | ||
|
|
04d3f80019 | ||
|
|
a4d78cf7fc | ||
|
|
9713c8ed11 | ||
|
|
2f4dbf34ba | ||
|
|
232d110e49 | ||
|
|
0af1507686 | ||
|
|
e481769198 | ||
|
|
830c390b95 | ||
|
|
c733a60571 | ||
|
|
d164d8e785 | ||
|
|
79221f35c6 | ||
|
|
826bd23767 | ||
|
|
baf81392eb | ||
|
|
a70f6fca56 | ||
|
|
1480fd88d1 | ||
|
|
11a5e10f4b | ||
|
|
eecf61b8b2 | ||
|
|
6c620e34e6 | ||
|
|
aa99bbc111 | ||
|
|
e34b5f586c | ||
|
|
80c0992eb4 |
132
.github/actions/build-android-app/action.yml
vendored
Normal file
132
.github/actions/build-android-app/action.yml
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
name: "Build Android App"
|
||||
description: "Builds Android APK/AAB, optionally signs and uploads to GitHub Release"
|
||||
inputs:
|
||||
run_tests:
|
||||
description: "Whether to run Android unit tests"
|
||||
required: false
|
||||
default: "false"
|
||||
signed:
|
||||
description: "Whether to sign the Android build"
|
||||
required: false
|
||||
default: "false"
|
||||
upload_to_release:
|
||||
description: "Whether to upload the APK to GitHub Release"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/mobile-app/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Extract version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./app.json').expo.version")
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Build JS bundle (Expo)
|
||||
run: |
|
||||
mkdir -p build
|
||||
npx expo export \
|
||||
--dev \
|
||||
--output-dir ./build \
|
||||
--platform android
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Run Android Unit Tests
|
||||
if: ${{ inputs.run_tests == 'true' }}
|
||||
run: |
|
||||
cd android
|
||||
./gradlew :app:testDebugUnitTest --tests "net.aliasvault.app.*"
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Upload Android Test Reports
|
||||
if: ${{ inputs.run_tests == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: android-test-reports
|
||||
path: apps/mobile-app/android/app/build/reports/tests/testDebugUnitTest/
|
||||
retention-days: 7
|
||||
|
||||
- name: Decode keystore
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
run: echo "${{ env.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Configure signing
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
run: |
|
||||
cat >> android/gradle.properties <<EOF
|
||||
ALIASVAULT_UPLOAD_STORE_FILE=keystore.jks
|
||||
ALIASVAULT_UPLOAD_KEY_ALIAS=${{ env.ANDROID_KEY_ALIAS }}
|
||||
ALIASVAULT_UPLOAD_STORE_PASSWORD=${{ env.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ALIASVAULT_UPLOAD_KEY_PASSWORD=${{ env.ANDROID_KEY_PASSWORD }}
|
||||
EOF
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Build APK & AAB (Release only if signed)
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
run: |
|
||||
cd android
|
||||
./gradlew bundleRelease
|
||||
./gradlew assembleRelease
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Rename APK and AAB files
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
run: |
|
||||
mv android/app/build/outputs/apk/release/app-release.apk android/app/build/outputs/apk/release/aliasvault-${VERSION}-android.apk
|
||||
mv android/app/build/outputs/bundle/release/app-release.aab android/app/build/outputs/bundle/release/aliasvault-${VERSION}-android.aab
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Upload AAB as artifact
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-${{ env.VERSION }}-android.aab
|
||||
path: apps/mobile-app/android/app/build/outputs/bundle/release/aliasvault-${{ env.VERSION }}-android.aab
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload APK as artifact
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-${{ env.VERSION }}-android.apk
|
||||
path: apps/mobile-app/android/app/build/outputs/apk/release/aliasvault-${{ env.VERSION }}-android.apk
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload APK to release
|
||||
if: ${{ inputs.upload_to_release == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: apps/mobile-app/android/app/build/outputs/apk/release/aliasvault-${{ env.VERSION }}-android.apk
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
104
.github/actions/build-browser-extension/action.yml
vendored
Normal file
104
.github/actions/build-browser-extension/action.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: "Build Browser Extension"
|
||||
description: "Builds, tests, lints, zips, and optionally uploads a browser extension"
|
||||
inputs:
|
||||
browser:
|
||||
description: "Target browser to build for (chrome, firefox, edge)"
|
||||
required: true
|
||||
upload_to_release:
|
||||
description: "Whether to upload the resulting zip to GitHub Release"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:${{ inputs.browser }}
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Zip Extension
|
||||
run: npm run zip:${{ inputs.browser }}
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Extract version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Unzip extension
|
||||
run: |
|
||||
mkdir -p dist/${{ inputs.browser }}-unpacked
|
||||
unzip dist/aliasvault-browser-extension-${{ env.VERSION }}-${{ inputs.browser }}.zip -d dist/${{ inputs.browser }}-unpacked
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-${{ env.VERSION }}-${{ inputs.browser }}
|
||||
path: apps/browser-extension/dist/${{ inputs.browser }}-unpacked
|
||||
|
||||
- name: Unzip and upload Firefox sources
|
||||
if: ${{ inputs.browser == 'firefox' }}
|
||||
run: |
|
||||
mkdir -p dist/sources-unpacked
|
||||
unzip dist/aliasvault-browser-extension-${{ env.VERSION }}-sources.zip -d dist/sources-unpacked
|
||||
shell: bash
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
- name: Upload Firefox sources artifact
|
||||
if: ${{ inputs.browser == 'firefox' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-${{ env.VERSION }}-browser-extension-sources
|
||||
path: apps/browser-extension/dist/sources-unpacked
|
||||
|
||||
- name: Rename zip files
|
||||
run: |
|
||||
mv apps/browser-extension/dist/aliasvault-browser-extension-${{ env.VERSION }}-${{ inputs.browser }}.zip apps/browser-extension/dist/aliasvault-${{ env.VERSION }}-${{ inputs.browser }}.zip
|
||||
if [ -f apps/browser-extension/dist/aliasvault-browser-extension-${{ env.VERSION }}-sources.zip ]; then
|
||||
mv apps/browser-extension/dist/aliasvault-browser-extension-${{ env.VERSION }}-sources.zip apps/browser-extension/dist/aliasvault-${{ env.VERSION }}-browser-extension-sources.zip
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Upload to GitHub Release
|
||||
if: ${{ inputs.upload_to_release == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
apps/browser-extension/dist/aliasvault-${{ env.VERSION }}-${{ inputs.browser }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Firefox sources to Release
|
||||
if: ${{ inputs.upload_to_release == 'true' && inputs.browser == 'firefox' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: apps/browser-extension/dist/aliasvault-${{ env.VERSION }}-browser-extension-sources.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
|
||||
118
.github/actions/build-ios-app/action.yml
vendored
Normal file
118
.github/actions/build-ios-app/action.yml
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
name: "Build iOS App"
|
||||
description: "Builds iOS App, optionally signs and uploads to App Store Connect"
|
||||
inputs:
|
||||
run_tests:
|
||||
description: "Whether to run iOS unit tests"
|
||||
required: false
|
||||
default: "false"
|
||||
signed:
|
||||
description: "Whether to sign the iOS build"
|
||||
required: false
|
||||
default: "false"
|
||||
upload_to_app_store_connect:
|
||||
description: "Whether to upload the iOS App to App Store Connect"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/mobile-app/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Extract version
|
||||
run: |
|
||||
VERSION=$(node -p "require('./app.json').expo.version")
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Fastlane
|
||||
run: gem install fastlane
|
||||
shell: bash
|
||||
|
||||
- name: Install CocoaPods
|
||||
run: |
|
||||
sudo gem install cocoapods
|
||||
shell: bash
|
||||
|
||||
- name: Create ASC private key file
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
run: |
|
||||
mkdir -p $RUNNER_TEMP/asc
|
||||
echo "${{ env.ASC_PRIVATE_KEY_BASE64 }}" | base64 --decode > $RUNNER_TEMP/asc/AuthKey.p8
|
||||
shell: bash
|
||||
|
||||
- name: Install CocoaPods
|
||||
run: |
|
||||
cd ios
|
||||
pod install
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Build iOS IPA
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
env:
|
||||
IDEFileSystemSynchronizedGroupsAreEnabled: NO
|
||||
XCODE_WORKSPACE: AliasVault.xcworkspace
|
||||
XCODE_SCHEME: AliasVault
|
||||
XCODE_CONFIGURATION: Release
|
||||
XCODE_ARCHIVE_PATH: AliasVault.xcarchive
|
||||
XCODE_EXPORT_PATH: ./build
|
||||
XCODE_SKIP_FILESYSTEM_SYNC: true
|
||||
run: |
|
||||
cd ios
|
||||
xcodebuild clean -workspace "$XCODE_WORKSPACE" \
|
||||
-scheme "$XCODE_SCHEME" \
|
||||
-configuration "$XCODE_CONFIGURATION"
|
||||
xcodebuild -workspace "$XCODE_WORKSPACE" \
|
||||
-scheme "$XCODE_SCHEME" \
|
||||
-configuration "$XCODE_CONFIGURATION" \
|
||||
-archivePath "$XCODE_ARCHIVE_PATH" \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-allowProvisioningUpdates \
|
||||
-authenticationKeyPath $RUNNER_TEMP/asc/AuthKey.p8 \
|
||||
-authenticationKeyID ${{ env.ASC_KEY_ID }} \
|
||||
-authenticationKeyIssuerID ${{ env.ASC_ISSUER_ID }} \
|
||||
archive
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$XCODE_ARCHIVE_PATH" \
|
||||
-exportOptionsPlist ../exportOptions.plist \
|
||||
-exportPath "$XCODE_EXPORT_PATH"
|
||||
shell: bash
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
- name: Upload IPA as artifact
|
||||
if: ${{ inputs.signed == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-${{ env.VERSION }}-ios.ipa
|
||||
path: apps/mobile-app/ios/build/AliasVault.ipa
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload to App Store Connect via Fastlane
|
||||
if: ${{ inputs.upload_to_app_store_connect == 'true' }}
|
||||
env:
|
||||
ASC_KEY_ID: ${{ env.ASC_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ env.ASC_ISSUER_ID }}
|
||||
run: |
|
||||
cd apps/mobile-app/ios
|
||||
fastlane pilot upload \
|
||||
--ipa "./build/AliasVault.ipa" \
|
||||
--api_key_path "$RUNNER_TEMP/asc/AuthKey.p8" \
|
||||
--skip_waiting_for_build_processing true
|
||||
shell: bash
|
||||
157
.github/workflows/browser-extension-build.yml
vendored
157
.github/workflows/browser-extension-build.yml
vendored
@@ -32,8 +32,9 @@ jobs:
|
||||
run: |
|
||||
# Check if files exist and were recently modified
|
||||
TARGET_DIRS=(
|
||||
"apps/browser-extension/src/utils/shared/identity-generator"
|
||||
"apps/browser-extension/src/utils/shared/password-generator"
|
||||
"apps/browser-extension/src/utils/dist/shared/identity-generator"
|
||||
"apps/browser-extension/src/utils/dist/shared/password-generator"
|
||||
"apps/browser-extension/src/utils/dist/shared/models"
|
||||
)
|
||||
|
||||
for dir in "${TARGET_DIRS[@]}"; do
|
||||
@@ -42,15 +43,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for required files
|
||||
REQUIRED_FILES=("index.js" "index.mjs" "index.d.ts" "index.js.map" "index.mjs.map")
|
||||
for file in "${REQUIRED_FILES[@]}"; do
|
||||
if [ ! -f "$dir/$file" ]; then
|
||||
echo "❌ Required file $dir/$file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if files were modified in the last 5 minutes
|
||||
find "$dir" -type f -mmin -5 | grep -q . || {
|
||||
echo "❌ Files in $dir were not recently modified"
|
||||
@@ -63,157 +55,32 @@ jobs:
|
||||
build-chrome-extension:
|
||||
needs: build-shared-libraries
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Build Chrome Extension
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:chrome
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Chrome Extension
|
||||
run: npm run zip:chrome
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/chrome-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-chrome.zip -d dist/chrome-unpacked
|
||||
|
||||
- name: Upload dist artifact Chrome
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-chrome
|
||||
path: apps/browser-extension/dist/chrome-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
browser: chrome
|
||||
|
||||
build-firefox-extension:
|
||||
needs: build-shared-libraries
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Build Firefox Extension
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:firefox
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Firefox Extension
|
||||
run: npm run zip:firefox
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/firefox-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-firefox.zip -d dist/firefox-unpacked
|
||||
mkdir -p dist/sources-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-sources.zip -d dist/sources-unpacked
|
||||
|
||||
- name: Upload dist artifact Firefox
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-firefox
|
||||
path: apps/browser-extension/dist/firefox-unpacked
|
||||
|
||||
- name: Upload dist artifact Firefox sources
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-sources
|
||||
path: apps/browser-extension/dist/sources-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
browser: firefox
|
||||
|
||||
build-edge-extension:
|
||||
needs: build-shared-libraries
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/browser-extension
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get short SHA
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Build Edge Extension
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/browser-extension/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build extension
|
||||
run: npm run build:edge
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Zip Edge Extension
|
||||
run: npm run zip:edge
|
||||
|
||||
- name: Unzip for artifact
|
||||
run: |
|
||||
mkdir -p dist/edge-unpacked
|
||||
unzip dist/aliasvault-browser-extension-*-edge.zip -d dist/edge-unpacked
|
||||
|
||||
- name: Upload dist artifact Edge
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: aliasvault-browser-extension-${{ github.event_name == 'release' && github.ref_name || (github.ref_name == 'main' && format('main-{0}', steps.vars.outputs.sha_short) || steps.vars.outputs.sha_short) }}-edge
|
||||
path: apps/browser-extension/dist/edge-unpacked
|
||||
|
||||
outputs:
|
||||
sha_short: ${{ steps.vars.outputs.sha_short }}
|
||||
browser: edge
|
||||
|
||||
117
.github/workflows/mobile-app-build.yml
vendored
117
.github/workflows/mobile-app-build.yml
vendored
@@ -6,18 +6,33 @@ on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_android_signed:
|
||||
description: 'Build signed Android APK/AAB'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
build_ios_signed:
|
||||
description: 'Build signed iOS IPA'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
upload_to_app_store_connect:
|
||||
description: 'Upload iOS IPA to App Store Connect'
|
||||
required: true
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-react-native-app:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile-app
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -38,8 +53,9 @@ jobs:
|
||||
run: |
|
||||
# Check if files exist and were recently modified
|
||||
TARGET_DIRS=(
|
||||
"utils/shared/identity-generator"
|
||||
"utils/shared/password-generator"
|
||||
"utils/dist/shared/identity-generator"
|
||||
"utils/dist/shared/password-generator"
|
||||
"utils/dist/shared/models"
|
||||
)
|
||||
|
||||
for dir in "${TARGET_DIRS[@]}"; do
|
||||
@@ -48,15 +64,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for required files
|
||||
REQUIRED_FILES=("index.js" "index.mjs" "index.d.ts" "index.js.map" "index.mjs.map")
|
||||
for file in "${REQUIRED_FILES[@]}"; do
|
||||
if [ ! -f "$dir/$file" ]; then
|
||||
echo "❌ Required file $dir/$file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if files were modified in the last 5 minutes
|
||||
find "$dir" -type f -mmin -5 | grep -q . || {
|
||||
echo "❌ Files in $dir were not recently modified"
|
||||
@@ -69,6 +76,31 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
build-ios:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/mobile-app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/mobile-app/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build JS bundle (iOS - Expo)
|
||||
run: |
|
||||
mkdir -p build
|
||||
@@ -77,8 +109,59 @@ jobs:
|
||||
--output-dir ./build \
|
||||
--platform ios
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test
|
||||
build-android:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
- name: Build Android App
|
||||
uses: ./.github/actions/build-android-app
|
||||
with:
|
||||
run_tests: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
|
||||
build-android-signed:
|
||||
needs: setup
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_android_signed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build Android App
|
||||
uses: ./.github/actions/build-android-app
|
||||
with:
|
||||
signed: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
|
||||
build-ios-signed:
|
||||
needs: setup
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_ios_signed == 'true'
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build iOS App
|
||||
uses: ./.github/actions/build-ios-app
|
||||
with:
|
||||
signed: true
|
||||
upload_to_app_store_connect: ${{ github.event.inputs.upload_to_app_store_connect }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ASC_PRIVATE_KEY_BASE64: ${{ secrets.ASC_PRIVATE_KEY_BASE64 }}
|
||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||
ASC_TEAM_ID: ${{ secrets.ASC_TEAM_ID }}
|
||||
79
.github/workflows/release.yml
vendored
79
.github/workflows/release.yml
vendored
@@ -24,42 +24,67 @@ jobs:
|
||||
files: install.sh
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
package-browser-extensions:
|
||||
build-chrome-extension:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/browser-extension
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Build Chrome Extension
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: apps/browser-extension/package-lock.json
|
||||
browser: chrome
|
||||
upload_to_release: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
build-firefox-extension:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
- name: Build Firefox Extension
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
files: |
|
||||
apps/browser-extension/dist/aliasvault-browser-extension-*-chrome.zip
|
||||
apps/browser-extension/dist/aliasvault-browser-extension-*-firefox.zip
|
||||
apps/browser-extension/dist/aliasvault-browser-extension-*-edge.zip
|
||||
apps/browser-extension/dist/aliasvault-browser-extension-*-sources.zip
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
browser: firefox
|
||||
upload_to_release: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-edge-extension:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build Edge Extension
|
||||
uses: ./.github/actions/build-browser-extension
|
||||
with:
|
||||
browser: edge
|
||||
upload_to_release: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-android-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build Android App
|
||||
uses: ./.github/actions/build-android-app
|
||||
with:
|
||||
signed: true
|
||||
upload_to_release: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
|
||||
build-and-push-docker:
|
||||
needs: [upload-install-script, package-browser-extensions]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -66,9 +66,9 @@ jobs:
|
||||
run: |
|
||||
$scanner = "${{ github.workspace }}\.sonar\scanner\dotnet-sonarscanner"
|
||||
if ('${{ github.event_name }}' -eq 'pull_request_target') {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html"
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.pullrequest.key=${{ github.event.pull_request.number }} /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**"
|
||||
} else {
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html"
|
||||
& $scanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs" /d:sonar.exclusions="**/__tests__/test-forms/*.html,**/dist/shared/**"
|
||||
}
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -9,7 +9,6 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.code-workspace
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
@@ -419,4 +418,7 @@ temp
|
||||
# libraries and copied to the application so they can be used for debugging, but we don't need
|
||||
# to check them in as it's not needed for the applications to actually run.
|
||||
**/*.js.map
|
||||
**/*.mjs.map
|
||||
**/*.mjs.map
|
||||
|
||||
# Android keystore file (for publishing to Google Play)
|
||||
*.keystore
|
||||
|
||||
27
.vscode/AliasVault.code-workspace
vendored
Normal file
27
.vscode/AliasVault.code-workspace
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "AliasVault",
|
||||
"path": "../"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"path": "../apps/server"
|
||||
},
|
||||
{
|
||||
"name": "browser-extension",
|
||||
"path": "../apps/browser-extension"
|
||||
},
|
||||
{
|
||||
"name": "mobile-app",
|
||||
"path": "../apps/mobile-app"
|
||||
},
|
||||
{
|
||||
"path": "../docs"
|
||||
},
|
||||
{
|
||||
"path": "../shared"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -155,10 +155,10 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run Android App",
|
||||
"label": "Run release Android App (device)",
|
||||
"type": "shell",
|
||||
"command": "npx",
|
||||
"args": ["expo", "run:android"],
|
||||
"args": ["expo", "run:android", "--device", "--variant", "release"],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# <img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="35" alt="AliasVault"> AliasVault
|
||||
End-to-end encrypted password manager with built-in alias and email generation — giving you full control over your online identity and safeguarding your privacy. AliasVault: the privacy toolbox that you control.
|
||||
The privacy-first password & email alias manager. Fully end-to-end encrypted, with built-in alias generation and email server — giving you full control over your online identity and safeguarding your privacy.
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github&label=Release">](https://github.com/lanedirt/AliasVault/releases)
|
||||
[](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-tests.yml)
|
||||
@@ -47,7 +47,7 @@ Built on 15 years of experience, AliasVault is open-source, self-hostable and co
|
||||
## Cloud-hosted
|
||||
Use the official cloud version of AliasVault at [app.aliasvault.net](https://app.aliasvault.net). This fully supported platform is always up to date with our latest release.
|
||||
|
||||
AliasVault is available on: [Web](https://app.aliasvault.net) | [iOS](https://apps.apple.com/app/id6745490915) | [Chrome](https://chromewebstore.google.com/detail/aliasvault/bmoggiinmnodjphdjnmpcnlleamkfedj) | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/aliasvault/) | [Edge](https://microsoftedge.microsoft.com/addons/detail/aliasvault/kabaanafahnjkfkplbnllebdmppdemfo) | [Safari](https://apps.apple.com/app/id6743163173)
|
||||
AliasVault is available on: [Web](https://app.aliasvault.net) | [iOS](https://apps.apple.com/app/id6745490915) | [Android](https://play.google.com/store/apps/details?id=net.aliasvault.app) | [Chrome](https://chromewebstore.google.com/detail/aliasvault/bmoggiinmnodjphdjnmpcnlleamkfedj) | [Firefox](https://addons.mozilla.org/en-US/firefox/addon/aliasvault/) | [Edge](https://microsoftedge.microsoft.com/addons/detail/aliasvault/kabaanafahnjkfkplbnllebdmppdemfo) | [Safari](https://apps.apple.com/app/id6743163173)
|
||||
|
||||
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
|
||||
|
||||
@@ -114,8 +114,8 @@ Core features that are being worked on:
|
||||
- [x] Built-in TOTP authenticator
|
||||
- [x] Import passwords from traditional password managers
|
||||
- [x] iOS native app
|
||||
- [ ] Android native app
|
||||
- [ ] Data model improvements to support reusable identities in combination with aliases
|
||||
- [x] Android native app
|
||||
- [ ] Data model and usability improvements (more flexible aliases and credential types, folder support, editing in browser extension, bulk selecting etc.)
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
|
||||
1
apps/browser-extension/.gitignore
vendored
1
apps/browser-extension/.gitignore
vendored
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.output
|
||||
dist
|
||||
!src/utils/dist
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
|
||||
@@ -12,7 +12,7 @@ export default [
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"src/utils/shared/**",
|
||||
"src/utils/dist/**",
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
@@ -105,8 +105,57 @@ export default [
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react/jsx-no-constructed-context-values": "error",
|
||||
},
|
||||
"import/no-unresolved": [
|
||||
"error",
|
||||
{
|
||||
ignore: ['^#imports$'] // Ignore virtual imports from WXT which are not resolved by the typescript compiler
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
"builtin", // Node "fs", "path", etc.
|
||||
"external", // "react", "lodash", etc.
|
||||
"internal", // Aliased paths like "@/utils"
|
||||
"parent", // "../"
|
||||
"sibling", // "./"
|
||||
"index", // "./index"
|
||||
"object", // import 'foo'
|
||||
"type" // import type ...
|
||||
],
|
||||
"pathGroups": [
|
||||
{
|
||||
pattern: "@/entrypoints/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
},
|
||||
{
|
||||
pattern: "@/utils/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
},
|
||||
{
|
||||
pattern: "@/hooks/**",
|
||||
group: "internal",
|
||||
position: "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin"],
|
||||
"newlines-between": "always",
|
||||
"alphabetize": {
|
||||
order: "asc",
|
||||
caseInsensitive: true
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: './tsconfig.json',
|
||||
},
|
||||
},
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
|
||||
545
apps/browser-extension/package-lock.json
generated
545
apps/browser-extension/package-lock.json
generated
@@ -1,25 +1,28 @@
|
||||
{
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.0.0",
|
||||
"version": "0.18.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.0.0",
|
||||
"version": "0.18.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"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-hook-form": "^7.57.0",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
"vitest": "^3.0.8",
|
||||
"webext-bridge": "^6.0.1"
|
||||
"webext-bridge": "^6.0.1",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.280",
|
||||
@@ -28,12 +31,14 @@
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@types/yup": "^0.29.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@wxt-dev/module-react": "^1.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
@@ -699,6 +704,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||
"integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz",
|
||||
"integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@es-joy/jsdoccomment": {
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.49.0.tgz",
|
||||
@@ -1310,6 +1349,18 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz",
|
||||
"integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -1478,6 +1529,19 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
|
||||
"integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@tybys/wasm-util": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz",
|
||||
@@ -1894,6 +1958,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||
"integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -2085,6 +2166,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yup": {
|
||||
"version": "0.29.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.29.14.tgz",
|
||||
"integrity": "sha512-Ynb/CjHhE/Xp/4bhHmQC4U1Ox+I2OpfRYF3dnNgQqn1cHa6LK3H1wJMNPT02tSVZA6FYuXE2ITORfbnb6zBCSA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
||||
@@ -2278,6 +2366,247 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-darwin-arm64": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.11.tgz",
|
||||
"integrity": "sha512-i3/wlWjQJXMh1uiGtiv7k1EYvrrS3L1hdwmWJJiz1D8jWy726YFYPIxQWbEIVPVAgrfRR0XNlLrTQwq17cuCGw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-darwin-x64": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.11.tgz",
|
||||
"integrity": "sha512-8XXyFvc6w6kmMmi6VYchZhjd5CDcp+Lv6Cn1YmUme0ypsZ/0Kzd+9ESrWtDrWibKPTgSteDTxp75cvBOY64FQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-freebsd-x64": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.11.tgz",
|
||||
"integrity": "sha512-0qJBYzP8Qk24CZ05RSWDQUjdiQUeIJGfqMMzbtXgCKl/a5xa6thfC0MQkGIr55LCLd6YmMyO640ifYUa53lybQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.11.tgz",
|
||||
"integrity": "sha512-1sGwpgvx+WZf0GFT6vkkOm6UJ+mlsVnjw+Yv9esK71idWeRAG3bbpkf3AoY8KIqKqmnzJExi0uKxXpakQ5Pcbg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.11.tgz",
|
||||
"integrity": "sha512-D/1F/2lTe+XAl3ohkYj51NjniVly8sIqkA/n1aOND3ZMO418nl2JNU95iVa1/RtpzaKcWEsNTtHRogykrUflJg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.11.tgz",
|
||||
"integrity": "sha512-7vFWHLCCNFLEQlmwKQfVy066ohLLArZl+AV/AdmrD1/pD1FlmqM+FKbtnONnIwbHtgetFUCV/SRi1q4D49aTlw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm64-musl": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.11.tgz",
|
||||
"integrity": "sha512-tYkGIx8hjWPh4zcn17jLEHU8YMmdP2obRTGkdaB3BguGHh31VCS3ywqC4QjTODjmhhNyZYkj/1Dz/+0kKvg9YA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.11.tgz",
|
||||
"integrity": "sha512-6F328QIUev29vcZeRX6v6oqKxfUoGwIIAhWGD8wSysnBYFY0nivp25jdWmAb1GildbCCaQvOKEhCok7YfWkj4Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.11.tgz",
|
||||
"integrity": "sha512-NqhWmiGJGdzbZbeucPZIG9Iav4lyYLCarEnxAceguMx9qlpeEF7ENqYKOwB8Zqk7/CeuYMEcLYMaW2li6HyDzQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.11.tgz",
|
||||
"integrity": "sha512-J2RPIFKMdTrLtBdfR1cUMKl8Gcy05nlQ+bEs/6al7EdWLk9cs3tnDREHZ7mV9uGbeghpjo4i8neNZNx3PYUY9w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.11.tgz",
|
||||
"integrity": "sha512-bDpGRerHvvHdhun7MmFUNDpMiYcJSqWckwAVVRTJf8F+RyqYJOp/mx04PDc7DhpNPeWdnTMu91oZRMV+gGaVcQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-x64-gnu": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.11.tgz",
|
||||
"integrity": "sha512-G9U7bVmylzRLma3cK39RBm3guoD1HOvY4o0NS4JNm37AD0lS7/xyMt7kn0JejYyc0Im8J+rH69/dXGM9DAJcSQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-x64-musl": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.11.tgz",
|
||||
"integrity": "sha512-7qL20SBKomekSunm7M9Fe5L93bFbn+FbHiGJbfTlp0RKhPVoJDP73vOxf1QrmJHyDPECsGWPFnKa/f8fO2FsHw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-wasm32-wasi": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.11.tgz",
|
||||
"integrity": "sha512-jisvIva8MidjI+B1lFRZZMfCPaCISePgTyR60wNT1MeQvIh5Ksa0G3gvI+Iqyj3jqYbvOHByenpa5eDGcSdoSg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^0.2.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.11.tgz",
|
||||
"integrity": "sha512-G+H5nQZ8sRZ8ebMY6mRGBBvTEzMYEcgVauLsNHpvTUavZoCCRVP1zWkCZgOju2dW3O22+8seTHniTdl1/uLz3g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.11.tgz",
|
||||
"integrity": "sha512-Hfy46DBfFzyv0wgR0MMOwFFib2W2+Btc8oE5h4XlPhpelnSyA6nFxkVIyTgIXYGTdFaLoZFNn62fmqx3rjEg3A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-x64-msvc": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.11.tgz",
|
||||
"integrity": "sha512-7L8NdsQlCJ8T106Gbz/AjzM4QKWVsoQbKpB9bMBGcIZswUuAnJMHpvbqGW3RBqLHCIwX4XZ5fxSBHEFcK2h9wA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
|
||||
@@ -4421,9 +4750,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -5191,6 +5520,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-context": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.8.tgz",
|
||||
"integrity": "sha512-bq+F7nyc65sKpZGT09dY0S0QrOnQtuDVIfyTGQ8uuvtMIF7oHp6CEP3mouN0rrnYF3Jqo6Ke0BfU/5wASZue1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-tsconfig": "^4.10.1",
|
||||
"stable-hash-x": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-import-context"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"unrs-resolver": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"unrs-resolver": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
@@ -5213,6 +5567,41 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-typescript": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.3.tgz",
|
||||
"integrity": "sha512-elVDn1eWKFrWlzxlWl9xMt8LltjKl161Ix50JFC50tHXI5/TRP32SNEqlJ/bo/HV+g7Rou/tlPQU2AcRtIhrOg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.1",
|
||||
"eslint-import-context": "^0.1.8",
|
||||
"get-tsconfig": "^4.10.1",
|
||||
"is-bun-module": "^2.0.0",
|
||||
"stable-hash-x": "^0.1.1",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"unrs-resolver": "^1.7.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.17.0 || >=18.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-import-resolver-typescript"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "*",
|
||||
"eslint-plugin-import": "*",
|
||||
"eslint-plugin-import-x": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint-plugin-import": {
|
||||
"optional": true
|
||||
},
|
||||
"eslint-plugin-import-x": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
|
||||
@@ -6279,6 +6668,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
|
||||
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/giget": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-1.2.5.tgz",
|
||||
@@ -6911,6 +7313,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-bun-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
|
||||
"integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/is-callable": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||
@@ -8569,6 +8981,22 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz",
|
||||
"integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"napi-postinstall": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/napi-postinstall"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -9702,6 +10130,12 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
@@ -10340,6 +10774,22 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.57.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
|
||||
"integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -10588,6 +11038,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||
@@ -11333,6 +11793,16 @@
|
||||
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stable-hash-x": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.1.1.tgz",
|
||||
"integrity": "sha512-l0x1D6vhnsNUGPFVDx45eif0y6eedVC8nm5uACTrVFJFtl2mLRW17aWtVyxFCpn5t94VUPkjU8vSLwIuwwqtJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
@@ -11833,6 +12303,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-uid": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tiny-uid/-/tiny-uid-1.1.2.tgz",
|
||||
@@ -11852,9 +12328,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
|
||||
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.4.4",
|
||||
@@ -11986,6 +12462,12 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
@@ -12346,6 +12828,39 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
"version": "1.7.11",
|
||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.11.tgz",
|
||||
"integrity": "sha512-OhuAzBImFPjKNgZ2JwHMfGFUA6NSbRegd1+BPjC1Y0E6X9Y/vJ4zKeGmIMqmlYboj6cMNEwKI+xQisrg4J0HaQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unrs-resolver"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@unrs/resolver-binding-darwin-arm64": "1.7.11",
|
||||
"@unrs/resolver-binding-darwin-x64": "1.7.11",
|
||||
"@unrs/resolver-binding-freebsd-x64": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-arm-musleabihf": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-arm64-gnu": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-arm64-musl": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-ppc64-gnu": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-riscv64-gnu": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-riscv64-musl": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-s390x-gnu": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-x64-gnu": "1.7.11",
|
||||
"@unrs/resolver-binding-linux-x64-musl": "1.7.11",
|
||||
"@unrs/resolver-binding-wasm32-wasi": "1.7.11",
|
||||
"@unrs/resolver-binding-win32-arm64-msvc": "1.7.11",
|
||||
"@unrs/resolver-binding-win32-ia32-msvc": "1.7.11",
|
||||
"@unrs/resolver-binding-win32-x64-msvc": "1.7.11"
|
||||
}
|
||||
},
|
||||
"node_modules/untildify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
@@ -13472,6 +13987,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
|
||||
"integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-dir": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/zip-dir/-/zip-dir-2.0.0.tgz",
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.19.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"dev:edge": "wxt -b edge",
|
||||
"dev:safari": "wxt -b safari",
|
||||
"build:chrome": "wxt build -b chrome",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"build:edge": "wxt build -b edge",
|
||||
@@ -25,17 +26,20 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"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-hook-form": "^7.57.0",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0",
|
||||
"sql.js": "^1.12.0",
|
||||
"vitest": "^3.0.8",
|
||||
"webext-bridge": "^6.0.1"
|
||||
"webext-bridge": "^6.0.1",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.280",
|
||||
@@ -44,12 +48,14 @@
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@types/yup": "^0.29.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"@vitest/coverage-v8": "^3.0.8",
|
||||
"@wxt-dev/module-react": "^1.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
|
||||
@@ -447,7 +447,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -460,7 +460,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 0.19.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -479,7 +479,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -492,7 +492,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 0.19.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -515,7 +515,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -530,7 +530,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.17.1;
|
||||
MARKETING_VERSION = 0.19.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -554,7 +554,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -569,7 +569,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.17.1;
|
||||
MARKETING_VERSION = 0.19.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
html, body {
|
||||
width: 350px;
|
||||
max-width: 350px;
|
||||
height: 600px;
|
||||
max-height: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { defineBackground } from '#imports';
|
||||
import { onMessage, sendMessage } from "webext-bridge/background";
|
||||
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
|
||||
import { storage, browser } from '#imports';
|
||||
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentityLanguage, handleGetDerivedKey, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
|
||||
|
||||
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
|
||||
|
||||
import { defineBackground, storage, browser } from '#imports';
|
||||
|
||||
export default defineBackground({
|
||||
/**
|
||||
* This is the main entry point for the background script.
|
||||
@@ -14,6 +16,7 @@ export default defineBackground({
|
||||
// Listen for messages using webext-bridge
|
||||
onMessage('CHECK_AUTH_STATUS', () => handleCheckAuthStatus());
|
||||
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
|
||||
onMessage('UPLOAD_VAULT', ({ data }) => handleUploadVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
onMessage('CLEAR_VAULT', () => handleClearVault());
|
||||
@@ -27,12 +30,16 @@ export default defineBackground({
|
||||
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
|
||||
onMessage('TOGGLE_CONTEXT_MENU', ({ data }) => handleToggleContextMenu(data));
|
||||
|
||||
onMessage('PERSIST_FORM_VALUES', ({ data }) => handlePersistFormValues(data));
|
||||
onMessage('GET_PERSISTED_FORM_VALUES', () => handleGetPersistedFormValues());
|
||||
onMessage('CLEAR_PERSISTED_FORM_VALUES', () => handleClearPersistedFormValues());
|
||||
|
||||
// Setup context menus
|
||||
const isContextMenuEnabled = await storage.getItem(GLOBAL_CONTEXT_MENU_ENABLED_KEY) ?? true;
|
||||
if (isContextMenuEnabled) {
|
||||
setupContextMenus();
|
||||
}
|
||||
|
||||
|
||||
// Listen for custom commands
|
||||
try {
|
||||
browser.commands.onCommand.addListener(async (command) => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { sendMessage } from 'webext-bridge/background';
|
||||
import { browser } from "#imports";
|
||||
import { type Browser } from '@wxt-dev/browser';
|
||||
import { PasswordGenerator } from '@/utils/shared/password-generator';
|
||||
import { sendMessage } from 'webext-bridge/background';
|
||||
|
||||
import { PasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
import { browser } from "#imports";
|
||||
|
||||
/**
|
||||
* Setup the context menus.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { browser } from '#imports';
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
|
||||
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
import { setupContextMenus } from './ContextMenu';
|
||||
|
||||
import { browser } from '#imports';
|
||||
|
||||
/**
|
||||
* Handle opening the popup.
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { storage } from 'wxt/utils/storage';
|
||||
|
||||
import type { Vault, VaultResponse, VaultPostResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
import { Vault } from '@/utils/types/webapi/Vault';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
import { VaultPostResponse } from '@/utils/types/webapi/VaultPostResponse';
|
||||
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 { StringResponse as stringResponse } from '@/utils/types/messaging/StringResponse';
|
||||
import { PasswordSettingsResponse as messagePasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
|
||||
import { StoreVaultRequest } from '@/utils/types/messaging/StoreVaultRequest';
|
||||
import { StringResponse as stringResponse } from '@/utils/types/messaging/StringResponse';
|
||||
import { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
|
||||
import { VaultUploadResponse as messageVaultUploadResponse } from '@/utils/types/messaging/VaultUploadResponse';
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked.
|
||||
@@ -36,17 +37,32 @@ export async function handleStoreVault(
|
||||
message: any,
|
||||
) : Promise<messageBoolResponse> {
|
||||
try {
|
||||
const vaultResponse = message.vaultResponse as VaultResponse;
|
||||
const encryptedVaultBlob = vaultResponse.vault.blob;
|
||||
const vaultRequest = message as StoreVaultRequest;
|
||||
|
||||
// Store encrypted vault and derived key in session storage.
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: encryptedVaultBlob },
|
||||
{ key: 'session:derivedKey', value: message.derivedKey },
|
||||
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
|
||||
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
|
||||
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
|
||||
]);
|
||||
// Store new encrypted vault in session storage.
|
||||
await storage.setItem('session:encryptedVault', vaultRequest.vaultBlob);
|
||||
|
||||
/*
|
||||
* For all other values, check if they have a value and store them in session storage if they do.
|
||||
* Some updates, e.g. when mutating local database, these values will not be set.
|
||||
*/
|
||||
|
||||
// Store derived key in session storage (if it has a value)
|
||||
if (vaultRequest.derivedKey) {
|
||||
await storage.setItem('session:derivedKey', vaultRequest.derivedKey);
|
||||
}
|
||||
|
||||
if (vaultRequest.publicEmailDomainList) {
|
||||
await storage.setItem('session:publicEmailDomains', vaultRequest.publicEmailDomainList);
|
||||
}
|
||||
|
||||
if (vaultRequest.privateEmailDomainList) {
|
||||
await storage.setItem('session:privateEmailDomains', vaultRequest.privateEmailDomainList);
|
||||
}
|
||||
|
||||
if (vaultRequest.vaultRevisionNumber) {
|
||||
await storage.setItem('session:vaultRevisionNumber', vaultRequest.vaultRevisionNumber);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -210,48 +226,16 @@ export async function getEmailAddressesForVault(
|
||||
/**
|
||||
* Get default email domain for a vault.
|
||||
*/
|
||||
export function handleGetDefaultEmailDomain(
|
||||
) : Promise<stringResponse> {
|
||||
return (async () : Promise<stringResponse> => {
|
||||
export function handleGetDefaultEmailDomain(): 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[];
|
||||
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const defaultEmailDomain = sqliteClient.getDefaultEmailDomain();
|
||||
const defaultEmailDomain = sqliteClient.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
|
||||
/**
|
||||
* Check if a domain is valid.
|
||||
*/
|
||||
const isValidDomain = (domain: string) : boolean => {
|
||||
const isValid = (domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain))) as boolean;
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// First check if the default domain that is configured in the vault is still valid.
|
||||
if (defaultEmailDomain && isValidDomain(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, value: firstPrivate };
|
||||
}
|
||||
|
||||
// Return first valid public domain if no private domains are available.
|
||||
const firstPublic = publicEmailDomains.find(isValidDomain);
|
||||
|
||||
if (firstPublic) {
|
||||
return { success: true, value: firstPublic };
|
||||
}
|
||||
|
||||
// Return null if no valid domains are found
|
||||
return { success: true };
|
||||
return { success: true, value: defaultEmailDomain ?? undefined };
|
||||
} catch (error) {
|
||||
console.error('Error getting default email domain:', error);
|
||||
return { success: false, error: 'Failed to get default email domain' };
|
||||
@@ -300,10 +284,82 @@ export async function handleGetDerivedKey(
|
||||
return derivedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload the vault to the server.
|
||||
*/
|
||||
export async function handleUploadVault(
|
||||
message: any
|
||||
) : Promise<messageVaultUploadResponse> {
|
||||
try {
|
||||
// Store the new vault blob in session storage.
|
||||
await storage.setItem('session:encryptedVault', message.vaultBlob);
|
||||
|
||||
// Create new sqlite client which will use the new vault blob.
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
|
||||
// Upload the new vault to the server.
|
||||
const response = await uploadNewVaultToServer(sqliteClient);
|
||||
return { success: true, status: response.status, newRevisionNumber: response.newRevisionNumber };
|
||||
} catch (error) {
|
||||
console.error('Failed to upload vault:', error);
|
||||
return { success: false, error: 'Failed to upload vault' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle persisting form values to storage.
|
||||
* Data is encrypted using the derived key for additional security.
|
||||
*/
|
||||
export async function handlePersistFormValues(data: any): Promise<void> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
if (!derivedKey) {
|
||||
throw new Error('No derived key available for encryption');
|
||||
}
|
||||
|
||||
// Always stringify the data properly
|
||||
const serializedData = JSON.stringify(data);
|
||||
const encryptedData = await EncryptionUtility.symmetricEncrypt(
|
||||
serializedData,
|
||||
derivedKey
|
||||
);
|
||||
await storage.setItem('session:persistedFormValues', encryptedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle retrieving persisted form values from storage.
|
||||
* Data is decrypted using the derived key.
|
||||
*/
|
||||
export async function handleGetPersistedFormValues(): Promise<any | null> {
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
const encryptedData = await storage.getItem('session:persistedFormValues') as string | null;
|
||||
|
||||
if (!encryptedData || !derivedKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedData = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedData,
|
||||
derivedKey
|
||||
);
|
||||
return JSON.parse(decryptedData);
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt or parse persisted form values:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clearing persisted form values from storage.
|
||||
*/
|
||||
export async function handleClearPersistedFormValues(): Promise<void> {
|
||||
await storage.removeItem('session:persistedFormValues');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a new version of the vault to the server using the provided sqlite client.
|
||||
*/
|
||||
async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<void> {
|
||||
async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<VaultPostResponse> {
|
||||
const updatedVaultData = sqliteClient.exportToBase64();
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
@@ -347,6 +403,8 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<void
|
||||
} else {
|
||||
throw new Error('Failed to upload new vault to server');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,7 +413,6 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<void
|
||||
async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const derivedKey = await storage.getItem('session:derivedKey') as string;
|
||||
|
||||
if (!encryptedVault || !derivedKey) {
|
||||
throw new Error('No vault or derived key found');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import '@/entrypoints/contentScript/style.css';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from '@/entrypoints/contentScript/Popup';
|
||||
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
|
||||
import { onMessage } from "webext-bridge/content-script";
|
||||
|
||||
import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form';
|
||||
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse';
|
||||
|
||||
import { defineContentScript } from '#imports';
|
||||
import { createShadowRootUi } from '#imports';
|
||||
|
||||
@@ -55,6 +58,11 @@ export default defineContentScript({
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show popup for autofill-triggerable fields
|
||||
if (!formDetector.isAutofillTriggerableField()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only inject icon and show popup if autofill popup is enabled
|
||||
if (await isAutoShowPopupEnabled()) {
|
||||
injectIcon(inputElement, container);
|
||||
@@ -117,10 +125,10 @@ export default defineContentScript({
|
||||
}
|
||||
|
||||
/**
|
||||
* By default we check if the popup is not disabled (for current site)
|
||||
* By default we check if the popup is not disabled (for current site) and if the field is autofill-triggerable
|
||||
* but if forceShow is true, we show the popup regardless.
|
||||
*/
|
||||
const canShowPopup = forceShow || (await isAutoShowPopupEnabled());
|
||||
const canShowPopup = forceShow || (await isAutoShowPopupEnabled() && formDetector.isAutofillTriggerableField());
|
||||
|
||||
if (canShowPopup) {
|
||||
injectIcon(inputElement, container);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CombinedStopWords } from '@/utils/formDetector/FieldPatterns';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
|
||||
type CredentialWithPriority = Credential & {
|
||||
priority: number;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { openAutofillPopup } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { FormFiller } from '@/utils/formDetector/FormFiller';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import { openAutofillPopup } from '@/entrypoints/contentScript/Popup';
|
||||
|
||||
/**
|
||||
* Global timestamp to track popup debounce time.
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { storage } from '#imports';
|
||||
import { sendMessage } from 'webext-bridge/content-script';
|
||||
import { fillCredential } from '@/entrypoints/contentScript/Form';
|
||||
|
||||
import { filterCredentials } from '@/entrypoints/contentScript/Filter';
|
||||
import { IdentityGeneratorEn, IdentityGeneratorNl } from '@/utils/shared/identity-generator';
|
||||
import { PasswordGenerator } from '@/utils/shared/password-generator';
|
||||
import { fillCredential } from '@/entrypoints/contentScript/Form';
|
||||
|
||||
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY } from '@/utils/Constants';
|
||||
import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
|
||||
import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
import { BaseIdentityGenerator } from '@/utils/shared/identity-generator';
|
||||
import { StringResponse } from '@/utils/types/messaging/StringResponse';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, LAST_CUSTOM_EMAIL_KEY, LAST_CUSTOM_USERNAME_KEY } from '@/utils/Constants';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* WeakMap to store event listeners for popup containers
|
||||
@@ -243,23 +245,21 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
|
||||
} else {
|
||||
// Generate new random identity using identity generator.
|
||||
const identityLanguage = await sendMessage('GET_DEFAULT_IDENTITY_LANGUAGE', {}, 'background') as StringResponse;
|
||||
let identityGenerator: BaseIdentityGenerator;
|
||||
switch (identityLanguage.value) {
|
||||
case 'nl':
|
||||
identityGenerator = new IdentityGeneratorNl();
|
||||
break;
|
||||
case 'en':
|
||||
default:
|
||||
identityGenerator = new IdentityGeneratorEn();
|
||||
break;
|
||||
}
|
||||
const identity = await identityGenerator.generateRandomIdentity();
|
||||
const identityGenerator = CreateIdentityGenerator(identityLanguage.value ?? 'en');
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
|
||||
// Get password settings from background
|
||||
const passwordSettingsResponse = await sendMessage('GET_PASSWORD_SETTINGS', {}, 'background') as PasswordSettingsResponse;
|
||||
|
||||
// Initialize password generator with the retrieved settings
|
||||
const passwordGenerator = new PasswordGenerator(passwordSettingsResponse.settings);
|
||||
const passwordGenerator = CreatePasswordGenerator(passwordSettingsResponse.settings ?? {
|
||||
Length: 12,
|
||||
UseLowercase: true,
|
||||
UseUppercase: true,
|
||||
UseNumbers: true,
|
||||
UseSpecialChars: true,
|
||||
UseNonAmbiguousChars: true
|
||||
});
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
// Extract favicon from page and get the bytes
|
||||
@@ -946,6 +946,22 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
}
|
||||
});
|
||||
|
||||
// Get password settings from background
|
||||
let passwordGenerator: PasswordGenerator;
|
||||
sendMessage('GET_PASSWORD_SETTINGS', {}, 'background').then((response) => {
|
||||
const passwordSettingsResponse = response as PasswordSettingsResponse;
|
||||
passwordGenerator = CreatePasswordGenerator(passwordSettingsResponse.settings ?? {
|
||||
Length: 12,
|
||||
UseLowercase: true,
|
||||
UseUppercase: true,
|
||||
UseNumbers: true,
|
||||
UseSpecialChars: true,
|
||||
UseNonAmbiguousChars: true
|
||||
});
|
||||
// Generate initial password after settings are loaded
|
||||
passwordGenerator.generateRandomPassword();
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate and set password.
|
||||
*/
|
||||
@@ -960,15 +976,6 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
|
||||
updateVisibilityIcon(true);
|
||||
};
|
||||
|
||||
// Get password settings from background
|
||||
let passwordGenerator: PasswordGenerator;
|
||||
sendMessage('GET_PASSWORD_SETTINGS', {}, 'background').then((response) => {
|
||||
const passwordSettingsResponse = response as PasswordSettingsResponse;
|
||||
passwordGenerator = new PasswordGenerator(passwordSettingsResponse.settings);
|
||||
// Generate initial password after settings are loaded
|
||||
generatePassword();
|
||||
});
|
||||
|
||||
// Handle regenerate button click
|
||||
regenerateBtn.addEventListener('click', generatePassword);
|
||||
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
import Header from '@/entrypoints/popup/components/Layout/Header';
|
||||
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
|
||||
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import Home from '@/entrypoints/popup/pages/Home';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
|
||||
import Settings from '@/entrypoints/popup/pages/Settings';
|
||||
|
||||
import GlobalStateChangeHandler from '@/entrypoints/popup/components/GlobalStateChangeHandler';
|
||||
import BottomNav from '@/entrypoints/popup/components/Layout/BottomNav';
|
||||
import Header from '@/entrypoints/popup/components/Layout/Header';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { NavigationProvider } from '@/entrypoints/popup/context/NavigationContext';
|
||||
import AuthSettings from '@/entrypoints/popup/pages/AuthSettings';
|
||||
import CredentialAddEdit from '@/entrypoints/popup/pages/CredentialAddEdit';
|
||||
import CredentialDetails from '@/entrypoints/popup/pages/CredentialDetails';
|
||||
import CredentialsList from '@/entrypoints/popup/pages/CredentialsList';
|
||||
import EmailDetails from '@/entrypoints/popup/pages/EmailDetails';
|
||||
import EmailsList from '@/entrypoints/popup/pages/EmailsList';
|
||||
import Home from '@/entrypoints/popup/pages/Home';
|
||||
import Login from '@/entrypoints/popup/pages/Login';
|
||||
import Logout from '@/entrypoints/popup/pages/Logout';
|
||||
import Settings from '@/entrypoints/popup/pages/Settings';
|
||||
import Unlock from '@/entrypoints/popup/pages/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
import '@/entrypoints/popup/style.css';
|
||||
|
||||
/**
|
||||
@@ -35,13 +44,19 @@ const App: React.FC = () => {
|
||||
const { isInitialLoading } = useLoading();
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const { headerButtons } = useHeaderButtons();
|
||||
|
||||
// Add these route configurations
|
||||
const routes: RouteConfig[] = [
|
||||
{ path: '/', element: <Home />, showBackButton: false },
|
||||
{ path: '/login', element: <Login />, showBackButton: false },
|
||||
{ path: '/unlock', element: <Unlock />, showBackButton: false },
|
||||
{ path: '/unlock-success', element: <UnlockSuccess onClose={() => window.location.search = ''} />, showBackButton: false },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: 'Settings' },
|
||||
{ path: '/credentials', element: <CredentialsList />, showBackButton: false },
|
||||
{ path: '/credentials/add', element: <CredentialAddEdit />, showBackButton: true, title: 'Add credential' },
|
||||
{ path: '/credentials/:id', element: <CredentialDetails />, showBackButton: true, title: 'Credential details' },
|
||||
{ path: '/credentials/:id/edit', element: <CredentialAddEdit />, showBackButton: true, title: 'Edit credential' },
|
||||
{ path: '/emails', element: <EmailsList />, showBackButton: false },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: 'Email details' },
|
||||
{ path: '/settings', element: <Settings />, showBackButton: false },
|
||||
@@ -67,44 +82,45 @@ const App: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col">
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
<NavigationProvider>
|
||||
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GlobalStateChangeHandler />
|
||||
<Header
|
||||
routes={routes}
|
||||
/>
|
||||
<GlobalStateChangeHandler />
|
||||
<Header
|
||||
routes={routes}
|
||||
rightButtons={headerButtons}
|
||||
/>
|
||||
|
||||
<main
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: 'calc(100% - 120px)',
|
||||
maxHeight: '600px',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
{message && (
|
||||
<p className="text-red-500 mb-4">{message}</p>
|
||||
)}
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
<main
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: 'calc(100% - 120px)',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
{message && (
|
||||
<p className="text-red-500 mb-4">{message}</p>
|
||||
)}
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
</NavigationProvider>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
|
||||
type CredentialCardProps = {
|
||||
@@ -23,18 +24,34 @@ const CredentialCard: React.FC<CredentialCardProps> = ({ credential }) => {
|
||||
* @returns The display text for the credential
|
||||
*/
|
||||
const getDisplayText = (cred: Credential): string => {
|
||||
let returnValue = '';
|
||||
|
||||
// Show username if available
|
||||
if (cred.Username) {
|
||||
return cred.Username;
|
||||
returnValue = cred.Username;
|
||||
}
|
||||
|
||||
// Show email if username is not available
|
||||
if (cred.Alias?.Email) {
|
||||
return cred.Alias.Email;
|
||||
returnValue = cred.Alias.Email;
|
||||
}
|
||||
|
||||
// Show empty string if neither username nor email is available
|
||||
return '';
|
||||
// Trim the return value to max. 33 characters.
|
||||
return returnValue.length > 33 ? returnValue.slice(0, 30) + '...' : returnValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the service name for a credential, trimming it to maximum length so it doesn't overflow the UI.
|
||||
*/
|
||||
const getCredentialServiceName = (cred: Credential): string => {
|
||||
let returnValue = 'Untitled';
|
||||
|
||||
if (cred.ServiceName) {
|
||||
returnValue = cred.ServiceName;
|
||||
}
|
||||
|
||||
// Trim the return value to max. 33 characters.
|
||||
return returnValue.length > 33 ? returnValue.slice(0, 30) + '...' : returnValue;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -53,7 +70,7 @@ const CredentialCard: React.FC<CredentialCardProps> = ({ credential }) => {
|
||||
}}
|
||||
/>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{credential.ServiceName}</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{getCredentialServiceName(credential)}</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{getDisplayText(credential)}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
|
||||
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
|
||||
import { IdentityHelperUtils } from '@/utils/shared/identity-generator';
|
||||
|
||||
import { IdentityHelperUtils } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
type AliasBlockProps = {
|
||||
credential: Credential;
|
||||
|
||||
@@ -1,18 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import { EmailPreview } from '@/entrypoints/popup/components/EmailPreview';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
type EmailBlockProps = {
|
||||
email: string;
|
||||
isSupported: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the email block.
|
||||
*/
|
||||
const EmailBlock: React.FC<EmailBlockProps> = ({ email, isSupported }) => (
|
||||
<>
|
||||
{isSupported && <EmailPreview email={email} />}
|
||||
</>
|
||||
);
|
||||
const EmailBlock: React.FC<EmailBlockProps> = ({ email }) => {
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Check if the email domain is supported.
|
||||
*/
|
||||
const isEmailDomainSupported = async (email: string): Promise<boolean> => {
|
||||
const domain = email.split('@')[1]?.toLowerCase();
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const vaultMetadata = await dbContext.getVaultMetadata();
|
||||
const publicDomains = vaultMetadata?.publicEmailDomains ?? [];
|
||||
const privateDomains = vaultMetadata?.privateEmailDomains ?? [];
|
||||
|
||||
return [...publicDomains, ...privateDomains].some(supportedDomain =>
|
||||
domain === supportedDomain || domain.endsWith(`.${supportedDomain}`)
|
||||
);
|
||||
};
|
||||
|
||||
if (!isEmailDomainSupported(email)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{<EmailPreview email={email} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailBlock;
|
||||
@@ -1,58 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
|
||||
type HeaderBlockProps = {
|
||||
credential: Credential;
|
||||
onOpenNewPopup: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the header block.
|
||||
*/
|
||||
const HeaderBlock: React.FC<HeaderBlockProps> = ({ credential, onOpenNewPopup }) => (
|
||||
<div 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 break-all"
|
||||
>
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
const HeaderBlock: React.FC<HeaderBlockProps> = ({ credential }) => (
|
||||
<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-lg 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 break-all"
|
||||
>
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
)}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
|
||||
import { FormInputCopyToClipboard } from '@/entrypoints/popup/components/FormInputCopyToClipboard';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
type LoginCredentialsBlockProps = {
|
||||
credential: Credential;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { TotpCode } from '@/utils/types/TotpCode';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import type { TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
type TotpBlockProps = {
|
||||
credentialId: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import HeaderBlock from './HeaderBlock';
|
||||
import EmailBlock from './EmailBlock';
|
||||
import TotpBlock from './TotpBlock';
|
||||
import LoginCredentialsBlock from './LoginCredentialsBlock';
|
||||
import AliasBlock from './AliasBlock';
|
||||
import EmailBlock from './EmailBlock';
|
||||
import HeaderBlock from './HeaderBlock';
|
||||
import LoginCredentialsBlock from './LoginCredentialsBlock';
|
||||
import NotesBlock from './NotesBlock';
|
||||
import TotpBlock from './TotpBlock';
|
||||
|
||||
export {
|
||||
HeaderBlock,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { storage } from '#imports';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import type { ApiErrorResponse, MailboxEmail } from '@/utils/dist/shared/models/webapi';
|
||||
import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
type EmailPreviewProps = {
|
||||
email: string;
|
||||
@@ -19,6 +22,8 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastEmailId, setLastEmailId] = useState<number>(0);
|
||||
const [isSpamOk, setIsSpamOk] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSupportedDomain, setIsSupportedDomain] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
const dbContext = useDb();
|
||||
|
||||
@@ -31,14 +36,32 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return publicEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the email is a private domain.
|
||||
*/
|
||||
const isPrivateDomain = async (emailAddress: string): Promise<boolean> => {
|
||||
// Get metadata from storage
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[] ?? [];
|
||||
return privateEmailDomains.some(domain => emailAddress.toLowerCase().endsWith(domain));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the latest emails from the server and decrypts them locally if needed.
|
||||
*/
|
||||
const loadEmails = async (): Promise<void> => {
|
||||
try {
|
||||
setError(null);
|
||||
const isPublic = await isPublicDomain(email);
|
||||
const isPrivate = await isPrivateDomain(email);
|
||||
const isSupported = isPublic || isPrivate;
|
||||
|
||||
setIsSpamOk(isPublic);
|
||||
setIsSupportedDomain(isSupported);
|
||||
|
||||
if (!isSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPublic) {
|
||||
// For public domains (SpamOK), use the SpamOK API directly
|
||||
@@ -49,6 +72,12 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
'X-Asdasd-Platform-Version': AppInfo.VERSION,
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
setError('An error occurred while loading emails. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Only show the latest 2 emails to save space in UI
|
||||
@@ -62,32 +91,57 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
}
|
||||
|
||||
setEmails(latestMails);
|
||||
} else {
|
||||
} else if (isPrivate) {
|
||||
// For private domains, use existing encrypted email logic
|
||||
const response = await webApi.get(`EmailBox/${email}`);
|
||||
const data = response as { mails: MailboxEmail[] };
|
||||
try {
|
||||
/**
|
||||
* We use authFetch here because we don't want to the inner method to throw an error if HTTP status is not 200.
|
||||
* Instead we want to catch the error ourselves.
|
||||
*/
|
||||
const response = await webApi.authFetch(`EmailBox/${email}`, { method: 'GET' }, true, false);
|
||||
try {
|
||||
const data = response as { mails: MailboxEmail[] };
|
||||
|
||||
// Only show the latest 2 emails to save space in UI
|
||||
const latestMails = data.mails
|
||||
.toSorted((a, b) => new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime())
|
||||
.slice(0, 2);
|
||||
// Only show the latest 2 emails to save space in UI
|
||||
const latestMails = data.mails
|
||||
.toSorted((a, b) => new Date(b.dateSystem).getTime() - new Date(a.dateSystem).getTime())
|
||||
.slice(0, 2);
|
||||
|
||||
if (latestMails) {
|
||||
// Loop through all emails and decrypt them locally
|
||||
const decryptedEmails: MailboxEmail[] = await EncryptionUtility.decryptEmailList(
|
||||
latestMails,
|
||||
dbContext.sqliteClient!.getAllEncryptionKeys()
|
||||
);
|
||||
if (latestMails) {
|
||||
// Loop through all emails and decrypt them locally
|
||||
const decryptedEmails: MailboxEmail[] = await EncryptionUtility.decryptEmailList(
|
||||
latestMails,
|
||||
dbContext.sqliteClient!.getAllEncryptionKeys()
|
||||
);
|
||||
|
||||
if (loading && decryptedEmails.length > 0) {
|
||||
setLastEmailId(decryptedEmails[0].id);
|
||||
if (loading && decryptedEmails.length > 0) {
|
||||
setLastEmailId(decryptedEmails[0].id);
|
||||
}
|
||||
|
||||
setEmails(decryptedEmails);
|
||||
}
|
||||
} catch {
|
||||
// Try to parse as error response instead
|
||||
const apiErrorResponse = response as ApiErrorResponse;
|
||||
|
||||
if (apiErrorResponse?.code === 'CLAIM_DOES_NOT_MATCH_USER') {
|
||||
setError('The current chosen email address is already in use. Please change the email address by editing this credential.');
|
||||
} else if (apiErrorResponse?.code === 'CLAIM_DOES_NOT_EXIST') {
|
||||
setError('An error occurred while trying to load the emails. Please try to edit and save the credential entry to synchronize the database, then try again.');
|
||||
} else {
|
||||
setError('An error occurred while loading emails. Please try again later.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setEmails(decryptedEmails);
|
||||
} catch {
|
||||
setError('An error occurred while loading emails. Please try again later.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading emails:', err);
|
||||
setError('An unexpected error occurred while loading emails. Please try again later.');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
@@ -98,6 +152,24 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
return () : void => clearInterval(interval);
|
||||
}, [email, loading, webApi, dbContext]);
|
||||
|
||||
// Don't render anything if the domain is not supported
|
||||
if (!isSupportedDomain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">Recent emails</h2>
|
||||
</div>
|
||||
<div className="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded">
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-gray-500 dark:text-gray-400 mb-4">
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
/**
|
||||
* Button configuration for form input.
|
||||
*/
|
||||
type FormInputButton = {
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon component for form input buttons.
|
||||
*/
|
||||
const Icon: React.FC<{ name: string }> = ({ name }) => {
|
||||
switch (name) {
|
||||
case 'visibility':
|
||||
return (
|
||||
<>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</>
|
||||
);
|
||||
case 'visibility-off':
|
||||
return (
|
||||
<>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</>
|
||||
);
|
||||
case 'refresh':
|
||||
return (
|
||||
<>
|
||||
<path d="M23 4v6h-6" />
|
||||
<path d="M1 20v-6h6" />
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Form input props.
|
||||
*/
|
||||
type FormInputProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
type?: 'text' | 'password';
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
multiline?: boolean;
|
||||
rows?: number;
|
||||
error?: string;
|
||||
buttons?: FormInputButton[];
|
||||
showPassword?: boolean;
|
||||
onShowPasswordChange?: (show: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form input component.
|
||||
*/
|
||||
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
type = 'text',
|
||||
placeholder,
|
||||
required = false,
|
||||
multiline = false,
|
||||
rows = 1,
|
||||
error,
|
||||
buttons = [],
|
||||
showPassword: controlledShowPassword,
|
||||
onShowPasswordChange
|
||||
}, ref) => {
|
||||
const [internalShowPassword, setInternalShowPassword] = React.useState(false);
|
||||
|
||||
/**
|
||||
* Use controlled or uncontrolled showPassword state.
|
||||
* If controlledShowPassword is provided, use that value and call onShowPasswordChange.
|
||||
* Otherwise, use internal state.
|
||||
*/
|
||||
const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword;
|
||||
|
||||
/**
|
||||
* Set the showPassword state.
|
||||
* If controlledShowPassword is provided, use that value and call onShowPasswordChange.
|
||||
* Otherwise, use internal state.
|
||||
*/
|
||||
const setShowPassword = (value: boolean): void => {
|
||||
if (controlledShowPassword !== undefined) {
|
||||
onShowPasswordChange?.(value);
|
||||
} else {
|
||||
setInternalShowPassword(value);
|
||||
}
|
||||
};
|
||||
|
||||
const inputClasses = `mt-1 block w-full rounded-md ${
|
||||
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-700'
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm border focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400 py-2 px-3`;
|
||||
|
||||
// Add password visibility button if type is password
|
||||
const allButtons = type === 'password'
|
||||
? [...buttons, {
|
||||
icon: showPassword ? 'visibility-off' : 'visibility',
|
||||
/**
|
||||
* Toggle password visibility.
|
||||
*/
|
||||
onClick: (): void => setShowPassword(!showPassword),
|
||||
title: showPassword ? 'Hide password' : 'Show password'
|
||||
}]
|
||||
: buttons;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
<div className="relative">
|
||||
{multiline ? (
|
||||
<textarea
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={rows}
|
||||
placeholder={placeholder}
|
||||
className={inputClasses}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type === 'password' && !showPassword ? 'password' : 'text'}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={inputClasses}
|
||||
/>
|
||||
)}
|
||||
{allButtons.length > 0 && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{allButtons.map((button, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
onClick={button.onClick}
|
||||
title={button.title}
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name={button.icon} />
|
||||
</svg>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<p className="mt-1 text-sm text-red-500">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
FormInput.displayName = 'FormInput';
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { ClipboardCopyService } from '@/entrypoints/popup/utils/ClipboardCopyService';
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,43 @@ type FormInputCopyToClipboardProps = {
|
||||
|
||||
const clipboardService = new ClipboardCopyService();
|
||||
|
||||
/**
|
||||
* Icon component for form input buttons.
|
||||
*/
|
||||
const Icon: React.FC<{ name: string }> = ({ name }) => {
|
||||
switch (name) {
|
||||
case 'visibility':
|
||||
return (
|
||||
<>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</>
|
||||
);
|
||||
case 'visibility-off':
|
||||
return (
|
||||
<>
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
</>
|
||||
);
|
||||
case 'copy':
|
||||
return (
|
||||
<>
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
||||
</>
|
||||
);
|
||||
case 'check':
|
||||
return (
|
||||
<>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Form input copy to clipboard component.
|
||||
*/
|
||||
@@ -70,17 +108,38 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
|
||||
} text-gray-900 sm:text-sm rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400`}
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{copied && (
|
||||
<span className="text-green-500 dark:text-green-400">
|
||||
Copied!
|
||||
</span>
|
||||
{copied ? (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-green-500 dark:text-green-400 transition-colors duration-200"
|
||||
title="Copied!"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name="check" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyToClipboard}
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name="copy" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{type === 'password' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
className="p-1 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors duration-200"
|
||||
title={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<Icon name={showPassword ? 'visibility-off' : 'visibility'} />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
|
||||
type HeaderButtonProps = {
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
iconType: HeaderIconType;
|
||||
variant?: 'default' | 'primary' | 'danger';
|
||||
};
|
||||
|
||||
/**
|
||||
* Header button component for consistent header button styling
|
||||
*/
|
||||
const HeaderButton: React.FC<HeaderButtonProps> = ({
|
||||
onClick,
|
||||
title,
|
||||
iconType,
|
||||
variant = 'default'
|
||||
}) => {
|
||||
const colorClasses = {
|
||||
default: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700',
|
||||
primary: 'text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 hover:bg-primary-100 dark:hover:bg-primary-900/20',
|
||||
danger: 'text-red-500 hover:text-red-600 hover:bg-red-100 dark:hover:bg-red-900/20'
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`p-2 rounded-lg ${colorClasses[variant]}`}
|
||||
title={title}
|
||||
>
|
||||
<HeaderIcon type={iconType} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderButton;
|
||||
@@ -0,0 +1,163 @@
|
||||
import React from 'react';
|
||||
|
||||
export enum HeaderIconType {
|
||||
EXPAND = 'expand',
|
||||
EDIT = 'edit',
|
||||
DELETE = 'delete',
|
||||
SETTINGS = 'settings',
|
||||
RELOAD = 'reload',
|
||||
EXTERNAL_LINK = 'external_link',
|
||||
SAVE = 'save',
|
||||
PLUS = 'plus'
|
||||
}
|
||||
|
||||
type HeaderIconProps = {
|
||||
type: HeaderIconType;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to render header icons
|
||||
*/
|
||||
export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h-5' }) => {
|
||||
const icons = {
|
||||
[HeaderIconType.EXPAND]: (
|
||||
<svg
|
||||
className={className}
|
||||
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>
|
||||
),
|
||||
[HeaderIconType.EDIT]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.DELETE]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.SETTINGS]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.RELOAD]: (
|
||||
<svg
|
||||
className={className}
|
||||
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 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.EXTERNAL_LINK]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.SAVE]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 3H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V7l-4-4z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 3v5h10"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 12a2 2 0 100 4 2 2 0 000-4z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[HeaderIconType.PLUS]: (
|
||||
<svg
|
||||
className={className}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
return icons[type] || null;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
@@ -17,9 +18,13 @@ const BottomNav: React.FC = () => {
|
||||
|
||||
// Add effect to update currentTab based on route
|
||||
useEffect(() => {
|
||||
const path = location.pathname.substring(1) as TabName;
|
||||
if (['credentials', 'emails', 'settings'].includes(path)) {
|
||||
setCurrentTab(path);
|
||||
const path = location.pathname.substring(1); // Remove leading slash
|
||||
const tabNames: TabName[] = ['credentials', 'emails', 'settings'];
|
||||
|
||||
// Find the first tab name that matches the start of the path
|
||||
const matchingTab = tabNames.find(tab => path === tab || path.startsWith(`${tab}/`));
|
||||
if (matchingTab) {
|
||||
setCurrentTab(matchingTab);
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { storage } from '#imports';
|
||||
import { UserMenu } from '@/entrypoints/popup/components/Layout/UserMenu';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
/**
|
||||
* Header props.
|
||||
@@ -14,31 +12,20 @@ type HeaderProps = {
|
||||
showBackButton?: boolean;
|
||||
title?: string;
|
||||
}[];
|
||||
rightButtons?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header component.
|
||||
*/
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
routes = []
|
||||
routes = [],
|
||||
rightButtons
|
||||
}) => {
|
||||
const authContext = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
};
|
||||
|
||||
// Updated route matching logic to handle URL parameters
|
||||
const currentRoute = routes?.find(route => {
|
||||
// Convert route pattern to regex
|
||||
@@ -105,33 +92,22 @@ const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
<div className="flex-grow" />
|
||||
|
||||
<div className="flex items-center">
|
||||
{!currentRoute?.showBackButton ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{!authContext.isLoggedIn ? (
|
||||
<button
|
||||
onClick={openClientTab}
|
||||
className="p-2"
|
||||
id="settings"
|
||||
onClick={(handleSettings)}
|
||||
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
|
||||
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
|
||||
<span className="sr-only">Settings</span>
|
||||
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (<></>)}
|
||||
) : (
|
||||
rightButtons
|
||||
)}
|
||||
</div>
|
||||
{!authContext.isLoggedIn ? (
|
||||
<button
|
||||
id="settings"
|
||||
onClick={(handleSettings)}
|
||||
className="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<span className="sr-only">Settings</span>
|
||||
<svg className="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<UserMenu />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -1,88 +1,49 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
/**
|
||||
* User menu component.
|
||||
*/
|
||||
export const UserMenu: React.FC = () => {
|
||||
const UserMenu: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const navigate = useNavigate();
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Handle clicking outside the user menu.
|
||||
*/
|
||||
const handleClickOutside = (event: MouseEvent) : void => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
buttonRef.current &&
|
||||
!menuRef.current.contains(event.target as Node) &&
|
||||
!buttonRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () : void => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Toggle the user menu.
|
||||
* Handle logout.
|
||||
*/
|
||||
const toggleUserMenu = () : void => {
|
||||
setIsUserMenuOpen(!isUserMenuOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logging out.
|
||||
*/
|
||||
const onLogout = async () : Promise<void> => {
|
||||
showLoading();
|
||||
navigate('/logout', { replace: true });
|
||||
hideLoading();
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
await authContext.logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={toggleUserMenu}
|
||||
className="flex items-center text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
<span className="sr-only">Open menu</span>
|
||||
<svg className="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isUserMenuOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="absolute right-0 z-50 mt-2 w-48 py-1 bg-white rounded-lg shadow-lg dark:bg-gray-700 border border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-600">
|
||||
<span className="block text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{authContext.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:text-red-400 dark:hover:bg-gray-600"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Logged in
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { storage } from '#imports';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Component for displaying the login server information.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
|
||||
interface IModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
variant?: 'danger' | 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable modal component for confirmations and alerts.
|
||||
*/
|
||||
const Modal: React.FC<IModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
confirmText = 'Confirm',
|
||||
cancelText = 'Cancel',
|
||||
variant = 'default'
|
||||
}) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const confirmButtonClass = variant === 'danger'
|
||||
? 'bg-red-600 hover:bg-red-700 focus:ring-red-500'
|
||||
: 'bg-primary-600 hover:bg-primary-700 focus:ring-primary-500';
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black bg-opacity-60 transition-opacity" onClick={onClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all w-full max-w-lg">
|
||||
{/* Close button */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="sm:flex sm:items-start">
|
||||
{variant === 'danger' && (
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<svg className="h-6 w-6 text-red-600 dark:text-red-200" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${confirmButtonClass}`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 sm:mt-0 sm:w-auto"
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
@@ -1,13 +1,17 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { storage } from '#imports';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
type AuthContextType = {
|
||||
isLoggedIn: boolean;
|
||||
isInitialized: boolean;
|
||||
username: string | null;
|
||||
initializeAuth: () => Promise<{ isLoggedIn: boolean }>;
|
||||
setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise<void>;
|
||||
login: () => Promise<void>;
|
||||
logout: (errorMessage?: string) => Promise<void>;
|
||||
@@ -31,25 +35,29 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const dbContext = useDb();
|
||||
|
||||
/**
|
||||
* Check for tokens in browser local storage on initial load.
|
||||
* Initialize the authentication state.
|
||||
*
|
||||
* @returns object containing whether the user is logged in.
|
||||
*/
|
||||
const initializeAuth = useCallback(async () : Promise<{ isLoggedIn: boolean }> => {
|
||||
const accessToken = await storage.getItem('local:accessToken') as string;
|
||||
const refreshToken = await storage.getItem('local:refreshToken') as string;
|
||||
const username = await storage.getItem('local:username') as string;
|
||||
if (accessToken && refreshToken && username) {
|
||||
setUsername(username);
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
setIsInitialized(true);
|
||||
|
||||
return { isLoggedIn };
|
||||
}, [setUsername, setIsLoggedIn, isLoggedIn]);
|
||||
|
||||
/**
|
||||
* Check for tokens in browser local storage on initial load when this context is mounted.
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Initialize the authentication state.
|
||||
*/
|
||||
const initializeAuth = async () : Promise<void> => {
|
||||
const accessToken = await storage.getItem('local:accessToken') as string;
|
||||
const refreshToken = await storage.getItem('local:refreshToken') as string;
|
||||
const username = await storage.getItem('local:username') as string;
|
||||
if (accessToken && refreshToken && username) {
|
||||
setUsername(username);
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
}, []);
|
||||
}, [initializeAuth]);
|
||||
|
||||
/**
|
||||
* Set auth tokens in browser local storage as part of the login process. After db is initialized, the login method should be called as well.
|
||||
@@ -100,12 +108,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
isLoggedIn,
|
||||
isInitialized,
|
||||
username,
|
||||
initializeAuth,
|
||||
setAuthTokens,
|
||||
login,
|
||||
logout,
|
||||
globalMessage,
|
||||
clearGlobalMessage,
|
||||
}), [isLoggedIn, isInitialized, username, globalMessage, setAuthTokens, login, logout, clearGlobalMessage]);
|
||||
}), [isLoggedIn, isInitialized, username, initializeAuth, globalMessage, setAuthTokens, login, logout, clearGlobalMessage]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
|
||||
import type { VaultMetadata } from '@/utils/dist/shared/models/metadata';
|
||||
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
|
||||
import SqliteClient from '@/utils/SqliteClient';
|
||||
import { StoreVaultRequest } from '@/utils/types/messaging/StoreVaultRequest';
|
||||
import type { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
|
||||
|
||||
type DbContextType = {
|
||||
sqliteClient: SqliteClient | null;
|
||||
@@ -11,9 +14,8 @@ type DbContextType = {
|
||||
dbAvailable: boolean;
|
||||
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<void>;
|
||||
clearDatabase: () => void;
|
||||
vaultRevision: number;
|
||||
publicEmailDomains: string[];
|
||||
privateEmailDomains: string[];
|
||||
getVaultMetadata: () => Promise<VaultMetadata | null>;
|
||||
setCurrentVaultRevisionNumber: (revisionNumber: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const DbContext = createContext<DbContextType | undefined>(undefined);
|
||||
@@ -37,20 +39,10 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
*/
|
||||
const [dbAvailable, setDbAvailable] = useState(false);
|
||||
|
||||
/**
|
||||
* Public email domains.
|
||||
*/
|
||||
const [publicEmailDomains, setPublicEmailDomains] = useState<string[]>([]);
|
||||
|
||||
/**
|
||||
* Vault revision.
|
||||
*/
|
||||
const [vaultRevision, setVaultRevision] = useState(0);
|
||||
|
||||
/**
|
||||
* Private email domains.
|
||||
*/
|
||||
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
|
||||
const [vaultMetadata, setVaultMetadata] = useState<VaultMetadata | null>(null);
|
||||
|
||||
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string) => {
|
||||
// Attempt to decrypt the blob.
|
||||
@@ -66,17 +58,24 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
setPublicEmailDomains(vaultResponse.vault.publicEmailDomainList);
|
||||
setPrivateEmailDomains(vaultResponse.vault.privateEmailDomainList);
|
||||
setVaultRevision(vaultResponse.vault.currentRevisionNumber);
|
||||
setVaultMetadata({
|
||||
publicEmailDomains: vaultResponse.vault.publicEmailDomainList,
|
||||
privateEmailDomains: vaultResponse.vault.privateEmailDomainList,
|
||||
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
|
||||
});
|
||||
|
||||
/*
|
||||
/**
|
||||
* Store encrypted vault in background worker.
|
||||
*/
|
||||
sendMessage('STORE_VAULT', {
|
||||
const request: StoreVaultRequest = {
|
||||
vaultBlob: vaultResponse.vault.blob,
|
||||
derivedKey: derivedKey,
|
||||
vaultResponse: vaultResponse,
|
||||
}, 'background');
|
||||
publicEmailDomainList: vaultResponse.vault.publicEmailDomainList,
|
||||
privateEmailDomainList: vaultResponse.vault.privateEmailDomainList,
|
||||
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
|
||||
};
|
||||
|
||||
await sendMessage('STORE_VAULT', request, 'background');
|
||||
}, []);
|
||||
|
||||
const checkStoredVault = useCallback(async () => {
|
||||
@@ -89,9 +88,11 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
setPublicEmailDomains(response.publicEmailDomains ?? []);
|
||||
setPrivateEmailDomains(response.privateEmailDomains ?? []);
|
||||
setVaultRevision(response.vaultRevisionNumber ?? 0);
|
||||
setVaultMetadata({
|
||||
publicEmailDomains: response.publicEmailDomains ?? [],
|
||||
privateEmailDomains: response.privateEmailDomains ?? [],
|
||||
vaultRevisionNumber: response.vaultRevisionNumber ?? 0,
|
||||
});
|
||||
} else {
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(false);
|
||||
@@ -103,6 +104,24 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get the vault metadata.
|
||||
*/
|
||||
const getVaultMetadata = useCallback(async () : Promise<VaultMetadata | null> => {
|
||||
return vaultMetadata;
|
||||
}, [vaultMetadata]);
|
||||
|
||||
/**
|
||||
* Set the current vault revision number.
|
||||
*/
|
||||
const setCurrentVaultRevisionNumber = useCallback(async (revisionNumber: number) => {
|
||||
setVaultMetadata({
|
||||
publicEmailDomains: vaultMetadata?.publicEmailDomains ?? [],
|
||||
privateEmailDomains: vaultMetadata?.privateEmailDomains ?? [],
|
||||
vaultRevisionNumber: revisionNumber,
|
||||
});
|
||||
}, [vaultMetadata]);
|
||||
|
||||
/**
|
||||
* Check if database is initialized and try to retrieve vault from background
|
||||
*/
|
||||
@@ -127,10 +146,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
dbAvailable,
|
||||
initializeDatabase,
|
||||
clearDatabase,
|
||||
vaultRevision,
|
||||
publicEmailDomains,
|
||||
privateEmailDomains
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, vaultRevision, publicEmailDomains, privateEmailDomains]);
|
||||
getVaultMetadata,
|
||||
setCurrentVaultRevisionNumber,
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata, setCurrentVaultRevisionNumber]);
|
||||
|
||||
return (
|
||||
<DbContext.Provider value={contextValue}>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useMemo } from "react";
|
||||
|
||||
type HeaderButtonsContextType = {
|
||||
setHeaderButtons: (buttons: React.ReactNode) => void;
|
||||
headerButtons: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for managing header buttons in the popup
|
||||
*/
|
||||
export const HeaderButtonsContext = createContext<HeaderButtonsContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Provider component for HeaderButtonsContext
|
||||
*/
|
||||
export const HeaderButtonsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [headerButtons, setHeaderButtons] = useState<React.ReactNode>(null);
|
||||
|
||||
const handleSetHeaderButtons = useCallback((buttons: React.ReactNode) => {
|
||||
setHeaderButtons(buttons);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
setHeaderButtons: handleSetHeaderButtons,
|
||||
headerButtons
|
||||
}), [handleSetHeaderButtons, headerButtons]);
|
||||
|
||||
return (
|
||||
<HeaderButtonsContext.Provider value={value}>
|
||||
{children}
|
||||
</HeaderButtonsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to use the HeaderButtonsContext
|
||||
* @returns The HeaderButtonsContext value
|
||||
*/
|
||||
export const useHeaderButtons = (): {
|
||||
setHeaderButtons: (buttons: React.ReactNode) => void;
|
||||
headerButtons: React.ReactNode;
|
||||
} => {
|
||||
const context = useContext(HeaderButtonsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useHeaderButtons must be used within a HeaderButtonsProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { createContext, useContext, useState, useMemo } from 'react';
|
||||
|
||||
import LoadingSpinnerFullScreen from '@/entrypoints/popup/components/LoadingSpinnerFullScreen';
|
||||
|
||||
type LoadingContextType = {
|
||||
isLoading: boolean;
|
||||
showLoading: () => void;
|
||||
loadingMessage?: string;
|
||||
showLoading: (message?: string) => void;
|
||||
hideLoading: () => void;
|
||||
isInitialLoading: boolean;
|
||||
setIsInitialLoading: (isInitialLoading: boolean) => void;
|
||||
@@ -29,31 +31,39 @@ export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ child
|
||||
* Loading state that can be used by other components during normal operation.
|
||||
*/
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadingMessage, setLoadingMessage] = useState<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Show loading spinner
|
||||
* Show loading spinner with optional message
|
||||
*/
|
||||
const showLoading = (): void => setIsLoading(true);
|
||||
const showLoading = (message?: string): void => {
|
||||
setIsLoading(true);
|
||||
setLoadingMessage(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide loading spinner
|
||||
* Hide loading spinner and clear message
|
||||
*/
|
||||
const hideLoading = (): void => setIsLoading(false);
|
||||
const hideLoading = (): void => {
|
||||
setIsLoading(false);
|
||||
setLoadingMessage(undefined);
|
||||
};
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
isInitialLoading,
|
||||
setIsInitialLoading,
|
||||
}),
|
||||
[isLoading, isInitialLoading]
|
||||
[isLoading, loadingMessage, isInitialLoading]
|
||||
);
|
||||
|
||||
return (
|
||||
<LoadingContext.Provider value={value}>
|
||||
<LoadingSpinnerFullScreen />
|
||||
<LoadingSpinnerFullScreen message={loadingMessage} />
|
||||
{children}
|
||||
</LoadingContext.Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
const LAST_VISITED_PAGE_KEY = 'session:lastVisitedPage';
|
||||
const LAST_VISITED_TIME_KEY = 'session:lastVisitedTime';
|
||||
const NAVIGATION_HISTORY_KEY = 'session:navigationHistory';
|
||||
const PAGE_MEMORY_DURATION = 120 * 1000; // 2 minutes in milliseconds
|
||||
|
||||
type NavigationHistoryEntry = {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
type NavigationContextType = {
|
||||
storeCurrentPage: () => Promise<void>;
|
||||
restoreLastPage: () => Promise<void>;
|
||||
isFullyInitialized: boolean;
|
||||
requiresAuth: boolean;
|
||||
};
|
||||
|
||||
const NavigationContext = createContext<NavigationContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Navigation provider component that handles storing and restoring the last visited page,
|
||||
* as well as managing initialization and auth state redirects.
|
||||
*/
|
||||
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
// Auth and DB state
|
||||
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
|
||||
const { dbInitialized, dbAvailable } = useDb();
|
||||
|
||||
// Derived state
|
||||
const isFullyInitialized = authInitialized && dbInitialized;
|
||||
const requiresAuth = isFullyInitialized && (!isLoggedIn || !dbAvailable || isInlineUnlockMode);
|
||||
|
||||
/**
|
||||
* Store the current page path, timestamp, and navigation history in storage.
|
||||
*/
|
||||
const storeCurrentPage = useCallback(async (): Promise<void> => {
|
||||
// Pages that are not allowed to be stored as these are auth conditional pages.
|
||||
const notAllowedPaths = ['/', '/login', '/unlock', '/unlock-success', '/auth-settings'];
|
||||
|
||||
// Only store the page if we're fully initialized and don't need auth
|
||||
if (isFullyInitialized && !requiresAuth && !notAllowedPaths.includes(location.pathname)) {
|
||||
// Split the path into segments and build up the history
|
||||
const segments = location.pathname.split('/').filter(Boolean);
|
||||
const historyEntries: NavigationHistoryEntry[] = [];
|
||||
|
||||
let currentPath = '';
|
||||
for (const segment of segments) {
|
||||
currentPath += '/' + segment;
|
||||
historyEntries.push({
|
||||
pathname: currentPath,
|
||||
search: location.search,
|
||||
hash: location.hash,
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
storage.setItem(LAST_VISITED_PAGE_KEY, location.pathname),
|
||||
storage.setItem(LAST_VISITED_TIME_KEY, Date.now()),
|
||||
storage.setItem(NAVIGATION_HISTORY_KEY, historyEntries),
|
||||
]);
|
||||
}
|
||||
}, [location, isFullyInitialized, requiresAuth]);
|
||||
|
||||
/**
|
||||
* Restore the last visited page and navigation history if it was visited within the memory duration.
|
||||
*/
|
||||
const restoreLastPage = useCallback(async (): Promise<void> => {
|
||||
// Only restore if we're fully initialized and don't need auth
|
||||
if (!isFullyInitialized || requiresAuth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lastPage, lastVisitTime, savedHistory] = await Promise.all([
|
||||
storage.getItem(LAST_VISITED_PAGE_KEY) as Promise<string>,
|
||||
storage.getItem(LAST_VISITED_TIME_KEY) as Promise<number>,
|
||||
storage.getItem(NAVIGATION_HISTORY_KEY) as Promise<NavigationHistoryEntry[]>,
|
||||
]);
|
||||
|
||||
if (lastPage && lastVisitTime) {
|
||||
const timeSinceLastVisit = Date.now() - lastVisitTime;
|
||||
if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
|
||||
// Restore the navigation history
|
||||
if (savedHistory?.length) {
|
||||
// First navigate to credentials page as the base
|
||||
navigate('/credentials', { replace: true });
|
||||
|
||||
// Then restore the history stack
|
||||
for (const entry of savedHistory) {
|
||||
navigate(entry.pathname + entry.search + entry.hash);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to simple navigation if no history
|
||||
navigate('/credentials', { replace: true });
|
||||
navigate(lastPage, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Duration has expired, clear all stored navigation data
|
||||
await Promise.all([
|
||||
storage.removeItem(LAST_VISITED_PAGE_KEY),
|
||||
storage.removeItem(LAST_VISITED_TIME_KEY),
|
||||
storage.removeItem(NAVIGATION_HISTORY_KEY),
|
||||
sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background'),
|
||||
]);
|
||||
|
||||
// Navigate to the credentials page as default entry page.
|
||||
navigate('/credentials', { replace: true });
|
||||
}, [navigate, isFullyInitialized, requiresAuth]);
|
||||
|
||||
// Handle initialization and auth state changes
|
||||
useEffect(() => {
|
||||
// Check for inline unlock mode
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
|
||||
setIsInlineUnlockMode(inlineUnlock);
|
||||
|
||||
if (isFullyInitialized) {
|
||||
setIsInitialLoading(false);
|
||||
|
||||
if (requiresAuth) {
|
||||
const allowedPaths = ['/login', '/unlock', '/unlock-success', '/auth-settings'];
|
||||
if (allowedPaths.includes(location.pathname)) {
|
||||
// Do not override the navigation if the current path is in the allowed paths.
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine which auth page to show
|
||||
if (!isLoggedIn) {
|
||||
navigate('/login', { replace: true });
|
||||
} else if (!dbAvailable) {
|
||||
navigate('/unlock', { replace: true });
|
||||
} else if (inlineUnlock) {
|
||||
navigate('/unlock-success', { replace: true });
|
||||
}
|
||||
} else if (!isInitialized) {
|
||||
// First initialization, try to restore last page or go to credentials
|
||||
restoreLastPage().then(() => {
|
||||
setIsInitialized(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isFullyInitialized, requiresAuth, isLoggedIn, dbAvailable, isInitialized, navigate, restoreLastPage, setIsInitialLoading, location.pathname]);
|
||||
|
||||
// Store the current page whenever it changes
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
storeCurrentPage();
|
||||
}
|
||||
}, [location.pathname, location.search, location.hash, isInitialized, storeCurrentPage]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
storeCurrentPage,
|
||||
restoreLastPage,
|
||||
isFullyInitialized,
|
||||
requiresAuth
|
||||
}), [storeCurrentPage, restoreLastPage, isFullyInitialized, requiresAuth]);
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to access the navigation context.
|
||||
* @returns The navigation context
|
||||
*/
|
||||
export const useNavigation = (): NavigationContextType => {
|
||||
const context = useContext(NavigationContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useNavigation must be used within a NavigationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { createContext, useContext, useState, useMemo, useEffect, useCallback } from 'react';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
const WebApiContext = createContext<WebApiService | null>(null);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
|
||||
import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
import { UploadVaultRequest } from '@/utils/types/messaging/UploadVaultRequest';
|
||||
import { VaultUploadResponse as messageVaultUploadResponse } from '@/utils/types/messaging/VaultUploadResponse';
|
||||
|
||||
type VaultMutationOptions = {
|
||||
onSuccess?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to execute a vault mutation.
|
||||
*/
|
||||
export function useVaultMutate() : {
|
||||
executeVaultMutation: (operation: () => Promise<void>, options?: VaultMutationOptions) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
syncStatus: string;
|
||||
} {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [syncStatus, setSyncStatus] = useState('Syncing vault');
|
||||
const dbContext = useDb();
|
||||
const { syncVault } = useVaultSync();
|
||||
|
||||
/**
|
||||
* Execute the provided operation (e.g. create/update/delete credential)
|
||||
*/
|
||||
const executeMutateOperation = useCallback(async (
|
||||
operation: () => Promise<void>,
|
||||
options: VaultMutationOptions
|
||||
) : Promise<void> => {
|
||||
setSyncStatus('Saving changes to vault');
|
||||
|
||||
// Execute the provided operation (e.g. create/update/delete credential)
|
||||
await operation();
|
||||
|
||||
setSyncStatus('Uploading vault to server');
|
||||
|
||||
try {
|
||||
// Upload the updated vault to the server.
|
||||
const base64Vault = dbContext.sqliteClient!.exportToBase64();
|
||||
|
||||
// Get derived key from background worker
|
||||
const derivedKey = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
|
||||
// Encrypt the vault.
|
||||
const encryptedVaultBlob = await EncryptionUtility.symmetricEncrypt(
|
||||
base64Vault,
|
||||
derivedKey
|
||||
);
|
||||
|
||||
const request: UploadVaultRequest = {
|
||||
vaultBlob: encryptedVaultBlob,
|
||||
};
|
||||
|
||||
const response = await sendMessage('UPLOAD_VAULT', request, 'background') as messageVaultUploadResponse;
|
||||
|
||||
/*
|
||||
* If we get here, it means we have a valid connection to the server.
|
||||
* TODO: offline mode is not implemented for browser extension yet.
|
||||
* authContext.setOfflineMode(false);
|
||||
*/
|
||||
|
||||
if (response.status === 0 && response.newRevisionNumber) {
|
||||
await dbContext.setCurrentVaultRevisionNumber(response.newRevisionNumber);
|
||||
options.onSuccess?.();
|
||||
} else if (response.status === 1) {
|
||||
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
|
||||
} else {
|
||||
throw new Error('Failed to upload vault to server');
|
||||
}
|
||||
} catch (error) {
|
||||
// Check if it's a network error
|
||||
if (error instanceof Error && (error.message.includes('network') || error.message.includes('timeout'))) {
|
||||
/*
|
||||
* Network error, mark as offline and track pending changes
|
||||
* TODO: offline mode is not implemented for browser extension yet.
|
||||
* authContext.setOfflineMode(true);
|
||||
*/
|
||||
options.onError?.(new Error('Network error'));
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}, [dbContext]);
|
||||
|
||||
/**
|
||||
* Hook to execute a vault mutation which uploads a new encrypted vault to the server
|
||||
*/
|
||||
const executeVaultMutation = useCallback(async (
|
||||
operation: () => Promise<void>,
|
||||
options: VaultMutationOptions = {}
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setSyncStatus('Checking for vault updates');
|
||||
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle the status update.
|
||||
*/
|
||||
onStatus: (message) => setSyncStatus(message),
|
||||
/**
|
||||
* Handle successful vault sync and continue with vault mutation.
|
||||
*/
|
||||
onSuccess: async (hasNewVault) => {
|
||||
if (hasNewVault) {
|
||||
// Vault was changed, but has now been reloaded so we can continue with the operation.
|
||||
}
|
||||
await executeMutateOperation(operation, options);
|
||||
},
|
||||
/**
|
||||
* Handle error during vault sync.
|
||||
*/
|
||||
onError: (error) => {
|
||||
/**
|
||||
*Toast.show({
|
||||
*type: 'error',
|
||||
*text1: 'Failed to sync vault',
|
||||
*text2: error,
|
||||
*position: 'bottom'
|
||||
*});
|
||||
*/
|
||||
options.onError?.(new Error(error));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during vault mutation:', error);
|
||||
/*
|
||||
* Toast.show({
|
||||
*type: 'error',
|
||||
*text1: 'Operation failed',
|
||||
*text2: error instanceof Error ? error.message : 'Unknown error',
|
||||
*position: 'bottom'
|
||||
*});
|
||||
*/
|
||||
options.onError?.(error instanceof Error ? error : new Error('Unknown error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setSyncStatus('');
|
||||
}
|
||||
}, [syncVault, executeMutateOperation]);
|
||||
|
||||
return {
|
||||
executeVaultMutation,
|
||||
isLoading,
|
||||
syncStatus,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useCallback } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
|
||||
/**
|
||||
* Utility function to ensure a minimum time has elapsed for an operation
|
||||
*/
|
||||
const withMinimumDelay = async <T>(
|
||||
operation: () => Promise<T>,
|
||||
minDelayMs: number,
|
||||
enableDelay: boolean = true
|
||||
): Promise<T> => {
|
||||
if (!enableDelay) {
|
||||
// If delay is disabled, return the result immediately.
|
||||
return operation();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await operation();
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
|
||||
if (elapsedTime < minDelayMs) {
|
||||
await new Promise(resolve => setTimeout(resolve, minDelayMs - elapsedTime));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
type VaultSyncOptions = {
|
||||
initialSync?: boolean;
|
||||
onSuccess?: (hasNewVault: boolean) => void;
|
||||
onError?: (error: string) => void;
|
||||
onStatus?: (message: string) => void;
|
||||
_onOffline?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to sync the vault with the server.
|
||||
*/
|
||||
export const useVaultSync = () : {
|
||||
syncVault: (options?: VaultSyncOptions) => Promise<boolean>;
|
||||
} => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
|
||||
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
|
||||
const { initialSync = false, onSuccess, onError, onStatus, _onOffline } = options;
|
||||
|
||||
// For the initial sync, we add an artifical delay to various steps which makes it feel more fluid.
|
||||
const enableDelay = initialSync;
|
||||
|
||||
try {
|
||||
const { isLoggedIn } = await authContext.initializeAuth();
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// Not authenticated, return false immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check app status and vault revision
|
||||
onStatus?.('Checking vault updates');
|
||||
const statusResponse = await withMinimumDelay(() => webApi.getStatus(), 300, enableDelay);
|
||||
|
||||
// Check if server is actually available, 0.0.0 indicates connection error which triggers offline mode.
|
||||
if (statusResponse.serverVersion === '0.0.0') {
|
||||
// Offline mode is not implemented for browser extension yet, let it fail below due to the validateStatusResponse check.
|
||||
}
|
||||
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError) {
|
||||
onError?.(statusError);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* If we get here, it means we have a valid connection to the server.
|
||||
* TODO: browser extension does not support offline mode yet.
|
||||
* authContext.setOfflineMode(false);
|
||||
*/
|
||||
|
||||
// Compare vault revisions
|
||||
const vaultMetadata = await dbContext.getVaultMetadata();
|
||||
const vaultRevisionNumber = vaultMetadata?.vaultRevisionNumber ?? 0;
|
||||
|
||||
if (statusResponse.vaultRevision > vaultRevisionNumber) {
|
||||
onStatus?.('Syncing updated vault');
|
||||
const vaultResponseJson = await withMinimumDelay(() => webApi.get<VaultResponse>('Vault'), 1000, enableDelay);
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse);
|
||||
if (vaultError) {
|
||||
// Only logout if it's an authentication error, not a network error
|
||||
if (vaultError.includes('authentication') || vaultError.includes('unauthorized')) {
|
||||
await webApi.logout(vaultError);
|
||||
onError?.(vaultError);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: browser extension does not support offline mode yet.
|
||||
* For other errors, go into offline mode
|
||||
* authContext.setOfflineMode(true);
|
||||
*/
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get derived key from background worker
|
||||
const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, passwordHashBase64);
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
} catch {
|
||||
// Vault could not be decrypted, throw an error
|
||||
throw new Error('Vault could not be decrypted, if problem persists please logout and login again.');
|
||||
}
|
||||
}
|
||||
|
||||
await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay);
|
||||
return false;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync';
|
||||
console.error('Vault sync error:', err);
|
||||
|
||||
/*
|
||||
* Check if it's a network error
|
||||
* TODO: browser extension does not support offline mode yet.
|
||||
*/
|
||||
/*
|
||||
* if (errorMessage.includes('network') || errorMessage.includes('timeout')) {
|
||||
*authContext.setOfflineMode(true);
|
||||
*return true;
|
||||
*}
|
||||
*/
|
||||
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
}, [authContext, dbContext, webApi]);
|
||||
|
||||
return { syncVault };
|
||||
};
|
||||
@@ -1,14 +1,12 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import App from '@/entrypoints/popup/App';
|
||||
import { AuthProvider } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { WebApiProvider } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { DbProvider } from '@/entrypoints/popup/context/DbContext';
|
||||
import { HeaderButtonsProvider } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { LoadingProvider } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { ThemeProvider } from '@/entrypoints/popup/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();
|
||||
import { WebApiProvider } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
@@ -16,9 +14,11 @@ root.render(
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<LoadingProvider>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
<HeaderButtonsProvider>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</HeaderButtonsProvider>
|
||||
</LoadingProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { storage } from '#imports';
|
||||
import { GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
type ApiOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
@@ -13,6 +18,36 @@ const DEFAULT_OPTIONS: ApiOption[] = [
|
||||
{ label: 'Self-hosted', value: 'custom' }
|
||||
];
|
||||
|
||||
// Validation schema for URLs
|
||||
const urlSchema = Yup.object().shape({
|
||||
apiUrl: Yup.string()
|
||||
.required('API URL is required')
|
||||
.test('is-valid-api-url', 'Please enter a valid API URL', (value: string | undefined) => {
|
||||
if (!value) {
|
||||
return true; // Allow empty for non-custom option
|
||||
}
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
clientUrl: Yup.string()
|
||||
.required('Client URL is required')
|
||||
.test('is-valid-client-url', 'Please enter a valid client URL', (value: string | undefined) => {
|
||||
if (!value) {
|
||||
return true; // Allow empty for non-custom option
|
||||
}
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Auth settings page only shown when user is not logged in.
|
||||
*/
|
||||
@@ -21,6 +56,8 @@ const AuthSettings: React.FC = () => {
|
||||
const [customUrl, setCustomUrl] = useState<string>('');
|
||||
const [customClientUrl, setCustomClientUrl] = useState<string>('');
|
||||
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
|
||||
const [errors, setErrors] = useState<{ apiUrl?: string; clientUrl?: string }>({});
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -49,10 +86,11 @@ const AuthSettings: React.FC = () => {
|
||||
} else {
|
||||
setSelectedOption(DEFAULT_OPTIONS[0].value);
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
loadStoredSettings();
|
||||
}, []);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Handle option change
|
||||
@@ -63,6 +101,9 @@ const AuthSettings: React.FC = () => {
|
||||
if (value !== 'custom') {
|
||||
await storage.setItem('local:apiUrl', '');
|
||||
await storage.setItem('local:clientUrl', '');
|
||||
setCustomUrl('');
|
||||
setCustomClientUrl('');
|
||||
setErrors({});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -72,17 +113,37 @@ const AuthSettings: React.FC = () => {
|
||||
const handleCustomUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setCustomUrl(value);
|
||||
await storage.setItem('local:apiUrl', value);
|
||||
|
||||
try {
|
||||
await urlSchema.validateAt('apiUrl', { apiUrl: value });
|
||||
setErrors(prev => ({ ...prev, apiUrl: undefined }));
|
||||
await storage.setItem('local:apiUrl', value);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Yup.ValidationError) {
|
||||
setErrors(prev => ({ ...prev, apiUrl: error.message }));
|
||||
// On error we revert back to the aliasvault.net official hosted instance.
|
||||
await storage.setItem('local:apiUrl', AppInfo.DEFAULT_API_URL);
|
||||
await storage.setItem('local:clientUrl', AppInfo.DEFAULT_CLIENT_URL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle custom client URL change
|
||||
* @param e
|
||||
*/
|
||||
const handleCustomClientUrlChange = async (e: React.ChangeEvent<HTMLInputElement>) : Promise<void> => {
|
||||
const value = e.target.value;
|
||||
setCustomClientUrl(value);
|
||||
await storage.setItem('local:clientUrl', value);
|
||||
|
||||
try {
|
||||
await urlSchema.validateAt('clientUrl', { clientUrl: value });
|
||||
setErrors(prev => ({ ...prev, clientUrl: undefined }));
|
||||
await storage.setItem('local:clientUrl', value);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Yup.ValidationError) {
|
||||
setErrors(prev => ({ ...prev, clientUrl: error.message }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -133,8 +194,11 @@ const AuthSettings: React.FC = () => {
|
||||
value={customClientUrl}
|
||||
onChange={handleCustomClientUrlChange}
|
||||
placeholder="https://my-aliasvault-instance.com"
|
||||
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
className={`w-full bg-gray-50 border ${errors.clientUrl ? 'border-red-500' : 'border-gray-300'} text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white`}
|
||||
/>
|
||||
{errors.clientUrl && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.clientUrl}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<label htmlFor="custom-api-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
|
||||
@@ -146,8 +210,11 @@ const AuthSettings: React.FC = () => {
|
||||
value={customUrl}
|
||||
onChange={handleCustomUrlChange}
|
||||
placeholder="https://my-aliasvault-instance.com/api"
|
||||
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
className={`w-full bg-gray-50 border ${errors.apiUrl ? 'border-red-500' : 'border-gray-300'} text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white`}
|
||||
/>
|
||||
{errors.apiUrl && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.apiUrl}</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { FormInput } from '@/entrypoints/popup/components/FormInput';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import Modal from '@/entrypoints/popup/components/Modal';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
// Persisted form data type used for JSON serialization.
|
||||
type PersistedFormData = {
|
||||
credentialId: string | null;
|
||||
mode: CredentialMode;
|
||||
formValues: Omit<Credential, 'Logo'> & { Logo?: string | null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema for the credential form.
|
||||
*/
|
||||
const credentialSchema = Yup.object().shape({
|
||||
Id: Yup.string(),
|
||||
ServiceName: Yup.string().required('Service name is required'),
|
||||
ServiceUrl: Yup.string().url('Invalid URL format').nullable().optional(),
|
||||
Alias: Yup.object().shape({
|
||||
FirstName: Yup.string().nullable().optional(),
|
||||
LastName: Yup.string().nullable().optional(),
|
||||
NickName: Yup.string().nullable().optional(),
|
||||
BirthDate: Yup.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.test(
|
||||
'is-valid-date-format',
|
||||
'Date must be in YYYY-MM-DD format',
|
||||
value => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value);
|
||||
},
|
||||
),
|
||||
Gender: Yup.string().nullable().optional(),
|
||||
Email: Yup.string().email('Invalid email format').nullable().optional()
|
||||
}),
|
||||
Username: Yup.string().nullable().optional(),
|
||||
Password: Yup.string().nullable().optional(),
|
||||
Notes: Yup.string().nullable().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Add or edit credential page.
|
||||
*/
|
||||
const CredentialAddEdit: React.FC = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
const { executeVaultMutation, isLoading, syncStatus } = useVaultMutate();
|
||||
const [mode, setMode] = useState<CredentialMode>('random');
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [localLoading, setLocalLoading] = useState(true);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
|
||||
const serviceNameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { handleSubmit, setValue, watch, formState: { errors } } = useForm<Credential>({
|
||||
resolver: yupResolver(credentialSchema as Yup.ObjectSchema<Credential>),
|
||||
defaultValues: {
|
||||
Id: "",
|
||||
Username: "",
|
||||
Password: "",
|
||||
ServiceName: "",
|
||||
ServiceUrl: "",
|
||||
Notes: "",
|
||||
Alias: {
|
||||
FirstName: "",
|
||||
LastName: "",
|
||||
NickName: "",
|
||||
BirthDate: "",
|
||||
Gender: undefined,
|
||||
Email: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Persists the current form values to storage
|
||||
* @returns Promise that resolves when the form values are persisted
|
||||
*/
|
||||
const persistFormValues = useCallback(async (): Promise<void> => {
|
||||
if (localLoading) {
|
||||
// Do not persist values if the page is still loading.
|
||||
return;
|
||||
}
|
||||
|
||||
const formValues = watch();
|
||||
const persistedData: PersistedFormData = {
|
||||
credentialId: id || null,
|
||||
mode,
|
||||
formValues: {
|
||||
...formValues,
|
||||
Logo: null // Don't persist the Logo field as it can't be user modified in the UI.
|
||||
}
|
||||
};
|
||||
await sendMessage('PERSIST_FORM_VALUES', JSON.stringify(persistedData), 'background');
|
||||
}, [watch, id, mode, localLoading]);
|
||||
|
||||
/**
|
||||
* Watch for mode changes and persist form values
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!localLoading) {
|
||||
void persistFormValues();
|
||||
}
|
||||
}, [mode, persistFormValues, localLoading]);
|
||||
|
||||
// Watch for form changes and persist them
|
||||
useEffect(() => {
|
||||
const subscription = watch(() => {
|
||||
void persistFormValues();
|
||||
});
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [watch, persistFormValues]);
|
||||
|
||||
// If we received an ID, we're in edit mode
|
||||
const isEditMode = id !== undefined && id.length > 0;
|
||||
|
||||
/**
|
||||
* Loads persisted form values from storage. This is used to keep track of form changes
|
||||
* and restore them when the page is reloaded. The browser extension popup will close
|
||||
* automatically by clicking outside of the popup, but with this logic we can restore
|
||||
* the form values when the page is reloaded so the user can continue their mutation operation.
|
||||
*
|
||||
* @returns Promise that resolves when the form values are loaded
|
||||
*/
|
||||
const loadPersistedValues = useCallback(async (): Promise<void> => {
|
||||
const persistedData = await sendMessage('GET_PERSISTED_FORM_VALUES', null, 'background') as string | null;
|
||||
|
||||
// Try to parse the persisted data as a JSON object.
|
||||
try {
|
||||
let persistedDataObject: PersistedFormData | null = null;
|
||||
try {
|
||||
if (persistedData) {
|
||||
persistedDataObject = JSON.parse(persistedData) as PersistedFormData;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing persisted data:', error);
|
||||
}
|
||||
|
||||
// Check if the object has a value and is not null
|
||||
const objectEmpty = persistedDataObject === null || persistedDataObject === undefined;
|
||||
if (objectEmpty) {
|
||||
// If the persisted data object is empty, we don't have any values to restore and can exit early.
|
||||
setLocalLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPage = persistedDataObject?.credentialId == id;
|
||||
if (persistedDataObject && isCurrentPage) {
|
||||
// Only restore if the persisted credential ID matches current page
|
||||
setMode(persistedDataObject.mode);
|
||||
Object.entries(persistedDataObject.formValues).forEach(([key, value]) => {
|
||||
setValue(key as keyof Credential, value as Credential[keyof Credential]);
|
||||
});
|
||||
} else {
|
||||
console.error('Persisted values do not match current page');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading persisted data:', error);
|
||||
}
|
||||
|
||||
// Set local loading state to false which also activates the persisting of form value changes from this point on.
|
||||
setLocalLoading(false);
|
||||
}, [setValue, id, setMode, setLocalLoading]);
|
||||
|
||||
/**
|
||||
* Clears persisted form values from storage
|
||||
* @returns Promise that resolves when the form values are cleared
|
||||
*/
|
||||
const clearPersistedValues = useCallback(async (): Promise<void> => {
|
||||
await sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background');
|
||||
}, []);
|
||||
|
||||
// Clear persisted values when the page is unmounted.
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
void clearPersistedValues();
|
||||
};
|
||||
}, [clearPersistedValues]);
|
||||
|
||||
/**
|
||||
* Load an existing credential from the database in edit mode.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!dbContext?.sqliteClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
// On create mode, focus the service name field after a short delay to ensure the component is mounted.
|
||||
setTimeout(() => {
|
||||
serviceNameRef.current?.focus();
|
||||
}, 100);
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Load persisted form values if they exist.
|
||||
loadPersistedValues();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = dbContext.sqliteClient.getCredentialById(id);
|
||||
|
||||
if (result) {
|
||||
result.Alias.BirthDate = IdentityHelperUtils.normalizeBirthDateForDisplay(result.Alias.BirthDate);
|
||||
|
||||
// Set form values
|
||||
Object.entries(result).forEach(([key, value]) => {
|
||||
setValue(key as keyof Credential, value);
|
||||
});
|
||||
|
||||
setMode('manual');
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Check for persisted values that might override the loaded values if they exist.
|
||||
loadPersistedValues();
|
||||
} else {
|
||||
console.error('Credential not found');
|
||||
navigate('/credentials');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading credential:', err);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues]);
|
||||
|
||||
/**
|
||||
* Handle the delete button click.
|
||||
*/
|
||||
const handleDelete = useCallback(async (): Promise<void> => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
executeVaultMutation(async () => {
|
||||
dbContext.sqliteClient!.deleteCredentialById(id);
|
||||
}, {
|
||||
/**
|
||||
* Navigate to the credentials list page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void clearPersistedValues();
|
||||
navigate('/credentials');
|
||||
}
|
||||
});
|
||||
}, [id, executeVaultMutation, dbContext.sqliteClient, navigate, clearPersistedValues]);
|
||||
|
||||
/**
|
||||
* Initialize the identity and password generators with settings from user's vault.
|
||||
*/
|
||||
const initializeGenerators = useCallback(async () => {
|
||||
// Get default identity language from database
|
||||
const identityLanguage = dbContext.sqliteClient!.getDefaultIdentityLanguage();
|
||||
|
||||
// Initialize identity generator based on language
|
||||
const identityGenerator = CreateIdentityGenerator(identityLanguage);
|
||||
|
||||
// Initialize password generator with settings from vault
|
||||
const passwordSettings = dbContext.sqliteClient!.getPasswordSettings();
|
||||
const passwordGenerator = CreatePasswordGenerator(passwordSettings);
|
||||
|
||||
return { identityGenerator, passwordGenerator };
|
||||
}, [dbContext.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Generate a random alias and password.
|
||||
*/
|
||||
const generateRandomAlias = useCallback(async () => {
|
||||
const { identityGenerator, passwordGenerator } = await initializeGenerators();
|
||||
|
||||
const identity = identityGenerator.generateRandomIdentity();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
const metadata = await dbContext.getVaultMetadata();
|
||||
|
||||
const privateEmailDomains = metadata?.privateEmailDomains ?? [];
|
||||
const publicEmailDomains = metadata?.publicEmailDomains ?? [];
|
||||
const defaultEmailDomain = dbContext.sqliteClient!.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
|
||||
setValue('Alias.Email', email);
|
||||
setValue('Alias.FirstName', identity.firstName);
|
||||
setValue('Alias.LastName', identity.lastName);
|
||||
setValue('Alias.NickName', identity.nickName);
|
||||
setValue('Alias.Gender', identity.gender);
|
||||
setValue('Alias.BirthDate', IdentityHelperUtils.normalizeBirthDateForDisplay(identity.birthDate.toISOString()));
|
||||
|
||||
// In edit mode, preserve existing username and password if they exist
|
||||
if (isEditMode && watch('Username')) {
|
||||
// Keep the existing username in edit mode, so don't do anything here.
|
||||
} else {
|
||||
// Use the newly generated username
|
||||
setValue('Username', identity.nickName);
|
||||
}
|
||||
|
||||
if (isEditMode && watch('Password')) {
|
||||
// Keep the existing password in edit mode, so don't do anything here.
|
||||
} else {
|
||||
// Use the newly generated password
|
||||
setValue('Password', password);
|
||||
}
|
||||
}, [isEditMode, watch, setValue, initializeGenerators, dbContext]);
|
||||
|
||||
/**
|
||||
* Handle the generate random alias button press.
|
||||
*/
|
||||
const handleGenerateRandomAlias = useCallback(() => {
|
||||
void generateRandomAlias();
|
||||
}, [generateRandomAlias]);
|
||||
|
||||
const generateRandomUsername = useCallback(async () => {
|
||||
try {
|
||||
const usernameEmailGenerator = CreateUsernameEmailGenerator();
|
||||
|
||||
let gender = Gender.Other;
|
||||
try {
|
||||
gender = watch('Alias.Gender') as Gender;
|
||||
} catch {
|
||||
// Gender parsing failed, default to other.
|
||||
}
|
||||
|
||||
const identity: Identity = {
|
||||
firstName: watch('Alias.FirstName') ?? '',
|
||||
lastName: watch('Alias.LastName') ?? '',
|
||||
nickName: watch('Alias.NickName') ?? '',
|
||||
gender: gender,
|
||||
birthDate: new Date(watch('Alias.BirthDate') ?? ''),
|
||||
emailPrefix: watch('Alias.Email') ?? '',
|
||||
};
|
||||
|
||||
const username = usernameEmailGenerator.generateUsername(identity);
|
||||
setValue('Username', username);
|
||||
} catch (error) {
|
||||
console.error('Error generating random username:', error);
|
||||
}
|
||||
}, [setValue, watch]);
|
||||
|
||||
const generateRandomPassword = useCallback(async () => {
|
||||
try {
|
||||
const { passwordGenerator } = await initializeGenerators();
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
setValue('Password', password);
|
||||
setShowPassword(true);
|
||||
} catch (error) {
|
||||
console.error('Error generating random password:', error);
|
||||
}
|
||||
}, [initializeGenerators, setValue]);
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
*/
|
||||
const onSubmit = useCallback(async (data: Credential): Promise<void> => {
|
||||
// Normalize the birth date for database entry.
|
||||
let birthdate = data.Alias.BirthDate;
|
||||
if (birthdate) {
|
||||
birthdate = IdentityHelperUtils.normalizeBirthDateForDb(birthdate);
|
||||
}
|
||||
|
||||
// If we're creating a new credential and mode is random, generate random values here
|
||||
if (!isEditMode && mode === 'random') {
|
||||
// Generate random values now and then read them from the form fields to manually assign to the credentialToSave object
|
||||
await generateRandomAlias();
|
||||
data.Username = watch('Username');
|
||||
data.Password = watch('Password');
|
||||
data.Alias.FirstName = watch('Alias.FirstName');
|
||||
data.Alias.LastName = watch('Alias.LastName');
|
||||
data.Alias.NickName = watch('Alias.NickName');
|
||||
data.Alias.BirthDate = birthdate;
|
||||
data.Alias.Gender = watch('Alias.Gender');
|
||||
data.Alias.Email = watch('Alias.Email');
|
||||
}
|
||||
|
||||
// Extract favicon from service URL if the credential has one
|
||||
if (data.ServiceUrl) {
|
||||
setLocalLoading(true);
|
||||
try {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Favicon extraction timed out')), 5000)
|
||||
);
|
||||
|
||||
const faviconPromise = webApi.get<{ image: string }>('Favicon/Extract?url=' + data.ServiceUrl);
|
||||
const faviconResponse = await Promise.race([faviconPromise, timeoutPromise]) as { image: string };
|
||||
|
||||
if (faviconResponse?.image) {
|
||||
const decodedImage = Uint8Array.from(Buffer.from(faviconResponse.image, 'base64'));
|
||||
data.Logo = decodedImage;
|
||||
}
|
||||
} catch {
|
||||
// Favicon extraction failed or timed out, this is not a critical error so we can ignore it.
|
||||
}
|
||||
}
|
||||
|
||||
executeVaultMutation(async () => {
|
||||
setLocalLoading(false);
|
||||
|
||||
if (isEditMode) {
|
||||
await dbContext.sqliteClient!.updateCredentialById(data);
|
||||
} else {
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(data);
|
||||
data.Id = credentialId.toString();
|
||||
}
|
||||
}, {
|
||||
/**
|
||||
* Navigate to the credential details page on success.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
void clearPersistedValues();
|
||||
// If in add mode, navigate to the credential details page.
|
||||
if (!isEditMode) {
|
||||
// Navigate to the credential details page.
|
||||
navigate(`/credentials/${data.Id}`, { replace: true });
|
||||
} else {
|
||||
// If in edit mode, pop the current page from the history stack to end up on details page as well.
|
||||
navigate(-1);
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
// Only set the header buttons once on mount.
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditMode && (
|
||||
<HeaderButton
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
title="Delete credential"
|
||||
iconType={HeaderIconType.DELETE}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
title="Save credential"
|
||||
iconType={HeaderIconType.SAVE}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => {};
|
||||
}, [setHeaderButtons, handleSubmit, onSubmit, isEditMode]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
if (isEditMode && !watch('ServiceName')) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<button type="submit" style={{ display: 'none' }} />
|
||||
{(localLoading || isLoading) && (
|
||||
<div className="fixed inset-0 flex flex-col justify-center items-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90 z-50">
|
||||
<LoadingSpinner />
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
{syncStatus}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false);
|
||||
void handleDelete();
|
||||
}}
|
||||
title="Delete Credential"
|
||||
message="Are you sure you want to delete this credential? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
{!isEditMode && (
|
||||
<div className="flex space-x-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('random')}
|
||||
className={`flex-1 py-2 px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'random' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className='w-5 h-5' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8" cy="8" r="1"/>
|
||||
<circle cx="16" cy="8" r="1"/>
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="8" cy="16" r="1"/>
|
||||
<circle cx="16" cy="16" r="1"/>
|
||||
</svg>
|
||||
Random Alias
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMode('manual')}
|
||||
className={`flex-1 py-2 px-4 rounded flex items-center justify-center gap-2 ${
|
||||
mode === 'manual' ? 'bg-primary-500 text-white' : 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
<path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>
|
||||
</svg>
|
||||
Manual
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Service</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="serviceName"
|
||||
label="Service Name"
|
||||
ref={serviceNameRef}
|
||||
value={watch('ServiceName') ?? ''}
|
||||
onChange={(value) => setValue('ServiceName', value)}
|
||||
required
|
||||
error={errors.ServiceName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="serviceUrl"
|
||||
label="Service URL"
|
||||
value={watch('ServiceUrl') ?? ''}
|
||||
onChange={(value) => setValue('ServiceUrl', value)}
|
||||
error={errors.ServiceUrl?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(mode === 'manual' || isEditMode) && (
|
||||
<>
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Login Credentials</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="username"
|
||||
label="Username"
|
||||
value={watch('Username') ?? ''}
|
||||
onChange={(value) => setValue('Username', value)}
|
||||
error={errors.Username?.message}
|
||||
buttons={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
onClick: generateRandomUsername,
|
||||
title: 'Generate random username'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<FormInput
|
||||
id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
value={watch('Password') ?? ''}
|
||||
onChange={(value) => setValue('Password', value)}
|
||||
error={errors.Password?.message}
|
||||
showPassword={showPassword}
|
||||
onShowPasswordChange={setShowPassword}
|
||||
buttons={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
onClick: generateRandomPassword,
|
||||
title: 'Generate random password'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateRandomAlias}
|
||||
className="w-full bg-primary-500 text-white py-2 px-4 rounded hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
Generate Random Alias
|
||||
</button>
|
||||
<FormInput
|
||||
id="email"
|
||||
label="Email"
|
||||
value={watch('Alias.Email') ?? ''}
|
||||
onChange={(value) => setValue('Alias.Email', value)}
|
||||
error={errors.Alias?.Email?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Alias</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="firstName"
|
||||
label="First Name"
|
||||
value={watch('Alias.FirstName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.FirstName', value)}
|
||||
error={errors.Alias?.FirstName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="lastName"
|
||||
label="Last Name"
|
||||
value={watch('Alias.LastName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.LastName', value)}
|
||||
error={errors.Alias?.LastName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="nickName"
|
||||
label="Nick Name"
|
||||
value={watch('Alias.NickName') ?? ''}
|
||||
onChange={(value) => setValue('Alias.NickName', value)}
|
||||
error={errors.Alias?.NickName?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="gender"
|
||||
label="Gender"
|
||||
value={watch('Alias.Gender') ?? ''}
|
||||
onChange={(value) => setValue('Alias.Gender', value)}
|
||||
error={errors.Alias?.Gender?.message}
|
||||
/>
|
||||
<FormInput
|
||||
id="birthDate"
|
||||
label="Birth Date"
|
||||
placeholder="YYYY-MM-DD"
|
||||
value={watch('Alias.BirthDate') ?? ''}
|
||||
onChange={(value) => setValue('Alias.BirthDate', value)}
|
||||
error={errors.Alias?.BirthDate?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Metadata</h2>
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
id="notes"
|
||||
label="Notes"
|
||||
value={watch('Notes') ?? ''}
|
||||
onChange={(value) => setValue('Notes', value)}
|
||||
multiline
|
||||
rows={4}
|
||||
error={errors.Notes?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CredentialAddEdit;
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import {
|
||||
HeaderBlock,
|
||||
EmailBlock,
|
||||
@@ -11,16 +9,24 @@ import {
|
||||
AliasBlock,
|
||||
NotesBlock
|
||||
} from '@/entrypoints/popup/components/CredentialDetails';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
/**
|
||||
* Credential details page.
|
||||
*/
|
||||
const CredentialDetails: React.FC = () => {
|
||||
const CredentialDetails: React.FC = (): React.ReactElement => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
const [credential, setCredential] = useState<Credential | null>(null);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
@@ -33,9 +39,9 @@ const CredentialDetails: React.FC = () => {
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = (): void => {
|
||||
const width = 380;
|
||||
const height = 600;
|
||||
const openInNewPopup = useCallback((): void => {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
@@ -46,24 +52,14 @@ const CredentialDetails: React.FC = () => {
|
||||
);
|
||||
|
||||
window.close();
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
/**
|
||||
* Check if the email domain is supported.
|
||||
* Navigate to the edit page for this credential.
|
||||
*/
|
||||
const isEmailDomainSupported = (email: string): boolean => {
|
||||
const domain = email.split('@')[1]?.toLowerCase();
|
||||
if (!domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const publicDomains = dbContext.publicEmailDomains ?? [];
|
||||
const privateDomains = dbContext.privateEmailDomains ?? [];
|
||||
|
||||
return [...publicDomains, ...privateDomains].some(supportedDomain =>
|
||||
domain === supportedDomain || domain.endsWith(`.${supportedDomain}`)
|
||||
);
|
||||
};
|
||||
const handleEdit = useCallback((): void => {
|
||||
navigate(`/credentials/${id}/edit`);
|
||||
}, [id, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPopup()) {
|
||||
@@ -89,23 +85,49 @@ const CredentialDetails: React.FC = () => {
|
||||
}
|
||||
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
<HeaderButton
|
||||
onClick={handleEdit}
|
||||
title="Edit credential"
|
||||
iconType={HeaderIconType.EDIT}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => {};
|
||||
}, [setHeaderButtons, handleEdit, openInNewPopup]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
if (!credential) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<HeaderBlock credential={credential} onOpenNewPopup={openInNewPopup} />
|
||||
<div className="flex justify-between items-center">
|
||||
<HeaderBlock credential={credential} />
|
||||
</div>
|
||||
{credential.Alias?.Email && (
|
||||
<EmailBlock
|
||||
email={credential.Alias.Email}
|
||||
isSupported={isEmailDomainSupported(credential.Alias.Email)}
|
||||
/>
|
||||
)}
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
<TotpBlock credentialId={credential.Id} />
|
||||
<LoginCredentialsBlock credential={credential} />
|
||||
<AliasBlock credential={credential} />
|
||||
<NotesBlock notes={credential.Notes} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
|
||||
|
||||
/**
|
||||
* Credentials list page.
|
||||
@@ -16,15 +22,25 @@ import CredentialCard from '@/entrypoints/popup/components/CredentialCard';
|
||||
const CredentialsList: React.FC = () => {
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const navigate = useNavigate();
|
||||
const { syncVault } = useVaultSync();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Loading state with minimum duration for more fluid UX.
|
||||
*/
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 100);
|
||||
|
||||
/**
|
||||
* Handle add new credential.
|
||||
*/
|
||||
const handleAddCredential = useCallback(() : void => {
|
||||
navigate('/credentials/add');
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* Retrieve latest vault and refresh the credentials list.
|
||||
*/
|
||||
@@ -33,83 +49,84 @@ const CredentialsList: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do status check first to ensure the extension is (still) supported.
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
await webApi.logout(statusError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// If the vault revision is the same or lower, (re)load existing credentials.
|
||||
if (statusResponse.vaultRevision <= dbContext.vaultRevision) {
|
||||
const results = dbContext.sqliteClient.getAllCredentials();
|
||||
setCredentials(results);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the vault revision is higher, fetch the latest vault and initialize the SQLite context again.
|
||||
* This will trigger a new credentials list refresh.
|
||||
*/
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
|
||||
if (vaultError) {
|
||||
await webApi.logout(vaultError);
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get derived key from background worker
|
||||
const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string;
|
||||
|
||||
// Initialize the SQLite context again with the newly retrieved decrypted blob)
|
||||
try {
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
} catch {
|
||||
// Sync vault and load credentials
|
||||
await syncVault({
|
||||
/**
|
||||
* If error occurs during database initialization, it most likely has to do with decryption that
|
||||
* failed. This is most likely due to the user changing their password.
|
||||
* So we logout the user here to force them to re-authenticate.
|
||||
* On success.
|
||||
*/
|
||||
await webApi.logout('Vault could not be decrypted, please re-authenticate.');
|
||||
}
|
||||
onSuccess: async (_hasNewVault) => {
|
||||
// Credentials list is refreshed automatically when the (new) sqlite client is available via useEffect hook below.
|
||||
},
|
||||
/**
|
||||
* On offline.
|
||||
*/
|
||||
_onOffline: () => {
|
||||
// Not implemented for browser extension yet.
|
||||
},
|
||||
/**
|
||||
* On error.
|
||||
*/
|
||||
onError: async (error) => {
|
||||
console.error('Error syncing vault:', error);
|
||||
await webApi.logout('Error while syncing vault, please re-authenticate.');
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Refresh error:', err);
|
||||
console.error('Error refreshing credentials:', err);
|
||||
await webApi.logout('Error while syncing vault, please re-authenticate.');
|
||||
}
|
||||
}, [dbContext, webApi, hideLoading]);
|
||||
}, [dbContext, webApi, syncVault]);
|
||||
|
||||
/**
|
||||
* Manually refresh the credentials list.
|
||||
* Get latest vault from server and refresh the credentials list.
|
||||
*/
|
||||
const onManualRefresh = async (): Promise<void> => {
|
||||
showLoading();
|
||||
const syncVaultAndRefresh = useCallback(async () : Promise<void> => {
|
||||
setIsLoading(true);
|
||||
await onRefresh();
|
||||
hideLoading();
|
||||
};
|
||||
setIsLoading(false);
|
||||
setIsInitialLoading(false);
|
||||
}, [onRefresh, setIsLoading, setIsInitialLoading]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={handleAddCredential}
|
||||
title="Add new credential"
|
||||
iconType={HeaderIconType.PLUS}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons, handleAddCredential]);
|
||||
|
||||
/**
|
||||
* Load credentials list on mount and on sqlite client change.
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Refresh credentials list when sqlite client is available.
|
||||
* Refresh credentials list when a (new) sqlite client is available.
|
||||
*/
|
||||
const refreshCredentials = async () : Promise<void> => {
|
||||
if (dbContext?.sqliteClient) {
|
||||
setIsLoading(true);
|
||||
await onRefresh();
|
||||
const results = dbContext.sqliteClient?.getAllCredentials() ?? [];
|
||||
setCredentials(results);
|
||||
setIsLoading(false);
|
||||
|
||||
// Hide the global app initial loading state after the credentials list is loaded.
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
refreshCredentials();
|
||||
}, [dbContext?.sqliteClient, onRefresh, setIsLoading, setIsInitialLoading]);
|
||||
}, [dbContext?.sqliteClient, setIsLoading]);
|
||||
|
||||
// Call syncVaultAndRefresh when the page first mounts
|
||||
useEffect(() => {
|
||||
syncVaultAndRefresh();
|
||||
}, [syncVaultAndRefresh]);
|
||||
|
||||
// Add this function to filter credentials
|
||||
const filteredCredentials = credentials.filter(cred => {
|
||||
@@ -135,7 +152,7 @@ const CredentialsList: React.FC = () => {
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Credentials</h2>
|
||||
<ReloadButton onClick={onManualRefresh} />
|
||||
<ReloadButton onClick={syncVaultAndRefresh} />
|
||||
</div>
|
||||
|
||||
{credentials.length > 0 ? (
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Email } from '@/utils/types/webapi/Email';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { Attachment } from '@/utils/types/webapi/Attachment';
|
||||
import Modal from '@/entrypoints/popup/components/Modal';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import ConversionUtility from '@/entrypoints/popup/utils/ConversionUtility';
|
||||
|
||||
import type { EmailAttachment, Email } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
import HeaderButton from '../components/HeaderButton';
|
||||
import { HeaderIconType } from '../components/Icons/HeaderIcons';
|
||||
|
||||
/**
|
||||
* Email details page.
|
||||
*/
|
||||
const EmailDetails: React.FC = () => {
|
||||
const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const dbContext = useDb();
|
||||
@@ -21,16 +28,10 @@ const EmailDetails: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState<Email | null>(null);
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Make sure the initial loading state is set to false when this component is loaded itself.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [setIsInitialLoading, isLoading]);
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [headerButtonsConfigured, setHeaderButtonsConfigured] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// For popup windows, ensure we have proper history state for navigation
|
||||
@@ -62,23 +63,28 @@ const EmailDetails: React.FC = () => {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadEmail();
|
||||
}, [id, dbContext?.sqliteClient, webApi, setIsLoading]);
|
||||
}, [id, dbContext?.sqliteClient, webApi, setIsLoading, setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Handle deleting an email.
|
||||
*/
|
||||
const handleDelete = async () : Promise<void> => {
|
||||
const handleDelete = useCallback(async () : Promise<void> => {
|
||||
try {
|
||||
await webApi.delete(`Email/${id}`);
|
||||
navigate('/emails');
|
||||
if (isPopup()) {
|
||||
window.close();
|
||||
} else {
|
||||
navigate('/emails');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete email');
|
||||
}
|
||||
};
|
||||
}, [id, webApi, navigate]);
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
@@ -91,7 +97,7 @@ const EmailDetails: React.FC = () => {
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = () : void => {
|
||||
const openInNewPopup = useCallback((): void => {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
@@ -105,15 +111,15 @@ const EmailDetails: React.FC = () => {
|
||||
|
||||
// Close the current tab
|
||||
window.close();
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
/**
|
||||
* Handle downloading an attachment.
|
||||
*/
|
||||
const handleDownloadAttachment = async (attachment: Attachment): Promise<void> => {
|
||||
const handleDownloadAttachment = async (attachment: EmailAttachment): Promise<void> => {
|
||||
try {
|
||||
// Get the encrypted attachment bytes from the API
|
||||
const base64EncryptedAttachment = await webApi.downloadBlobAndConvertToBase64(`Email/${id}/attachments/${attachment.id}`);
|
||||
const encryptedBytes = await webApi.downloadBlob(`Email/${id}/attachments/${attachment.id}`);
|
||||
|
||||
if (!dbContext?.sqliteClient || !email) {
|
||||
setError('Database context or email not available');
|
||||
@@ -123,16 +129,18 @@ const EmailDetails: React.FC = () => {
|
||||
// Get encryption keys for decryption
|
||||
const encryptionKeys = dbContext.sqliteClient.getAllEncryptionKeys();
|
||||
|
||||
// Decrypt the attachment using ArrayBuffer
|
||||
const decryptedBytes = await EncryptionUtility.decryptAttachment(base64EncryptedAttachment, email, encryptionKeys);
|
||||
// Decrypt the attachment using raw bytes
|
||||
const decryptedBytes = await EncryptionUtility.decryptAttachment(encryptedBytes, email, encryptionKeys);
|
||||
|
||||
if (!decryptedBytes) {
|
||||
setError('Failed to decrypt attachment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create blob from decrypted bytes with proper MIME type
|
||||
const blob = new Blob([decryptedBytes], { type: attachment.mimeType ?? 'application/octet-stream' });
|
||||
// Create Blob directly from Uint8Array
|
||||
const blob = new Blob([new Uint8Array(decryptedBytes)], {
|
||||
type: attachment.mimeType ?? 'application/octet-stream'
|
||||
});
|
||||
|
||||
// Create download link and trigger download
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
@@ -151,6 +159,37 @@ const EmailDetails: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
// Only set the header buttons once on mount.
|
||||
if (!headerButtonsConfigured) {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openInNewPopup}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
<HeaderButton
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
title="Delete email"
|
||||
iconType={HeaderIconType.DELETE}
|
||||
variant="danger"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
setHeaderButtonsConfigured(true);
|
||||
}
|
||||
return () => {};
|
||||
}, [setHeaderButtons, headerButtonsConfigured, openInNewPopup]);
|
||||
|
||||
// Clear header buttons on unmount
|
||||
useEffect((): (() => void) => {
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-8">
|
||||
@@ -169,53 +208,24 @@ const EmailDetails: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Modal
|
||||
isOpen={showDeleteModal}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
onConfirm={() => {
|
||||
setShowDeleteModal(false);
|
||||
void handleDelete();
|
||||
}}
|
||||
title="Delete Email"
|
||||
message="Are you sure you want to delete this email? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">{email.subject}</h1>
|
||||
<div className="flex space-x-2">
|
||||
<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>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="p-2 text-red-500 hover:text-red-600 rounded-md hover:bg-red-100 dark:hover:bg-red-900/20"
|
||||
title="Delete email"
|
||||
>
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>From: {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})</p>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MailboxBulkRequest, MailboxBulkResponse } from '@/utils/types/webapi/MailboxBulk';
|
||||
import { MailboxEmail } from '@/utils/types/webapi/MailboxEmail';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import type { MailboxBulkRequest, MailboxBulkResponse, MailboxEmail } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
/**
|
||||
* Emails list page.
|
||||
@@ -17,6 +20,7 @@ const EmailsList: React.FC = () => {
|
||||
const webApi = useWebApi();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [emails, setEmails] = useState<MailboxEmail[]>([]);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
|
||||
/**
|
||||
* Loading state with minimum duration for more fluid UX.
|
||||
@@ -61,8 +65,9 @@ const EmailsList: React.FC = () => {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading]);
|
||||
}, [dbContext?.sqliteClient, webApi, setIsLoading, setIsInitialLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadEmails();
|
||||
|
||||
@@ -1,61 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import Unlock from '@/entrypoints/popup/pages/Unlock';
|
||||
import Login from '@/entrypoints/popup/pages/Login';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { useNavigation } from '@/entrypoints/popup/context/NavigationContext';
|
||||
|
||||
/**
|
||||
* Home page that shows the correct page based on the user's authentication state.
|
||||
* Most of the navigation logic is now handled by NavigationContext.
|
||||
*/
|
||||
const Home: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
|
||||
const { isFullyInitialized } = useNavigation();
|
||||
|
||||
// Initialization state.
|
||||
const isFullyInitialized = authContext.isInitialized && dbContext.dbInitialized;
|
||||
const isAuthenticated = authContext.isLoggedIn;
|
||||
const isDatabaseAvailable = dbContext.dbAvailable;
|
||||
const requireLoginOrUnlock = isFullyInitialized && (!isAuthenticated || !isDatabaseAvailable || isInlineUnlockMode);
|
||||
|
||||
useEffect(() => {
|
||||
// Detect if the user is coming from the unlock page with mode=inline_unlock.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isInlineUnlockMode = urlParams.get('mode') === 'inline_unlock';
|
||||
setIsInlineUnlockMode(isInlineUnlockMode);
|
||||
|
||||
// Redirect to credentials if fully initialized and doesn't need unlock.
|
||||
if (isFullyInitialized && !requireLoginOrUnlock) {
|
||||
navigate('/credentials', { replace: true });
|
||||
}
|
||||
}, [isFullyInitialized, requireLoginOrUnlock, isInlineUnlockMode, navigate]);
|
||||
|
||||
// Show loading state if not fully initialized or when about to redirect to credentials.
|
||||
if (!isFullyInitialized || (isFullyInitialized && !requireLoginOrUnlock)) {
|
||||
// Global loading spinner will be shown by the parent component.
|
||||
if (!isFullyInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsInitialLoading(false);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (!isDatabaseAvailable) {
|
||||
return <Unlock />;
|
||||
}
|
||||
|
||||
if (isInlineUnlockMode) {
|
||||
return <UnlockSuccess onClose={() => setIsInlineUnlockMode(false)} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
return <Navigate to="/credentials" replace />;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,19 +1,24 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Buffer } from 'buffer';
|
||||
import { storage } from '#imports';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
import { LoginResponse } from '@/utils/types/webapi/Login';
|
||||
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import type { VaultResponse, LoginResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
|
||||
import ConversionUtility from '../utils/ConversionUtility';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Login page
|
||||
*/
|
||||
@@ -24,7 +29,7 @@ const Login: React.FC = () => {
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
|
||||
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
|
||||
@@ -48,9 +53,10 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
setClientUrl(clientUrl);
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
loadClientUrl();
|
||||
}, []);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
@@ -66,7 +72,7 @@ const Login: React.FC = () => {
|
||||
authContext.clearGlobalMessage();
|
||||
|
||||
// Use the srpUtil instance instead of the imported singleton
|
||||
const loginResponse = await srpUtil.initiateLogin(credentials.username);
|
||||
const loginResponse = await srpUtil.initiateLogin(ConversionUtility.normalizeUsername(credentials.username));
|
||||
|
||||
// 1. Derive key from password using Argon2id
|
||||
const passwordHash = await EncryptionUtility.deriveKeyFromPassword(
|
||||
@@ -84,7 +90,7 @@ const Login: React.FC = () => {
|
||||
|
||||
// 2. Validate login with SRP protocol
|
||||
const validationResponse = await srpUtil.validateLogin(
|
||||
credentials.username,
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
loginResponse
|
||||
@@ -122,7 +128,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
// All is good. Store auth info which is required to make requests to the web API.
|
||||
await authContext.setAuthTokens(credentials.username, validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
@@ -164,7 +170,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
const validationResponse = await srpUtil.validateLogin2Fa(
|
||||
credentials.username,
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
loginResponse,
|
||||
@@ -189,7 +195,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
// All is good. Store auth info which is required to make requests to the web API.
|
||||
await authContext.setAuthTokens(credentials.username, validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), validationResponse.token.token, validationResponse.token.refreshToken);
|
||||
|
||||
// Initialize the SQLite context with the new vault data.
|
||||
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { storage } from "#imports";
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
import { DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, GLOBAL_CONTEXT_MENU_ENABLED_KEY, TEMPORARY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
|
||||
import { browser } from "#imports";
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, GLOBAL_CONTEXT_MENU_ENABLED_KEY, TEMPORARY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
|
||||
import { useLoading } from '../context/LoadingContext';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
/**
|
||||
* Popup settings type.
|
||||
@@ -23,6 +31,9 @@ type PopupSettings = {
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const authContext = useAuth();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
@@ -41,6 +52,35 @@ const Settings: React.FC = () => {
|
||||
return tab;
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
};
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title="Open web app"
|
||||
iconType={HeaderIconType.EXTERNAL_LINK}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
*/
|
||||
@@ -72,7 +112,8 @@ const Settings: React.FC = () => {
|
||||
isGloballyEnabled,
|
||||
isContextMenuEnabled
|
||||
});
|
||||
}, []);
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
@@ -188,12 +229,52 @@ const Settings: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
await authContext.logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">Settings</h2>
|
||||
</div>
|
||||
|
||||
{/* User Menu Section */}
|
||||
<section>
|
||||
<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 className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{authContext.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Logged in
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Global Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">Global Settings</h3>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Buffer } from 'buffer';
|
||||
import { storage } from '#imports';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
import { VaultResponse } from '@/utils/types/webapi/VaultResponse';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Unlock page
|
||||
@@ -25,7 +29,7 @@ const Unlock: React.FC = () => {
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -37,10 +41,11 @@ const Unlock: React.FC = () => {
|
||||
if (statusError !== null) {
|
||||
await webApi.logout(statusError);
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
}, [webApi, authContext]);
|
||||
}, [webApi, authContext, setIsInitialLoading]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Utility class for conversion operations.
|
||||
* TODO: make this a shared utility class in root /shared/ folder so we can reuse it between browser extension/mobile app
|
||||
* and possibly WASM client.
|
||||
*/
|
||||
class ConversionUtility {
|
||||
/**
|
||||
@@ -49,6 +51,15 @@ class ConversionUtility {
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a username by converting it to lowercase and trimming whitespace.
|
||||
* @param username The username to normalize.
|
||||
* @returns The normalized username.
|
||||
*/
|
||||
public normalizeUsername(username: string): string {
|
||||
return username.toLowerCase().trim();
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConversionUtility();
|
||||
|
||||
@@ -1,9 +1,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 type { LoginRequest, LoginResponse, ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse, BadRequestResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
/**
|
||||
* Utility class for SRP authentication operations.
|
||||
|
||||
@@ -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.17.1';
|
||||
public static readonly VERSION = '0.19.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import argon2 from 'argon2-browser/dist/argon2-bundled.min.js';
|
||||
import { Email } from './types/webapi/Email';
|
||||
import { EncryptionKey } from './types/EncryptionKey';
|
||||
import { MailboxEmail } from './types/webapi/MailboxEmail';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import argon2 from 'argon2-browser/dist/argon2-bundled.min.js';
|
||||
|
||||
import type { EncryptionKey } from '@/utils/dist/shared/models/vault';
|
||||
import type { Email, MailboxEmail } from '@/utils/dist/shared/models/webapi';
|
||||
|
||||
/**
|
||||
* Utility class for encryption operations including:
|
||||
* - Argon2Id key derivation
|
||||
@@ -118,6 +119,37 @@ export class EncryptionUtility {
|
||||
return decoder.decode(decrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts data using AES-GCM symmetric encryption with raw bytes input/output
|
||||
*/
|
||||
public static async symmetricDecryptBytes(encryptedBytes: Uint8Array, base64Key: string): Promise<Uint8Array> {
|
||||
if (!encryptedBytes || encryptedBytes.length === 0) {
|
||||
return encryptedBytes;
|
||||
}
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)),
|
||||
{
|
||||
name: "AES-GCM",
|
||||
length: 256,
|
||||
},
|
||||
false,
|
||||
["decrypt"]
|
||||
);
|
||||
|
||||
const iv = encryptedBytes.slice(0, 12);
|
||||
const ciphertext = encryptedBytes.slice(12);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
return new Uint8Array(decrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new RSA key pair for asymmetric encryption
|
||||
*/
|
||||
@@ -292,9 +324,13 @@ export class EncryptionUtility {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts an attachment based on the provided public/private key pairs and returns the decrypted bytes as a base64 string.
|
||||
* Decrypts an attachment and returns the decrypted content as Uint8Array (raw bytes).
|
||||
*/
|
||||
public static async decryptAttachment(base64EncryptedAttachment: string, email: Email, encryptionKeys: EncryptionKey[]): Promise<string> {
|
||||
public static async decryptAttachment(
|
||||
encryptedBytes: Uint8Array,
|
||||
email: Email,
|
||||
encryptionKeys: EncryptionKey[]
|
||||
): Promise<Uint8Array> {
|
||||
try {
|
||||
const encryptionKey = encryptionKeys.find(key => key.PublicKey === email.encryptionKey);
|
||||
|
||||
@@ -302,15 +338,17 @@ export class EncryptionUtility {
|
||||
throw new Error('Encryption key not found');
|
||||
}
|
||||
|
||||
// Decrypt symmetric key with asymmetric private key
|
||||
// Decrypt the symmetric key using private key (returns raw bytes)
|
||||
const symmetricKey = await EncryptionUtility.decryptWithPrivateKey(
|
||||
email.encryptedSymmetricKey,
|
||||
encryptionKey.PrivateKey
|
||||
);
|
||||
|
||||
// Convert symmetric key to base64 string if symmetricDecrypt expects it
|
||||
const symmetricKeyBase64 = Buffer.from(symmetricKey).toString('base64');
|
||||
|
||||
const encryptedBytesString = await EncryptionUtility.symmetricDecrypt(base64EncryptedAttachment, symmetricKeyBase64);
|
||||
return encryptedBytesString;
|
||||
// Decrypt the attachment using raw bytes
|
||||
return await EncryptionUtility.symmetricDecryptBytes(encryptedBytes, symmetricKeyBase64);
|
||||
} catch (err) {
|
||||
throw new Error(err instanceof Error ? err.message : 'Failed to decrypt attachment');
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* 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,8 +1,6 @@
|
||||
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';
|
||||
|
||||
import type { Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
/**
|
||||
* Placeholder base64 image for credentials without a logo.
|
||||
@@ -14,6 +12,7 @@ const placeholderBase64 = 'UklGRjoEAABXRUJQVlA4IC4EAAAwFwCdASqAAIAAPpFCm0olo6Ihp
|
||||
*/
|
||||
export class SqliteClient {
|
||||
private db: Database | null = null;
|
||||
private isInTransaction: boolean = false;
|
||||
|
||||
/**
|
||||
* Initialize the SQLite database from a base64 string
|
||||
@@ -45,6 +44,69 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a new transaction
|
||||
*/
|
||||
public beginTransaction(): void {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
if (this.isInTransaction) {
|
||||
throw new Error('Transaction already in progress');
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.run('BEGIN TRANSACTION');
|
||||
this.isInTransaction = true;
|
||||
} catch (error) {
|
||||
console.error('Error beginning transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the current transaction and persist changes to the vault
|
||||
*/
|
||||
public async commitTransaction(): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
if (!this.isInTransaction) {
|
||||
throw new Error('No transaction in progress');
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.run('COMMIT');
|
||||
this.isInTransaction = false;
|
||||
} catch (error) {
|
||||
console.error('Error committing transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the current transaction
|
||||
*/
|
||||
public rollbackTransaction(): void {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
if (!this.isInTransaction) {
|
||||
throw new Error('No transaction in progress');
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.run('ROLLBACK');
|
||||
this.isInTransaction = false;
|
||||
} catch (error) {
|
||||
console.error('Error rolling back transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the SQLite database to a base64 string
|
||||
* @returns Base64 encoded string of the database
|
||||
@@ -279,9 +341,41 @@ export class SqliteClient {
|
||||
|
||||
/**
|
||||
* Get the default email domain from the database.
|
||||
* @param privateEmailDomains - Array of private email domains
|
||||
* @param publicEmailDomains - Array of public email domains
|
||||
* @returns The default email domain or null if no valid domain is found
|
||||
*/
|
||||
public getDefaultEmailDomain(): string {
|
||||
return this.getSetting('DefaultEmailDomain');
|
||||
public getDefaultEmailDomain(privateEmailDomains: string[], publicEmailDomains: string[]): string | null {
|
||||
const defaultEmailDomain = this.getSetting('DefaultEmailDomain');
|
||||
|
||||
/**
|
||||
* Check if a domain is valid.
|
||||
*/
|
||||
const isValidDomain = (domain: string): boolean => {
|
||||
return Boolean(domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain)));
|
||||
};
|
||||
|
||||
// First check if the default domain that is configured in the vault is still valid.
|
||||
if (defaultEmailDomain && isValidDomain(defaultEmailDomain)) {
|
||||
return defaultEmailDomain;
|
||||
}
|
||||
|
||||
// If default domain is not valid, fall back to first available private domain.
|
||||
const firstPrivate = privateEmailDomains.find(isValidDomain);
|
||||
if (firstPrivate) {
|
||||
return firstPrivate;
|
||||
}
|
||||
|
||||
// Return first valid public domain if no private domains are available.
|
||||
const firstPublic = publicEmailDomains.find(isValidDomain);
|
||||
if (firstPublic) {
|
||||
return firstPublic;
|
||||
}
|
||||
|
||||
// Return null if no valid domains are found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,15 +415,15 @@ export class SqliteClient {
|
||||
/**
|
||||
* Create a new credential with associated entities
|
||||
* @param credential The credential object to insert
|
||||
* @returns The number of rows modified
|
||||
* @returns The ID of the created credential
|
||||
*/
|
||||
public createCredential(credential: Credential): number {
|
||||
public async createCredential(credential: Credential): Promise<string> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.run('BEGIN TRANSACTION');
|
||||
this.beginTransaction();
|
||||
|
||||
// 1. Insert Service
|
||||
let logoData = null;
|
||||
@@ -417,11 +511,11 @@ export class SqliteClient {
|
||||
]);
|
||||
}
|
||||
|
||||
this.db.run('COMMIT');
|
||||
return 1;
|
||||
await this.commitTransaction();
|
||||
return credentialId;
|
||||
|
||||
} catch (error) {
|
||||
this.db.run('ROLLBACK');
|
||||
this.rollbackTransaction();
|
||||
console.error('Error creating credential:', error);
|
||||
throw error;
|
||||
}
|
||||
@@ -502,6 +596,225 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a credential by ID
|
||||
* @param credentialId - The ID of the credential to delete
|
||||
* @returns The number of rows deleted
|
||||
*/
|
||||
public async deleteCredentialById(credentialId: string): Promise<number> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
this.beginTransaction();
|
||||
|
||||
const currentDateTime = new Date().toISOString()
|
||||
.replace('T', ' ')
|
||||
.replace('Z', '')
|
||||
.substring(0, 23);
|
||||
|
||||
// Update the credential, alias, and service to be deleted
|
||||
const query = `
|
||||
UPDATE Credentials
|
||||
SET IsDeleted = 1,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
|
||||
const aliasQuery = `
|
||||
UPDATE Aliases
|
||||
SET IsDeleted = 1,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT AliasId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
const serviceQuery = `
|
||||
UPDATE Services
|
||||
SET IsDeleted = 1,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT ServiceId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
const results = this.executeUpdate(query, [currentDateTime, credentialId]);
|
||||
this.executeUpdate(aliasQuery, [currentDateTime, credentialId]);
|
||||
this.executeUpdate(serviceQuery, [currentDateTime, credentialId]);
|
||||
|
||||
await this.commitTransaction();
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.rollbackTransaction();
|
||||
console.error('Error deleting credential:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing credential with associated entities
|
||||
* @param credential The credential object to update
|
||||
* @returns The number of rows modified
|
||||
*/
|
||||
public async updateCredentialById(credential: Credential): Promise<number> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
this.beginTransaction();
|
||||
const currentDateTime = new Date().toISOString()
|
||||
.replace('T', ' ')
|
||||
.replace('Z', '')
|
||||
.substring(0, 23);
|
||||
|
||||
// Get existing credential to compare changes
|
||||
const existingCredential = this.getCredentialById(credential.Id);
|
||||
if (!existingCredential) {
|
||||
throw new Error('Credential not found');
|
||||
}
|
||||
|
||||
// 1. Update Service
|
||||
const serviceQuery = `
|
||||
UPDATE Services
|
||||
SET Name = ?,
|
||||
Url = ?,
|
||||
Logo = COALESCE(?, Logo),
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT ServiceId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
let logoData = null;
|
||||
try {
|
||||
if (credential.Logo) {
|
||||
// Handle object-like array conversion
|
||||
if (typeof credential.Logo === 'object' && !ArrayBuffer.isView(credential.Logo)) {
|
||||
const values = Object.values(credential.Logo);
|
||||
logoData = new Uint8Array(values);
|
||||
// Handle existing array types
|
||||
} else if (Array.isArray(credential.Logo) || credential.Logo instanceof ArrayBuffer || credential.Logo instanceof Uint8Array) {
|
||||
logoData = new Uint8Array(credential.Logo);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to convert logo to Uint8Array:', error);
|
||||
logoData = null;
|
||||
}
|
||||
|
||||
this.executeUpdate(serviceQuery, [
|
||||
credential.ServiceName,
|
||||
credential.ServiceUrl ?? null,
|
||||
logoData,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 2. Update Alias
|
||||
const aliasQuery = `
|
||||
UPDATE Aliases
|
||||
SET FirstName = ?,
|
||||
LastName = ?,
|
||||
NickName = ?,
|
||||
BirthDate = ?,
|
||||
Gender = ?,
|
||||
Email = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = (
|
||||
SELECT AliasId
|
||||
FROM Credentials
|
||||
WHERE Id = ?
|
||||
)`;
|
||||
|
||||
// Only update BirthDate if it's actually different (accounting for format differences)
|
||||
let birthDate = credential.Alias.BirthDate;
|
||||
if (birthDate && existingCredential.Alias.BirthDate) {
|
||||
const newDate = new Date(birthDate);
|
||||
const existingDate = new Date(existingCredential.Alias.BirthDate);
|
||||
if (newDate.getTime() === existingDate.getTime()) {
|
||||
birthDate = existingCredential.Alias.BirthDate;
|
||||
}
|
||||
}
|
||||
|
||||
this.executeUpdate(aliasQuery, [
|
||||
credential.Alias.FirstName ?? null,
|
||||
credential.Alias.LastName ?? null,
|
||||
credential.Alias.NickName ?? null,
|
||||
birthDate ?? null,
|
||||
credential.Alias.Gender ?? null,
|
||||
credential.Alias.Email ?? null,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 3. Update Credential
|
||||
const credentialQuery = `
|
||||
UPDATE Credentials
|
||||
SET Username = ?,
|
||||
Notes = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
|
||||
this.executeUpdate(credentialQuery, [
|
||||
credential.Username ?? null,
|
||||
credential.Notes ?? null,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
|
||||
// 4. Update Password if changed
|
||||
if (credential.Password !== existingCredential.Password) {
|
||||
// Check if a password record already exists for this credential, if not, then create one.
|
||||
const passwordRecordExistsQuery = `
|
||||
SELECT Id
|
||||
FROM Passwords
|
||||
WHERE CredentialId = ?`;
|
||||
const passwordResults = this.executeQuery(passwordRecordExistsQuery, [credential.Id]);
|
||||
|
||||
if (passwordResults.length === 0) {
|
||||
// Create a new password record
|
||||
const passwordQuery = `
|
||||
INSERT INTO Passwords (Id, Value, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
this.executeUpdate(passwordQuery, [
|
||||
crypto.randomUUID().toUpperCase(),
|
||||
credential.Password,
|
||||
credential.Id,
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
} else {
|
||||
// Update the existing password record
|
||||
const passwordQuery = `
|
||||
UPDATE Passwords
|
||||
SET Value = ?, UpdatedAt = ?
|
||||
WHERE CredentialId = ?`;
|
||||
|
||||
this.executeUpdate(passwordQuery, [
|
||||
credential.Password,
|
||||
currentDateTime,
|
||||
credential.Id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await this.commitTransaction();
|
||||
return 1;
|
||||
|
||||
} catch (error) {
|
||||
this.rollbackTransaction();
|
||||
console.error('Error updating credential:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert binary data to a base64 encoded image source.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { StatusResponse, VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
|
||||
import { AppInfo } from "./AppInfo";
|
||||
import { StatusResponse } from "./types/webapi/StatusResponse";
|
||||
import { VaultResponse } from "./types/webapi/VaultResponse";
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
type RequestInit = globalThis.RequestInit;
|
||||
@@ -42,7 +43,8 @@ export class WebApiService {
|
||||
public async authFetch<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
parseJson: boolean = true
|
||||
parseJson: boolean = true,
|
||||
throwOnError: boolean = true
|
||||
): Promise<T> {
|
||||
const headers = new Headers(options.headers ?? {});
|
||||
|
||||
@@ -80,7 +82,7 @@ export class WebApiService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (!response.ok && throwOnError) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -162,9 +164,9 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue GET request to the API expecting a file download and return it as a Base64 string.
|
||||
* Issue GET request to the API expecting a file download and return it as raw bytes.
|
||||
*/
|
||||
public async downloadBlobAndConvertToBase64(endpoint: string): Promise<string> {
|
||||
public async downloadBlob(endpoint: string): Promise<Uint8Array> {
|
||||
try {
|
||||
const response = await this.authFetch<Response>(endpoint, {
|
||||
method: 'GET',
|
||||
@@ -173,11 +175,11 @@ export class WebApiService {
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Ensure we get the response as a blob
|
||||
const blob = await response.blob();
|
||||
return await this.blobToBase64(blob);
|
||||
// Get the response as an ArrayBuffer
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return new Uint8Array(arrayBuffer);
|
||||
} catch (error) {
|
||||
console.error('Error fetching and converting to Base64:', error);
|
||||
console.error('Error downloading blob:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AppInfo } from '../AppInfo';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { AppInfo } from '../AppInfo';
|
||||
|
||||
describe('AppInfo', () => {
|
||||
describe('isVersionSupported', () => {
|
||||
it('should support exact version match', () => {
|
||||
|
||||
@@ -5,5 +5,5 @@ This folder contains the output of the shared `identity-generator` module from t
|
||||
**Do not edit any of these files manually.**
|
||||
|
||||
To make changes:
|
||||
1. Update the source files in `packages/identity-generator/src`
|
||||
2. Run the `build-and-distribute.sh` script at the root of the project to regenerate the outputs and copy them here.
|
||||
1. Update the source files in the `/shared/identity-generator/src` directory
|
||||
2. Run the `build.sh` script in the module directory to regenerate the outputs and copy them here.
|
||||
@@ -16,6 +16,89 @@ type Identity = {
|
||||
nickName: string;
|
||||
};
|
||||
|
||||
interface IIdentityGenerator {
|
||||
generateRandomIdentity(): Identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base identity generator.
|
||||
*/
|
||||
declare abstract class IdentityGenerator implements IIdentityGenerator {
|
||||
protected firstNamesMale: string[];
|
||||
protected firstNamesFemale: string[];
|
||||
protected lastNames: string[];
|
||||
private readonly random;
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
constructor();
|
||||
protected abstract getFirstNamesMaleJson(): string[];
|
||||
protected abstract getFirstNamesFemaleJson(): string[];
|
||||
protected abstract getLastNamesJson(): string[];
|
||||
/**
|
||||
* Generate a random date of birth.
|
||||
*/
|
||||
protected generateRandomDateOfBirth(): Date;
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
generateRandomIdentity(): Identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity generator for English language using English word dictionaries.
|
||||
*/
|
||||
declare class IdentityGeneratorEn extends IdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
protected getFirstNamesMaleJson(): string[];
|
||||
/**
|
||||
* Get the female first names.
|
||||
*/
|
||||
protected getFirstNamesFemaleJson(): string[];
|
||||
/**
|
||||
* Get the last names.
|
||||
*/
|
||||
protected getLastNamesJson(): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity generator for Dutch language using Dutch word dictionaries.
|
||||
*/
|
||||
declare class IdentityGeneratorNl extends IdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
protected getFirstNamesMaleJson(): string[];
|
||||
/**
|
||||
* Get the female first names.
|
||||
*/
|
||||
protected getFirstNamesFemaleJson(): string[];
|
||||
/**
|
||||
* Get the last names.
|
||||
*/
|
||||
protected getLastNamesJson(): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utilities for identity generation that can be used by multiple client applications.
|
||||
*/
|
||||
declare class IdentityHelperUtils {
|
||||
/**
|
||||
* Normalize a birth date for display.
|
||||
*/
|
||||
static normalizeBirthDateForDisplay(birthDate: string | undefined): string;
|
||||
/**
|
||||
* Normalize a birth date for database.
|
||||
*/
|
||||
static normalizeBirthDateForDb(input: string | undefined): string;
|
||||
/**
|
||||
* Check if a birth date is valid.
|
||||
*/
|
||||
static isValidBirthDate(input: string | undefined): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a username or email prefix.
|
||||
*/
|
||||
@@ -49,87 +132,18 @@ declare class UsernameEmailGenerator {
|
||||
private getSecureRandom;
|
||||
}
|
||||
|
||||
interface IIdentityGenerator {
|
||||
generateRandomIdentity(): Promise<Identity>;
|
||||
}
|
||||
/**
|
||||
* Creates a new identity generator based on the language.
|
||||
* @param language - The language to use for generating the identity (e.g. "en", "nl").
|
||||
* @returns A new identity generator instance.
|
||||
*/
|
||||
declare const CreateIdentityGenerator: (language: string) => IIdentityGenerator;
|
||||
|
||||
/**
|
||||
* Base identity generator.
|
||||
* Creates a new username email generator. This is used by the .NET Blazor WASM JSinterop
|
||||
* as it cannot create instances of classes directly, it has to use a factory method.
|
||||
* @returns A new username email generator instance.
|
||||
*/
|
||||
declare abstract class BaseIdentityGenerator implements IIdentityGenerator {
|
||||
protected firstNamesMale: string[];
|
||||
protected firstNamesFemale: string[];
|
||||
protected lastNames: string[];
|
||||
private readonly random;
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
constructor();
|
||||
protected abstract getFirstNamesMaleJson(): string[];
|
||||
protected abstract getFirstNamesFemaleJson(): string[];
|
||||
protected abstract getLastNamesJson(): string[];
|
||||
/**
|
||||
* Generate a random date of birth.
|
||||
*/
|
||||
protected generateRandomDateOfBirth(): Date;
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
generateRandomIdentity(): Promise<Identity>;
|
||||
}
|
||||
declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator;
|
||||
|
||||
/**
|
||||
* Identity generator for English language using English word dictionaries.
|
||||
*/
|
||||
declare class IdentityGeneratorEn extends BaseIdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
protected getFirstNamesMaleJson(): string[];
|
||||
/**
|
||||
* Get the female first names.
|
||||
*/
|
||||
protected getFirstNamesFemaleJson(): string[];
|
||||
/**
|
||||
* Get the last names.
|
||||
*/
|
||||
protected getLastNamesJson(): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity generator for Dutch language using Dutch word dictionaries.
|
||||
*/
|
||||
declare class IdentityGeneratorNl extends BaseIdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
protected getFirstNamesMaleJson(): string[];
|
||||
/**
|
||||
* Get the female first names.
|
||||
*/
|
||||
protected getFirstNamesFemaleJson(): string[];
|
||||
/**
|
||||
* Get the last names.
|
||||
*/
|
||||
protected getLastNamesJson(): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utilities for identity generation that can be used by multiple client applications.
|
||||
*/
|
||||
declare class IdentityHelperUtils {
|
||||
/**
|
||||
* Normalize a birth date for display.
|
||||
*/
|
||||
static normalizeBirthDateForDisplay(birthDate: string | undefined): string;
|
||||
/**
|
||||
* Normalize a birth date for database.
|
||||
*/
|
||||
static normalizeBirthDateForDb(input: string | undefined): string;
|
||||
/**
|
||||
* Check if a birth date is valid.
|
||||
*/
|
||||
static isValidBirthDate(input: string | undefined): boolean;
|
||||
}
|
||||
|
||||
export { BaseIdentityGenerator, Gender, type Identity, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator };
|
||||
export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator };
|
||||
@@ -1,3 +1,6 @@
|
||||
// <auto-generated>
|
||||
// This file was automatically generated. Do not edit manually.
|
||||
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
@@ -20,8 +23,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
BaseIdentityGenerator: () => BaseIdentityGenerator,
|
||||
CreateIdentityGenerator: () => CreateIdentityGenerator,
|
||||
CreateUsernameEmailGenerator: () => CreateUsernameEmailGenerator,
|
||||
Gender: () => Gender,
|
||||
IdentityGenerator: () => IdentityGenerator,
|
||||
IdentityGeneratorEn: () => IdentityGeneratorEn,
|
||||
IdentityGeneratorNl: () => IdentityGeneratorNl,
|
||||
IdentityHelperUtils: () => IdentityHelperUtils,
|
||||
@@ -29,6 +34,14 @@ __export(index_exports, {
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/types/Gender.ts
|
||||
var Gender = /* @__PURE__ */ ((Gender2) => {
|
||||
Gender2["Male"] = "Male";
|
||||
Gender2["Female"] = "Female";
|
||||
Gender2["Other"] = "Other";
|
||||
return Gender2;
|
||||
})(Gender || {});
|
||||
|
||||
// src/utils/UsernameEmailGenerator.ts
|
||||
var _UsernameEmailGenerator = class _UsernameEmailGenerator {
|
||||
constructor() {
|
||||
@@ -52,6 +65,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator {
|
||||
*/
|
||||
generateEmailPrefix(identity) {
|
||||
const parts = [];
|
||||
if (typeof identity.birthDate === "string") {
|
||||
identity.birthDate = new Date(identity.birthDate);
|
||||
}
|
||||
switch (this.getSecureRandom(4)) {
|
||||
case 0:
|
||||
parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase());
|
||||
@@ -127,16 +143,8 @@ _UsernameEmailGenerator.MIN_LENGTH = 6;
|
||||
_UsernameEmailGenerator.MAX_LENGTH = 20;
|
||||
var UsernameEmailGenerator = _UsernameEmailGenerator;
|
||||
|
||||
// src/types/Gender.ts
|
||||
var Gender = /* @__PURE__ */ ((Gender2) => {
|
||||
Gender2["Male"] = "Male";
|
||||
Gender2["Female"] = "Female";
|
||||
Gender2["Other"] = "Other";
|
||||
return Gender2;
|
||||
})(Gender || {});
|
||||
|
||||
// src/implementations/base/BaseIdentityGenerator.ts
|
||||
var BaseIdentityGenerator = class {
|
||||
// src/implementations/base/IdentityGenerator.ts
|
||||
var IdentityGenerator = class {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
@@ -164,7 +172,7 @@ var BaseIdentityGenerator = class {
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
async generateRandomIdentity() {
|
||||
generateRandomIdentity() {
|
||||
const identity = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@@ -979,7 +987,7 @@ var lastnames_default = [
|
||||
];
|
||||
|
||||
// src/implementations/IdentityGeneratorEn.ts
|
||||
var IdentityGeneratorEn = class extends BaseIdentityGenerator {
|
||||
var IdentityGeneratorEn = class extends IdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
@@ -1643,7 +1651,7 @@ var lastnames_default2 = [
|
||||
];
|
||||
|
||||
// src/implementations/IdentityGeneratorNl.ts
|
||||
var IdentityGeneratorNl = class extends BaseIdentityGenerator {
|
||||
var IdentityGeneratorNl = class extends IdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
@@ -1712,10 +1720,28 @@ var IdentityHelperUtils = class {
|
||||
return yearValid;
|
||||
}
|
||||
};
|
||||
|
||||
// src/factories/IdentityGeneratorFactory.ts
|
||||
var CreateIdentityGenerator = (language) => {
|
||||
switch (language) {
|
||||
case "en":
|
||||
return new IdentityGeneratorEn();
|
||||
case "nl":
|
||||
return new IdentityGeneratorNl();
|
||||
}
|
||||
throw new Error(`Unsupported language: ${language}`);
|
||||
};
|
||||
|
||||
// src/factories/UsernameEmailGeneratorFactory.ts
|
||||
var CreateUsernameEmailGenerator = () => {
|
||||
return new UsernameEmailGenerator();
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
BaseIdentityGenerator,
|
||||
CreateIdentityGenerator,
|
||||
CreateUsernameEmailGenerator,
|
||||
Gender,
|
||||
IdentityGenerator,
|
||||
IdentityGeneratorEn,
|
||||
IdentityGeneratorNl,
|
||||
IdentityHelperUtils,
|
||||
@@ -1,3 +1,15 @@
|
||||
// <auto-generated>
|
||||
// This file was automatically generated. Do not edit manually.
|
||||
|
||||
|
||||
// src/types/Gender.ts
|
||||
var Gender = /* @__PURE__ */ ((Gender2) => {
|
||||
Gender2["Male"] = "Male";
|
||||
Gender2["Female"] = "Female";
|
||||
Gender2["Other"] = "Other";
|
||||
return Gender2;
|
||||
})(Gender || {});
|
||||
|
||||
// src/utils/UsernameEmailGenerator.ts
|
||||
var _UsernameEmailGenerator = class _UsernameEmailGenerator {
|
||||
constructor() {
|
||||
@@ -21,6 +33,9 @@ var _UsernameEmailGenerator = class _UsernameEmailGenerator {
|
||||
*/
|
||||
generateEmailPrefix(identity) {
|
||||
const parts = [];
|
||||
if (typeof identity.birthDate === "string") {
|
||||
identity.birthDate = new Date(identity.birthDate);
|
||||
}
|
||||
switch (this.getSecureRandom(4)) {
|
||||
case 0:
|
||||
parts.push(identity.firstName.substring(0, 1).toLowerCase() + identity.lastName.toLowerCase());
|
||||
@@ -96,16 +111,8 @@ _UsernameEmailGenerator.MIN_LENGTH = 6;
|
||||
_UsernameEmailGenerator.MAX_LENGTH = 20;
|
||||
var UsernameEmailGenerator = _UsernameEmailGenerator;
|
||||
|
||||
// src/types/Gender.ts
|
||||
var Gender = /* @__PURE__ */ ((Gender2) => {
|
||||
Gender2["Male"] = "Male";
|
||||
Gender2["Female"] = "Female";
|
||||
Gender2["Other"] = "Other";
|
||||
return Gender2;
|
||||
})(Gender || {});
|
||||
|
||||
// src/implementations/base/BaseIdentityGenerator.ts
|
||||
var BaseIdentityGenerator = class {
|
||||
// src/implementations/base/IdentityGenerator.ts
|
||||
var IdentityGenerator = class {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
@@ -133,7 +140,7 @@ var BaseIdentityGenerator = class {
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
async generateRandomIdentity() {
|
||||
generateRandomIdentity() {
|
||||
const identity = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@@ -948,7 +955,7 @@ var lastnames_default = [
|
||||
];
|
||||
|
||||
// src/implementations/IdentityGeneratorEn.ts
|
||||
var IdentityGeneratorEn = class extends BaseIdentityGenerator {
|
||||
var IdentityGeneratorEn = class extends IdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
@@ -1612,7 +1619,7 @@ var lastnames_default2 = [
|
||||
];
|
||||
|
||||
// src/implementations/IdentityGeneratorNl.ts
|
||||
var IdentityGeneratorNl = class extends BaseIdentityGenerator {
|
||||
var IdentityGeneratorNl = class extends IdentityGenerator {
|
||||
/**
|
||||
* Get the male first names.
|
||||
*/
|
||||
@@ -1681,9 +1688,27 @@ var IdentityHelperUtils = class {
|
||||
return yearValid;
|
||||
}
|
||||
};
|
||||
|
||||
// src/factories/IdentityGeneratorFactory.ts
|
||||
var CreateIdentityGenerator = (language) => {
|
||||
switch (language) {
|
||||
case "en":
|
||||
return new IdentityGeneratorEn();
|
||||
case "nl":
|
||||
return new IdentityGeneratorNl();
|
||||
}
|
||||
throw new Error(`Unsupported language: ${language}`);
|
||||
};
|
||||
|
||||
// src/factories/UsernameEmailGeneratorFactory.ts
|
||||
var CreateUsernameEmailGenerator = () => {
|
||||
return new UsernameEmailGenerator();
|
||||
};
|
||||
export {
|
||||
BaseIdentityGenerator,
|
||||
CreateIdentityGenerator,
|
||||
CreateUsernameEmailGenerator,
|
||||
Gender,
|
||||
IdentityGenerator,
|
||||
IdentityGeneratorEn,
|
||||
IdentityGeneratorNl,
|
||||
IdentityHelperUtils,
|
||||
9
apps/browser-extension/src/utils/dist/shared/models/README.md
vendored
Normal file
9
apps/browser-extension/src/utils/dist/shared/models/README.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# ⚠️ Auto-Generated Files
|
||||
|
||||
This folder contains the output of the shared `models` module from the `/shared` directory in the AliasVault project.
|
||||
|
||||
**Do not edit any of these files manually.**
|
||||
|
||||
To make changes:
|
||||
1. Update the source files in the `/shared/models/src` directory
|
||||
2. Run the `build.sh` script in the module directory to regenerate the outputs and copy them here.
|
||||
18
apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts
vendored
Normal file
18
apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
type VaultMetadata = {
|
||||
publicEmailDomains: string[];
|
||||
privateEmailDomains: string[];
|
||||
vaultRevisionNumber: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* These parameters for deriving encryption key from plain text password. These are stored
|
||||
* as metadata in the vault upon initial login, and are used to derive the encryption key
|
||||
* from the plain text password in the unlock screen.
|
||||
*/
|
||||
type EncryptionKeyDerivationParams = {
|
||||
encryptionType: string;
|
||||
encryptionSettings: string;
|
||||
salt: string;
|
||||
};
|
||||
|
||||
export type { EncryptionKeyDerivationParams, VaultMetadata };
|
||||
3
apps/browser-extension/src/utils/dist/shared/models/metadata/index.js
vendored
Normal file
3
apps/browser-extension/src/utils/dist/shared/models/metadata/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
//# sourceMappingURL=index.js.map
|
||||
80
apps/browser-extension/src/utils/dist/shared/models/vault/index.d.ts
vendored
Normal file
80
apps/browser-extension/src/utils/dist/shared/models/vault/index.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Encryption key SQLite database type.
|
||||
*/
|
||||
type EncryptionKey = {
|
||||
Id: string;
|
||||
PublicKey: string;
|
||||
PrivateKey: string;
|
||||
IsPrimary: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Settings for password generation stored in SQLite database settings table as string.
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* TotpCode SQLite database type.
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Credential SQLite database type.
|
||||
*/
|
||||
type Credential = {
|
||||
Id: string;
|
||||
Username?: string;
|
||||
Password: string;
|
||||
ServiceName: string;
|
||||
ServiceUrl?: string;
|
||||
Logo?: Uint8Array | number[];
|
||||
Notes?: string;
|
||||
Alias: Alias;
|
||||
};
|
||||
/**
|
||||
* Alias SQLite database type.
|
||||
*/
|
||||
type Alias = {
|
||||
FirstName?: string;
|
||||
LastName?: string;
|
||||
NickName?: string;
|
||||
BirthDate: string;
|
||||
Gender?: string;
|
||||
Email?: string;
|
||||
};
|
||||
|
||||
export type { Alias, Credential, EncryptionKey, PasswordSettings, TotpCode };
|
||||
3
apps/browser-extension/src/utils/dist/shared/models/vault/index.js
vendored
Normal file
3
apps/browser-extension/src/utils/dist/shared/models/vault/index.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
//# sourceMappingURL=index.js.map
|
||||
382
apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts
vendored
Normal file
382
apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts
vendored
Normal file
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* Represents the error response returned by the API.
|
||||
*/
|
||||
type ApiErrorResponse = {
|
||||
/**
|
||||
* The main error message.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* The error code associated with this error.
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Additional details about the error.
|
||||
*/
|
||||
details: Record<string, unknown>;
|
||||
/**
|
||||
* The HTTP status code associated with this error.
|
||||
*/
|
||||
statusCode: number;
|
||||
/**
|
||||
* The timestamp when the error occurred.
|
||||
*/
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vault type.
|
||||
*/
|
||||
type Vault = {
|
||||
blob: string;
|
||||
createdAt: string;
|
||||
credentialsCount: number;
|
||||
currentRevisionNumber: number;
|
||||
emailAddressList: string[];
|
||||
privateEmailDomainList: string[];
|
||||
publicEmailDomainList: string[];
|
||||
encryptionPublicKey: string;
|
||||
updatedAt: string;
|
||||
username: string;
|
||||
version: string;
|
||||
client: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vault response type.
|
||||
*/
|
||||
type VaultResponse = {
|
||||
status: number;
|
||||
vault: Vault;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vault post response type returned after uploading a new vault to the server.
|
||||
*/
|
||||
type VaultPostResponse = {
|
||||
status: number;
|
||||
newRevisionNumber: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Status response type.
|
||||
*/
|
||||
type StatusResponse = {
|
||||
clientVersionSupported: boolean;
|
||||
serverVersion: string;
|
||||
vaultRevision: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Login request type.
|
||||
*/
|
||||
type LoginRequest = {
|
||||
username: string;
|
||||
};
|
||||
/**
|
||||
* Login response type.
|
||||
*/
|
||||
type LoginResponse = {
|
||||
salt: string;
|
||||
serverEphemeral: string;
|
||||
encryptionType: string;
|
||||
encryptionSettings: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate login request type.
|
||||
*/
|
||||
type ValidateLoginRequest = {
|
||||
username: string;
|
||||
rememberMe: boolean;
|
||||
clientPublicEphemeral: string;
|
||||
clientSessionProof: string;
|
||||
};
|
||||
/**
|
||||
* Validate login request type for 2FA.
|
||||
*/
|
||||
type ValidateLoginRequest2Fa = {
|
||||
username: string;
|
||||
code2Fa: number;
|
||||
rememberMe: boolean;
|
||||
clientPublicEphemeral: string;
|
||||
clientSessionProof: string;
|
||||
};
|
||||
/**
|
||||
* Validate login response type.
|
||||
*/
|
||||
type ValidateLoginResponse = {
|
||||
requiresTwoFactor: boolean;
|
||||
token?: {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
serverSessionProof: string;
|
||||
};
|
||||
|
||||
type MailboxEmail = {
|
||||
/** The preview of the email message */
|
||||
messagePreview: string;
|
||||
/** Indicates whether the email has attachments */
|
||||
hasAttachments: boolean;
|
||||
/** The ID of the email */
|
||||
id: number;
|
||||
/** The subject of the email */
|
||||
subject: string;
|
||||
/** The display name of the sender */
|
||||
fromDisplay: string;
|
||||
/** The domain of the sender's email address */
|
||||
fromDomain: string;
|
||||
/** The local part of the sender's email address */
|
||||
fromLocal: string;
|
||||
/** The domain of the recipient's email address */
|
||||
toDomain: string;
|
||||
/** The local part of the recipient's email address */
|
||||
toLocal: string;
|
||||
/** The date of the email */
|
||||
date: string;
|
||||
/** The system date of the email */
|
||||
dateSystem: string;
|
||||
/** The number of seconds ago the email was received */
|
||||
secondsAgo: number;
|
||||
/**
|
||||
* The encrypted symmetric key which was used to encrypt the email message.
|
||||
* This key is encrypted with the public key of the user.
|
||||
*/
|
||||
encryptedSymmetricKey: string;
|
||||
/** The public key of the user used to encrypt the symmetric key */
|
||||
encryptionKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mailbox bulk request type.
|
||||
*/
|
||||
type MailboxBulkRequest = {
|
||||
addresses: string[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
/**
|
||||
* Mailbox bulk response type.
|
||||
*/
|
||||
type MailboxBulkResponse = {
|
||||
addresses: string[];
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalRecords: number;
|
||||
mails: MailboxEmail[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Email attachment type.
|
||||
*/
|
||||
type EmailAttachment = {
|
||||
/** The ID of the attachment */
|
||||
id: number;
|
||||
/** The ID of the email the attachment belongs to */
|
||||
emailId: number;
|
||||
/** The filename of the attachment */
|
||||
filename: string;
|
||||
/** The MIME type of the attachment */
|
||||
mimeType: string;
|
||||
/** The size of the attachment in bytes */
|
||||
filesize: number;
|
||||
};
|
||||
|
||||
type Email = {
|
||||
/** The body of the email message */
|
||||
messageHtml: string;
|
||||
/** The plain text body of the email message */
|
||||
messagePlain: string;
|
||||
/** The ID of the email */
|
||||
id: number;
|
||||
/** The subject of the email */
|
||||
subject: string;
|
||||
/** The display name of the sender */
|
||||
fromDisplay: string;
|
||||
/** The domain of the sender's email address */
|
||||
fromDomain: string;
|
||||
/** The local part of the sender's email address */
|
||||
fromLocal: string;
|
||||
/** The domain of the recipient's email address */
|
||||
toDomain: string;
|
||||
/** The local part of the recipient's email address */
|
||||
toLocal: string;
|
||||
/** The date of the email */
|
||||
date: string;
|
||||
/** The system date of the email */
|
||||
dateSystem: string;
|
||||
/** The number of seconds ago the email was received */
|
||||
secondsAgo: number;
|
||||
/**
|
||||
* The encrypted symmetric key which was used to encrypt the email message.
|
||||
* This key is encrypted with the public key of the user.
|
||||
*/
|
||||
encryptedSymmetricKey: string;
|
||||
/** The public key of the user used to encrypt the symmetric key */
|
||||
encryptionKey: string;
|
||||
/** The attachments of the email */
|
||||
attachments: EmailAttachment[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Auth Log model.
|
||||
*/
|
||||
type AuthLogModel = {
|
||||
/**
|
||||
* Gets or sets the primary key for the auth log entry.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the timestamp of the auth log entry.
|
||||
*/
|
||||
timestamp: string;
|
||||
/**
|
||||
* Gets or sets the type of authentication event.
|
||||
*/
|
||||
eventType: number;
|
||||
/**
|
||||
* Gets or sets the username associated with the auth log entry.
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
* Gets or sets the IP address from which the authentication attempt was made.
|
||||
*/
|
||||
ipAddress: string;
|
||||
/**
|
||||
* Gets or sets the user agent string of the device used for the authentication attempt.
|
||||
*/
|
||||
userAgent: string;
|
||||
/**
|
||||
* Gets or sets the client application name and version.
|
||||
*/
|
||||
client: string;
|
||||
/**
|
||||
* Gets or sets a value indicating whether the authentication attempt was successful.
|
||||
*/
|
||||
isSuccess: boolean;
|
||||
};
|
||||
|
||||
type RefreshToken = {
|
||||
/**
|
||||
* Gets or sets the unique identifier for the refresh token.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Gets or sets the device identifier associated with the refresh token.
|
||||
*/
|
||||
deviceIdentifier: string;
|
||||
/**
|
||||
* Gets or sets the expiration date of the refresh token.
|
||||
*/
|
||||
expireDate: string;
|
||||
/**
|
||||
* Gets or sets the creation date of the refresh token.
|
||||
*/
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type FaviconExtractModel = {
|
||||
image: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a delete account initiate response.
|
||||
*/
|
||||
type DeleteAccountInitiateRequest = {
|
||||
username: string;
|
||||
};
|
||||
/**
|
||||
* Represents a delete account initiate response.
|
||||
*/
|
||||
type DeleteAccountInitiateResponse = {
|
||||
salt: string;
|
||||
serverEphemeral: string;
|
||||
encryptionType: string;
|
||||
encryptionSettings: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a delete account request.
|
||||
*/
|
||||
type DeleteAccountRequest = {
|
||||
username: string;
|
||||
clientPublicEphemeral: string;
|
||||
clientSessionProof: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a password change initiate response.
|
||||
*/
|
||||
type PasswordChangeInitiateResponse = {
|
||||
salt: string;
|
||||
serverEphemeral: string;
|
||||
encryptionType: string;
|
||||
encryptionSettings: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a request to change the users password including a new vault that is encrypted with the new password.
|
||||
*/
|
||||
type VaultPasswordChangeRequest = Vault & {
|
||||
currentClientPublicEphemeral: string;
|
||||
currentClientSessionProof: string;
|
||||
newPasswordSalt: string;
|
||||
newPasswordVerifier: string;
|
||||
};
|
||||
|
||||
type BadRequestResponse = {
|
||||
type: string;
|
||||
title: string;
|
||||
status: number;
|
||||
errors: Record<string, string[]>;
|
||||
traceId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the type of authentication event.
|
||||
*/
|
||||
declare enum AuthEventType {
|
||||
/**
|
||||
* Represents a standard login attempt.
|
||||
*/
|
||||
Login = 1,
|
||||
/**
|
||||
* Represents a two-factor authentication attempt.
|
||||
*/
|
||||
TwoFactorAuthentication = 2,
|
||||
/**
|
||||
* Represents a user logout event.
|
||||
*/
|
||||
Logout = 3,
|
||||
/**
|
||||
* Represents JWT access token refresh event issued by client to API.
|
||||
*/
|
||||
TokenRefresh = 10,
|
||||
/**
|
||||
* Represents a password reset event.
|
||||
*/
|
||||
PasswordReset = 20,
|
||||
/**
|
||||
* Represents a password change event.
|
||||
*/
|
||||
PasswordChange = 21,
|
||||
/**
|
||||
* Represents enabling two-factor authentication in settings.
|
||||
*/
|
||||
TwoFactorAuthEnable = 22,
|
||||
/**
|
||||
* Represents disabling two-factor authentication in settings.
|
||||
*/
|
||||
TwoFactorAuthDisable = 23,
|
||||
/**
|
||||
* Represents a user registration event.
|
||||
*/
|
||||
Register = 30,
|
||||
/**
|
||||
* Represents a user account deletion event.
|
||||
*/
|
||||
AccountDeletion = 99
|
||||
}
|
||||
|
||||
export { type ApiErrorResponse, AuthEventType, type AuthLogModel, type BadRequestResponse, type DeleteAccountInitiateRequest, type DeleteAccountInitiateResponse, type DeleteAccountRequest, type Email, type EmailAttachment, type FaviconExtractModel, type LoginRequest, type LoginResponse, type MailboxBulkRequest, type MailboxBulkResponse, type MailboxEmail, type PasswordChangeInitiateResponse, type RefreshToken, type StatusResponse, type ValidateLoginRequest, type ValidateLoginRequest2Fa, type ValidateLoginResponse, type Vault, type VaultPasswordChangeRequest, type VaultPostResponse, type VaultResponse };
|
||||
22
apps/browser-extension/src/utils/dist/shared/models/webapi/index.js
vendored
Normal file
22
apps/browser-extension/src/utils/dist/shared/models/webapi/index.js
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
// <auto-generated>
|
||||
// This file was automatically generated. Do not edit manually.
|
||||
|
||||
|
||||
// src/webapi/AuthEventType.ts
|
||||
var AuthEventType = /* @__PURE__ */ ((AuthEventType2) => {
|
||||
AuthEventType2[AuthEventType2["Login"] = 1] = "Login";
|
||||
AuthEventType2[AuthEventType2["TwoFactorAuthentication"] = 2] = "TwoFactorAuthentication";
|
||||
AuthEventType2[AuthEventType2["Logout"] = 3] = "Logout";
|
||||
AuthEventType2[AuthEventType2["TokenRefresh"] = 10] = "TokenRefresh";
|
||||
AuthEventType2[AuthEventType2["PasswordReset"] = 20] = "PasswordReset";
|
||||
AuthEventType2[AuthEventType2["PasswordChange"] = 21] = "PasswordChange";
|
||||
AuthEventType2[AuthEventType2["TwoFactorAuthEnable"] = 22] = "TwoFactorAuthEnable";
|
||||
AuthEventType2[AuthEventType2["TwoFactorAuthDisable"] = 23] = "TwoFactorAuthDisable";
|
||||
AuthEventType2[AuthEventType2["Register"] = 30] = "Register";
|
||||
AuthEventType2[AuthEventType2["AccountDeletion"] = 99] = "AccountDeletion";
|
||||
return AuthEventType2;
|
||||
})(AuthEventType || {});
|
||||
|
||||
export { AuthEventType };
|
||||
//# sourceMappingURL=index.js.map
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -5,5 +5,5 @@ This folder contains the output of the shared `password-generator` module from t
|
||||
**Do not edit any of these files manually.**
|
||||
|
||||
To make changes:
|
||||
1. Update the source files in `packages/password-generator/src`
|
||||
2. Run the `build-and-distribute.sh` script at the root of the project to regenerate the outputs and copy them here.
|
||||
1. Update the source files in the `/shared/password-generator/src` directory
|
||||
2. Run the `build.sh` script in the module directory to regenerate the outputs and copy them here.
|
||||
@@ -110,4 +110,11 @@ declare class PasswordGenerator {
|
||||
private addCharacterFromSet;
|
||||
}
|
||||
|
||||
export { PasswordGenerator, type PasswordSettings };
|
||||
/**
|
||||
* Creates a new password generator.
|
||||
* @param settings - The settings for the password generator.
|
||||
* @returns A new password generator instance.
|
||||
*/
|
||||
declare const CreatePasswordGenerator: (settings: PasswordSettings) => PasswordGenerator;
|
||||
|
||||
export { CreatePasswordGenerator, PasswordGenerator, type PasswordSettings };
|
||||
@@ -1,3 +1,6 @@
|
||||
// <auto-generated>
|
||||
// This file was automatically generated. Do not edit manually.
|
||||
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
@@ -20,6 +23,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
CreatePasswordGenerator: () => CreatePasswordGenerator,
|
||||
PasswordGenerator: () => PasswordGenerator
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
@@ -230,8 +234,14 @@ var PasswordGenerator = class {
|
||||
return password.substring(0, pos) + char + password.substring(pos + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// src/factories/PasswordGeneratorFactory.ts
|
||||
var CreatePasswordGenerator = (settings) => {
|
||||
return new PasswordGenerator(settings);
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
CreatePasswordGenerator,
|
||||
PasswordGenerator
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -1,3 +1,7 @@
|
||||
// <auto-generated>
|
||||
// This file was automatically generated. Do not edit manually.
|
||||
|
||||
|
||||
// src/utils/PasswordGenerator.ts
|
||||
var PasswordGenerator = class {
|
||||
/**
|
||||
@@ -204,7 +208,13 @@ var PasswordGenerator = class {
|
||||
return password.substring(0, pos) + char + password.substring(pos + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// src/factories/PasswordGeneratorFactory.ts
|
||||
var CreatePasswordGenerator = (settings) => {
|
||||
return new PasswordGenerator(settings);
|
||||
};
|
||||
export {
|
||||
CreatePasswordGenerator,
|
||||
PasswordGenerator
|
||||
};
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormFields } from "./types/FormFields";
|
||||
import { CombinedFieldPatterns, CombinedGenderOptionPatterns, CombinedStopWords } from "./FieldPatterns";
|
||||
import { FormFields } from "./types/FormFields";
|
||||
|
||||
/**
|
||||
* Form detector.
|
||||
@@ -22,7 +22,7 @@ export class FormDetector {
|
||||
* Detect login forms on the page based on the clicked element.
|
||||
*/
|
||||
public containsLoginForm(): boolean {
|
||||
let formWrapper = this.clickedElement?.closest('form, [role="dialog"]') as HTMLElement | null;
|
||||
let formWrapper = this.getFormWrapper();
|
||||
if (formWrapper?.getAttribute('role') === 'dialog') {
|
||||
// If we hit a dialog, search for form only within the dialog
|
||||
formWrapper = formWrapper.querySelector('form') as HTMLElement | null ?? formWrapper;
|
||||
@@ -58,7 +58,7 @@ export class FormDetector {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formWrapper = this.clickedElement.closest('form') ?? this.document.body;
|
||||
const formWrapper = this.getFormWrapper();
|
||||
return this.detectFormFields(formWrapper);
|
||||
}
|
||||
|
||||
@@ -162,6 +162,13 @@ export class FormDetector {
|
||||
return [domainSuggestion];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the form wrapper element.
|
||||
*/
|
||||
private getFormWrapper(): HTMLElement | null {
|
||||
return this.clickedElement?.closest('form, [role="dialog"]') as HTMLElement | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an element and all its parents are visible.
|
||||
* This checks for display:none, visibility:hidden, and opacity:0
|
||||
@@ -242,73 +249,76 @@ export class FormDetector {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an input field based on common patterns in its attributes.
|
||||
* Find all input/select elements matching patterns and types, ordered by best match.
|
||||
*/
|
||||
private findInputField(
|
||||
private findAllInputFields(
|
||||
form: HTMLFormElement | null,
|
||||
patterns: string[],
|
||||
types: string[],
|
||||
excludeElements: HTMLInputElement[] = []
|
||||
): HTMLInputElement | null {
|
||||
): HTMLInputElement[] {
|
||||
// Query for both standard input elements and any element with a type attribute
|
||||
const candidates = form
|
||||
? form.querySelectorAll<HTMLInputElement>('input, select')
|
||||
: this.document.querySelectorAll<HTMLInputElement>('input, select');
|
||||
? form.querySelectorAll<HTMLElement>('input, select, [type]')
|
||||
: this.document.querySelectorAll<HTMLElement>('input, select, [type]');
|
||||
|
||||
// Track best match and its pattern index
|
||||
let bestMatch: HTMLInputElement | null = null;
|
||||
let bestMatchIndex = patterns.length;
|
||||
const matches: { input: HTMLInputElement; score: number }[] = [];
|
||||
|
||||
for (const input of Array.from(candidates)) {
|
||||
// Skip if this element is already used
|
||||
if (excludeElements.includes(input)) {
|
||||
if (excludeElements.includes(input as HTMLInputElement)) {
|
||||
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();
|
||||
// Get type from either the element's type property or its type attribute
|
||||
const type = input.tagName.toLowerCase() === 'select'
|
||||
? 'select'
|
||||
: (input as HTMLInputElement).type?.toLowerCase() || input.getAttribute('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;
|
||||
if (types.includes('email') && type === 'email') {
|
||||
matches.push({ input: input as HTMLInputElement, score: -1 });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect all text attributes to check
|
||||
const attributes = [
|
||||
const attributesToCheck = [
|
||||
input.id,
|
||||
input.name,
|
||||
input.placeholder
|
||||
].map(attr => attr?.toLowerCase() ?? '');
|
||||
input.getAttribute('name'),
|
||||
input.getAttribute('placeholder')
|
||||
]
|
||||
.map(a => a?.toLowerCase() ?? '');
|
||||
|
||||
// Check for associated labels if input has an ID or name
|
||||
if (input.id || input.name) {
|
||||
const label = this.document.querySelector(`label[for="${input.id || input.name}"]`);
|
||||
if (input.id || input.getAttribute('name')) {
|
||||
const label = this.document.querySelector(`label[for="${input.id || input.getAttribute('name')}"]`);
|
||||
if (label) {
|
||||
attributes.push(label.textContent?.toLowerCase() ?? '');
|
||||
attributesToCheck.push(label.textContent?.toLowerCase() ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for sibling elements with class containing "label"
|
||||
const parent = input.parentElement;
|
||||
if (parent) {
|
||||
const siblings = Array.from(parent.children);
|
||||
for (const sibling of siblings) {
|
||||
if (sibling !== input && Array.from(sibling.classList).some(c => c.toLowerCase().includes('label'))) {
|
||||
attributes.push(sibling.textContent?.toLowerCase() ?? '');
|
||||
for (const sib of Array.from(parent.children)) {
|
||||
if (
|
||||
sib !== input &&
|
||||
Array.from(sib.classList).some(c => c.toLowerCase().includes('label'))
|
||||
) {
|
||||
attributesToCheck.push(sib.textContent?.toLowerCase() ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for parent label and table cell structure
|
||||
let currentElement = input;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let currentElement: HTMLElement | null = input;
|
||||
for (let depth = 0; depth < 5 && currentElement; depth++) {
|
||||
// Stop if we have too many child elements (near body)
|
||||
if (currentElement.children.length > 15) {
|
||||
break;
|
||||
@@ -317,48 +327,65 @@ export class FormDetector {
|
||||
// Check for label - search both parent and child elements
|
||||
const childLabel = currentElement.querySelector('label');
|
||||
if (childLabel) {
|
||||
attributes.push(childLabel.textContent?.toLowerCase() ?? '');
|
||||
attributesToCheck.push(childLabel.textContent?.toLowerCase() ?? '');
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for table cell structure
|
||||
const parentTd = currentElement.closest('td');
|
||||
if (parentTd) {
|
||||
const td = currentElement.closest('td');
|
||||
if (td) {
|
||||
// Get the parent row
|
||||
const parentTr = parentTd.closest('tr');
|
||||
if (parentTr) {
|
||||
const row = td.closest('tr');
|
||||
if (row) {
|
||||
// Check all sibling cells in the row
|
||||
const siblingTds = parentTr.querySelectorAll('td');
|
||||
for (const td of siblingTds) {
|
||||
if (td !== parentTd) { // Skip the cell containing the input
|
||||
attributes.push(td.textContent?.toLowerCase() ?? '');
|
||||
for (const cell of Array.from(row.querySelectorAll('td'))) {
|
||||
if (cell !== td) {
|
||||
attributesToCheck.push(cell.textContent?.toLowerCase() ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // Found table structure, no need to continue up the tree
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentElement.parentElement) {
|
||||
currentElement = currentElement.parentElement as HTMLInputElement;
|
||||
} else {
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
|
||||
let bestIndex = patterns.length;
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
if (attributesToCheck.some(a => a.includes(patterns[i]))) {
|
||||
bestIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the earliest matching pattern
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
if (i >= bestMatchIndex) {
|
||||
break;
|
||||
} // Skip if we already have a better match
|
||||
if (attributes.some(attr => attr.includes(patterns[i]))) {
|
||||
bestMatch = input;
|
||||
bestMatchIndex = i;
|
||||
break; // Found the best possible match for this input
|
||||
}
|
||||
if (bestIndex < patterns.length) {
|
||||
matches.push({ input: input as HTMLInputElement, score: bestIndex });
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
return matches
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.map(m => m.input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single input/select element based on common patterns in its attributes.
|
||||
*/
|
||||
private findInputField(
|
||||
form: HTMLFormElement | null,
|
||||
patterns: string[],
|
||||
types: string[],
|
||||
excludeElements: HTMLInputElement[] = []
|
||||
): HTMLInputElement | null {
|
||||
const all = this.findAllInputFields(form, patterns, types, excludeElements);
|
||||
// if email type explicitly requested, prefer actual <input type="email">
|
||||
if (types.includes('email')) {
|
||||
const emailMatch = all.find(i => (i.type || '').toLowerCase() === 'email');
|
||||
if (emailMatch) {
|
||||
return emailMatch;
|
||||
}
|
||||
}
|
||||
return all.length > 0 ? all[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,15 +573,11 @@ export class FormDetector {
|
||||
primary: HTMLInputElement | null,
|
||||
confirm: HTMLInputElement | null
|
||||
} {
|
||||
const candidates = form
|
||||
? form.querySelectorAll<HTMLInputElement>('input[type="password"]')
|
||||
: this.document.querySelectorAll<HTMLInputElement>('input[type="password"]');
|
||||
|
||||
const visibleCandidates = Array.from(candidates).filter(input => this.isElementVisible(input));
|
||||
const passwordFields = this.findAllInputFields(form, CombinedFieldPatterns.password, ['password']);
|
||||
|
||||
return {
|
||||
primary: visibleCandidates[0] ?? null,
|
||||
confirm: visibleCandidates[1] ?? null
|
||||
primary: passwordFields[0] ?? null,
|
||||
confirm: passwordFields[1] ?? null
|
||||
};
|
||||
}
|
||||
|
||||
@@ -601,6 +624,34 @@ export class FormDetector {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a field is an autofill-triggerable field (username, email, or password).
|
||||
*/
|
||||
public isAutofillTriggerableField(): boolean {
|
||||
// Check if it's a username, email or password field by reusing the existing detection logic
|
||||
const formWrapper = this.getFormWrapper();
|
||||
|
||||
// Check if the clicked element is a username field.
|
||||
const usernameFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text']);
|
||||
if (usernameFields.some(input => input === this.clickedElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the clicked element is a password field.
|
||||
const passwordField = this.findPasswordField(formWrapper as HTMLFormElement | null);
|
||||
if (passwordField.primary === this.clickedElement || passwordField.confirm === this.clickedElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the clicked element is an email field.
|
||||
const emailFields = this.findAllInputFields(formWrapper as HTMLFormElement | null, CombinedFieldPatterns.email, ['text', 'email']);
|
||||
if (emailFields.some(input => input === this.clickedElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a form entry.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Credential } from "@/utils/types/Credential";
|
||||
import { FormFields } from "@/utils/formDetector/types/FormFields";
|
||||
import { Gender, IdentityHelperUtils } from "@/utils/dist/shared/identity-generator";
|
||||
import type { Credential } from "@/utils/dist/shared/models/vault";
|
||||
import { CombinedDateOptionPatterns, CombinedGenderOptionPatterns } from "@/utils/formDetector/FieldPatterns";
|
||||
import { Gender, IdentityHelperUtils } from "@/utils/shared/identity-generator";
|
||||
import { FormFields } from "@/utils/formDetector/types/FormFields";
|
||||
/**
|
||||
* Class to fill the fields of a form with the given credential.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FormField, testField } from './TestUtils';
|
||||
|
||||
describe('FormDetector English tests', () => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createTestDom } from './TestUtils';
|
||||
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
import { createTestDom } from './TestUtils';
|
||||
|
||||
describe('FormDetector generic tests', () => {
|
||||
describe('Invalid form not detected as login form 1', () => {
|
||||
const htmlFile = 'invalid-form1.html';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { FormField, testField, testBirthdateFormat } from './TestUtils';
|
||||
|
||||
describe('FormDetector Dutch tests', () => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createTestDocument } from './TestUtils';
|
||||
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
import { createTestDocument } from './TestUtils';
|
||||
|
||||
describe('FormDetector.getSuggestedServiceName (English)', () => {
|
||||
it('should extract service name from title with divider and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createTestDocument } from './TestUtils';
|
||||
|
||||
import { FormDetector } from '../FormDetector';
|
||||
|
||||
import { createTestDocument } from './TestUtils';
|
||||
|
||||
describe('FormDetector.getSuggestedServiceName (Dutch)', () => {
|
||||
it('should extract service name from title with divider and include domain', () => {
|
||||
const { document, location } = createTestDocument(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { FormFiller } from '../FormFiller';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { FormFiller } from '../FormFiller';
|
||||
import { FormFields } from '../types/FormFields';
|
||||
import { Credential } from '../../types/Credential';
|
||||
|
||||
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
|
||||
|
||||
const { window } = new JSDOM('<!DOCTYPE html>');
|
||||
global.HTMLSelectElement = window.HTMLSelectElement;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { FormFiller } from '../FormFiller';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { FormFiller } from '../FormFiller';
|
||||
import { FormFields } from '../types/FormFields';
|
||||
import { Credential } from '../../types/Credential';
|
||||
|
||||
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
|
||||
|
||||
const { window } = new JSDOM('<!DOCTYPE html>');
|
||||
global.HTMLSelectElement = window.HTMLSelectElement;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { FormFiller } from '../FormFiller';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { FormFiller } from '../FormFiller';
|
||||
import { FormFields } from '../types/FormFields';
|
||||
import { Credential } from '../../types/Credential';
|
||||
|
||||
import { setupTestDOM, createMockFormFields, createMockCredential, wasTriggerCalledFor, createDateSelects } from './TestUtils';
|
||||
|
||||
const { window } = new JSDOM('<!DOCTYPE html>');
|
||||
global.HTMLSelectElement = window.HTMLSelectElement;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { it, expect, vi } from 'vitest';
|
||||
|
||||
import { JSDOM, DOMWindow } from 'jsdom';
|
||||
import { it, expect, vi } from 'vitest';
|
||||
|
||||
import { Gender } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { FormDetector } from '@/utils/formDetector/FormDetector';
|
||||
import { FormFields } from '@/utils/formDetector/types/FormFields';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import { Gender } from '@/utils/shared/identity-generator';
|
||||
|
||||
export enum FormField {
|
||||
Username = 'username',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user