mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
379 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50c401cee4 | ||
|
|
4e09912420 | ||
|
|
6c8843dc5b | ||
|
|
4c4aa4ba26 | ||
|
|
5ac5f54f78 | ||
|
|
d488107b75 | ||
|
|
fe30116b33 | ||
|
|
77ced32206 | ||
|
|
299d1f6075 | ||
|
|
9811e32a73 | ||
|
|
7655773fa3 | ||
|
|
7a5afcac9c | ||
|
|
1ab736fd03 | ||
|
|
018895e8e9 | ||
|
|
0b07a37d73 | ||
|
|
5c0d7fc571 | ||
|
|
d9d84dd90f | ||
|
|
70b7063af2 | ||
|
|
87287e0237 | ||
|
|
477e786454 | ||
|
|
361ea77ab7 | ||
|
|
36237176fd | ||
|
|
e15ecaf793 | ||
|
|
4422ddcaa3 | ||
|
|
e34e96746f | ||
|
|
4c4d51d78e | ||
|
|
e4b12c4617 | ||
|
|
1cf9b5e93c | ||
|
|
6664266c3f | ||
|
|
79af285124 | ||
|
|
66928f74b7 | ||
|
|
c8599ccd9e | ||
|
|
53f69c97af | ||
|
|
11d8c941d2 | ||
|
|
e31f3df45b | ||
|
|
e2aafa3704 | ||
|
|
c2290f3ba4 | ||
|
|
b134ef3aee | ||
|
|
912c486266 | ||
|
|
51901e6ce3 | ||
|
|
0dbe417636 | ||
|
|
6f9528ea2d | ||
|
|
3266f7394e | ||
|
|
9fd5848029 | ||
|
|
0e2d7cabe8 | ||
|
|
2e5b00ea2c | ||
|
|
ff535188da | ||
|
|
bb41207cfe | ||
|
|
5944cd3248 | ||
|
|
0f02412db2 | ||
|
|
db479182f0 | ||
|
|
d5f8516abc | ||
|
|
1682304ae7 | ||
|
|
d0bbf3ac9f | ||
|
|
12492c922d | ||
|
|
3240c3760a | ||
|
|
58801926cc | ||
|
|
39b5c03ae1 | ||
|
|
b01cdc1f52 | ||
|
|
ce0f466f01 | ||
|
|
80e40b3ceb | ||
|
|
70bb8ef3e4 | ||
|
|
00fb290598 | ||
|
|
9d8a2e784f | ||
|
|
e57cb01164 | ||
|
|
6f421bbdc1 | ||
|
|
eaa42196f8 | ||
|
|
e844e20322 | ||
|
|
b53a4334ca | ||
|
|
afe2ba52b5 | ||
|
|
3e82c6e5d0 | ||
|
|
68dbecd536 | ||
|
|
c0c1b75e73 | ||
|
|
8510648b5f | ||
|
|
0e803205c0 | ||
|
|
2fc7ffa509 | ||
|
|
b16fd8e157 | ||
|
|
effeb211ff | ||
|
|
bfc15fcea6 | ||
|
|
6bb204efb9 | ||
|
|
dbc9724377 | ||
|
|
71783f1af2 | ||
|
|
7ead1d270b | ||
|
|
19b89cbfda | ||
|
|
0617ccb42e | ||
|
|
a3d702f2e5 | ||
|
|
3967b0f832 | ||
|
|
867dd90000 | ||
|
|
6ed1be3b91 | ||
|
|
56e065feea | ||
|
|
3b27e647ef | ||
|
|
62732a71f0 | ||
|
|
f3ad61a77a | ||
|
|
0d878f669f | ||
|
|
6fba784cfe | ||
|
|
c46a95cf82 | ||
|
|
bba16e6e14 | ||
|
|
b4c4603868 | ||
|
|
925455b5d6 | ||
|
|
6aa0c2b9df | ||
|
|
1799a2f580 | ||
|
|
615b5b2883 | ||
|
|
006f89b6b7 | ||
|
|
76c60ad200 | ||
|
|
1830dc0ca1 | ||
|
|
c3599c9f26 | ||
|
|
5d050cd278 | ||
|
|
ff57091eef | ||
|
|
64ef5837c0 | ||
|
|
771f372434 | ||
|
|
7690355434 | ||
|
|
822b95d940 | ||
|
|
41b2a959ed | ||
|
|
3e82f78fe9 | ||
|
|
421884e301 | ||
|
|
d149e5aeec | ||
|
|
8b2702cbe3 | ||
|
|
7b1cfd363c | ||
|
|
5e965d7b3f | ||
|
|
d8ac05f325 | ||
|
|
a1c13a15f9 | ||
|
|
f285b36c61 | ||
|
|
c6fa90e00c | ||
|
|
cb8de80f08 | ||
|
|
15bb7f6593 | ||
|
|
516dd524df | ||
|
|
87e58f8546 | ||
|
|
3baaf78689 | ||
|
|
336bbafe27 | ||
|
|
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 |
@@ -14,9 +14,9 @@
|
||||
# Docker containers to apply the changes.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# Set the ports that your AliasVault will be accessible at.
|
||||
# These are the default ports that will be used by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these to any other ports that are available on your system.
|
||||
# Configure the network ports used by AliasVault by the `reverse-proxy` and `smtp` containers.
|
||||
# You can change these if the defaults are in use on your system.
|
||||
# After making changes, re-run the install script to apply them.
|
||||
HTTP_PORT=80
|
||||
HTTPS_PORT=443
|
||||
SMTP_PORT=25
|
||||
|
||||
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
|
||||
158
.github/workflows/browser-extension-build.yml
vendored
158
.github/workflows/browser-extension-build.yml
vendored
@@ -32,8 +32,10 @@ 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"
|
||||
"apps/browser-extension/src/utils/dist/shared/vault-sql"
|
||||
)
|
||||
|
||||
for dir in "${TARGET_DIRS[@]}"; do
|
||||
@@ -42,15 +44,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 +56,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
|
||||
|
||||
41
.github/workflows/docker-build.yml
vendored
41
.github/workflows/docker-build.yml
vendored
@@ -32,6 +32,23 @@ jobs:
|
||||
echo "BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Check local docker-compose.yml for :latest tags
|
||||
run: |
|
||||
# Check for explicit version tags instead of :latest
|
||||
if grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml; then
|
||||
echo "❌ Error: docker-compose.yml contains explicit version tags instead of :latest"
|
||||
echo "Found the following explicit versions:"
|
||||
grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml
|
||||
echo ""
|
||||
echo "All AliasVault images in docker-compose.yml must use ':latest' tags, not explicit versions."
|
||||
echo "Please update docker-compose.yml to use ':latest' for all AliasVault images."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ docker-compose.yml correctly uses :latest tags for all AliasVault images"
|
||||
|
||||
- name: Download install script from current branch
|
||||
run: |
|
||||
INSTALL_SCRIPT_URL="https://raw.githubusercontent.com/$REPO_FULL_NAME/$BRANCH_NAME/install.sh"
|
||||
@@ -125,8 +142,8 @@ jobs:
|
||||
- name: Test reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
output=$(./install.sh reset-admin-password | sed 's/\x1b\[[0-9;]*m//g')
|
||||
if ! echo "$output" | grep -Eq '^\s*Password: [A-Za-z0-9+/=]{8,}'; then
|
||||
echo "Invalid reset-admin-password output"
|
||||
exit 1
|
||||
fi
|
||||
@@ -143,6 +160,21 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Check local docker-compose.yml for :latest tags
|
||||
run: |
|
||||
# Check for explicit version tags instead of :latest
|
||||
if grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml; then
|
||||
echo "❌ Error: docker-compose.yml contains explicit version tags instead of :latest"
|
||||
echo "Found the following explicit versions:"
|
||||
grep -E "ghcr\.io/lanedirt/aliasvault-[^:]+:[0-9]+\.[0-9]+\.[0-9]+" docker-compose.yml
|
||||
echo ""
|
||||
echo "All AliasVault images in docker-compose.yml must use ':latest' tags, not explicit versions."
|
||||
echo "Please update docker-compose.yml to use ':latest' for all AliasVault images."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ docker-compose.yml correctly uses :latest tags for all AliasVault images"
|
||||
|
||||
- name: Create .env file with custom SMTP port
|
||||
run: echo "SMTP_PORT=2525" > .env
|
||||
|
||||
@@ -197,9 +229,10 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Test reset-admin-password output
|
||||
if: ${{ !steps.install_script.outputs.skip_remaining }}
|
||||
run: |
|
||||
output=$(./install.sh reset-admin-password)
|
||||
if ! echo "$output" | grep -E '.*New admin password: [A-Za-z0-9+/=]{8,}.*'; then
|
||||
output=$(./install.sh reset-admin-password | sed 's/\x1b\[[0-9;]*m//g')
|
||||
if ! echo "$output" | grep -Eq '^\s*Password: [A-Za-z0-9+/=]{8,}'; then
|
||||
echo "Invalid reset-admin-password output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
118
.github/workflows/mobile-app-build.yml
vendored
118
.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,10 @@ 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"
|
||||
"utils/dist/shared/vault-sql"
|
||||
)
|
||||
|
||||
for dir in "${TARGET_DIRS[@]}"; do
|
||||
@@ -48,15 +65,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 +77,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 +110,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/**,**/*.sql"
|
||||
} 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/**,**/*.sql"
|
||||
}
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -9,7 +9,6 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.code-workspace
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
@@ -379,6 +378,10 @@ FodyWeavers.xsd
|
||||
# Codebuddy Rider plugin
|
||||
.codebuddy
|
||||
|
||||
# Claude Code
|
||||
.claude
|
||||
CLAUDE.md
|
||||
|
||||
# -------------------
|
||||
# AliasVault specifics
|
||||
# -------------------
|
||||
@@ -419,4 +422,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": {}
|
||||
}
|
||||
18
.vscode/tasks.json
vendored
18
.vscode/tasks.json
vendored
@@ -43,6 +43,20 @@
|
||||
"cwd": "${workspaceFolder}/apps/server/AliasVault.Admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch SMTP Service",
|
||||
"type": "shell",
|
||||
"command": "dotnet watch",
|
||||
"args": [],
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/server/Services/AliasVault.SmtpService"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build and watch Client CSS",
|
||||
"type": "shell",
|
||||
@@ -155,10 +169,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,26 +1,60 @@
|
||||
# Contributing to the source code
|
||||
We welcome contributions to AliasVault. Please read the guidelines on the official AliasVault docs website on how to get your local development environment setup and the general contribution guidelines:
|
||||
# Contributing to AliasVault
|
||||
|
||||
Thanks for your interest in contributing to the AliasVault project! There are a lot of ways to help out.
|
||||
|
||||
## Community Engagement
|
||||
|
||||
Become active in AliasVault's community, helping by:
|
||||
|
||||
- **Answering questions** in our [Discord community](https://discord.gg/DsaXMTEtpF)
|
||||
- **Helping users** with self-hosting setup and troubleshooting
|
||||
- **Reporting bugs** and suggesting improvements
|
||||
- **Participating in discussions** about features and improvements
|
||||
|
||||
## Spreading the Word
|
||||
|
||||
Getting the word out about AliasVault is important so we can reach and help more people to improve their privacy. You can help by:
|
||||
|
||||
- **Sharing on social media** (X, Reddit, Mastodon, etc.)
|
||||
- **Writing blog posts** about your AliasVault experience
|
||||
- **Creating video tutorials** or walkthroughs
|
||||
- **Mentioning AliasVault** in privacy/self-hosting discussions
|
||||
- **Telling friends and colleagues** about the project
|
||||
|
||||
## Contributing to the Documentation
|
||||
|
||||
The docs are built using Jekyll and automatically deploy to GitHub Pages via GitHub Actions. You can build the docs locally by running `docker compose up` in the `./docs` folder.
|
||||
|
||||
The docs site is based on the open-source template called Just The Docs. Find more information about how this template works in the [official docs](https://just-the-docs.github.io/just-the-docs/).
|
||||
|
||||
To make changes to the AliasVault documentation please make a PR that directly edits the `docs` markdown files in this repository.
|
||||
|
||||
## Contributing to the Main Codebase
|
||||
### Get in contact
|
||||
If you’re planning to work on a new feature or improvement for AliasVault, we strongly encourage you to get in touch with us first. This ensures that your proposed changes align with the project's direction and increases the likelihood of your work being accepted into the official repository. You can reach us through:
|
||||
|
||||
- Opening an issue on GitHub to discuss your proposed changes
|
||||
- Reaching out via Discord or email
|
||||
- Contacting the maintainers directly
|
||||
|
||||
### Set up your local development environment
|
||||
You can find instructions on how to get your local development environment setup for the different parts of the AliasVault codebase here:
|
||||
|
||||
https://docs.aliasvault.net/misc/dev/
|
||||
|
||||
> Tip: if the URL above is not available, the raw doc pages can also be found in the `docs` folder in this repository.
|
||||
|
||||
## Contributing to the documentation
|
||||
The docs are built using Jekyll and automatically deploy to GitHub Pages via GitHub Actions. You can build the docs locally by running `docker compose up` in the `./docs` folder.
|
||||
If you run into any issues, feel free to join our [Discord](https://discord.gg/DsaXMTEtpF) to chat with the maintainers and author.
|
||||
|
||||
The docs site is based on the open-source template called Just The Docs. Find more information about how this template works in the [official docs](https://just-the-docs.github.io/just-the-docs/).
|
||||
## License and Contributions
|
||||
|
||||
AliasVault is licensed under the GNU Affero General Public License v3.0 (AGPLv3). By submitting code, documentation, or other contributions to this project, you agree that:
|
||||
|
||||
To make changes to the AliasVault documentation please make a PR that directly edits the `docs` markdown files in this repository.
|
||||
1. Your contribution will be licensed under the same AGPLv3 license as the project
|
||||
2. You have the legal right to grant this license (e.g., you are the author, or have permission)
|
||||
3. You understand that your contribution will be made public under the AGPLv3 terms
|
||||
4. You are not expected to provide support or warranties for your contribution
|
||||
|
||||
## Contributor License Agreement (CLA)
|
||||
Thank you for your interest in contributing to AliasVault (“Project”).
|
||||
✅ There is no Contributor License Agreement (CLA) required. We believe in a balanced open source model where all contributors are treated equally under the terms of the AGPLv3.
|
||||
|
||||
By submitting code, documentation, or other contributions to this Project, you agree to the following:
|
||||
|
||||
1. You are legally entitled to grant this license (e.g., you are the author, or have permission).
|
||||
2. You grant the Project maintainers a perpetual, worldwide, non-exclusive, royalty-free license to use, modify, distribute, and sublicense your contribution as part of the Project and any derivative works.
|
||||
3. You understand that your contribution will be made public and licensed under the same terms as the Project (e.g., AGPLv3), or any later version the maintainers may release.
|
||||
4. You are not expected to provide support or warranties for your contribution.
|
||||
|
||||
> All contributors must accept the CLA as a condition of contributing. By opening a pull request, you agree to these terms. We may enforce this automatically via GitHub if needed.
|
||||
> By opening a pull request, you agree to these terms. Your contributions will be published under the AGPLv3 license.
|
||||
|
||||
32
README.md
32
README.md
@@ -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.
|
||||
AliasVault is a privacy-first password and email alias manager. Create unique identities, strong passwords, and random email aliases for every website you use. Fully end-to-end encrypted, with a built-in email server and zero third-party dependencies.
|
||||
|
||||
[<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)
|
||||
@@ -8,12 +8,12 @@ End-to-end encrypted password manager with built-in alias and email generation
|
||||
|
||||
<a href="https://app.aliasvault.net">Try the cloud version 🔥</a> | <a href="https://aliasvault.net?utm_source=gh-readme">Website </a> | <a href="https://docs.aliasvault.net?utm_source=gh-readme">Documentation </a> | <a href="#self-hosting">Self-host instructions</a>
|
||||
|
||||
⭐ Star us on GitHub — it motivates us a lot!
|
||||
|
||||
## About
|
||||
AliasVault helps protect your privacy online by generating a unique password, identity, and email alias for every service you use. Everything is end-to-end encrypted and under your control — whether in the cloud or self-hosted.
|
||||
Built on 15 years of experience, AliasVault is independent, open-source, self-hostable and community-driven. It’s the response to a web that tracks everything: a way to take back control of your digital privacy and help you stay secure online.
|
||||
|
||||
Built on 15 years of experience, AliasVault is open-source, self-hostable and community-driven. It’s the response to a web that tracks everything: a way to take back control of your digital privacy and help you stay secure online.
|
||||
|
||||
– Leendert de Borst (@lanedirt), Creator of AliasVault
|
||||
– Leendert de Borst ([@lanedirt](https://github.com/lanedirt)), Creator of AliasVault
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -47,7 +47,17 @@ 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 (universal)](https://app.aliasvault.net)
|
||||
- [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)
|
||||
|
||||
<p>
|
||||
<a href="https://apps.apple.com/app/id6745490915" style="display: inline-block; margin-right: 20px;"><img src="https://github.com/user-attachments/assets/bad09b85-2635-4e3e-b154-9f348b88f6d6" style="height: 40px;margin-right:10px;" alt="Download on the App Store"></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=net.aliasvault.app" style="display: inline-block;"><img src="https://github.com/user-attachments/assets/b28979c9-f4b8-4090-8735-e384a7fdaa47" style="height: 40px;" alt="Get it on Google Play"></a>
|
||||
</p>
|
||||
|
||||
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
|
||||
|
||||
@@ -62,7 +72,7 @@ This method uses pre-built Docker images and works on minimal hardware specifica
|
||||
- 1 vCPU
|
||||
- 1GB RAM
|
||||
- 16GB disk space
|
||||
- Docker installed
|
||||
- Docker (20.10+) and Docker Compose (2.0+)
|
||||
|
||||
```bash
|
||||
# Download install script from latest stable release
|
||||
@@ -114,8 +124,9 @@ 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
|
||||
- [x] Editing in browser extension
|
||||
- [ ] Data model and usability improvements (more flexible aliases and credential types, folder support, bulk selecting etc.)
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
@@ -127,5 +138,4 @@ Feel free to open an issue or join our [Discord](https://discord.gg/DsaXMTEtpF)!
|
||||
### Support the mission
|
||||
Your donation helps me dedicate more time and resources to improving AliasVault, making the internet safer for everyone!
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
||||
|
||||
<a href="https://www.buymeacoffee.com/lanedirt" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 50px !important;" ></a>
|
||||
|
||||
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.20.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 = 23;
|
||||
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.20.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 = 23;
|
||||
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.20.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 = 23;
|
||||
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.20.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 = 23;
|
||||
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.20.2;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
@media (max-width: 380px) {
|
||||
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, handleGetDefaultIdentitySettings, 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,25 +16,30 @@ 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());
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DEFAULT_IDENTITY_LANGUAGE', () => handleGetDefaultIdentityLanguage());
|
||||
onMessage('GET_DEFAULT_IDENTITY_SETTINGS', () => handleGetDefaultIdentitySettings());
|
||||
onMessage('GET_PASSWORD_SETTINGS', () => handleGetPasswordSettings());
|
||||
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
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.
|
||||
@@ -60,9 +62,7 @@ export function handleContextMenuClick(info: Browser.contextMenus.OnClickData, t
|
||||
args: [password]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (info.menuItemId === "aliasvault-activate-form" && tab?.id) {
|
||||
} else if (info.menuItemId === "aliasvault-activate-form" && tab?.id) {
|
||||
// First get the active element's identifier
|
||||
browser.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
|
||||
@@ -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,21 +1,23 @@
|
||||
/* 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 { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
|
||||
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.
|
||||
* Check if the user is logged in and if the vault is locked, and also check for pending migrations.
|
||||
*/
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> {
|
||||
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean, hasPendingMigrations: boolean, error?: string }> {
|
||||
const username = await storage.getItem('local:username');
|
||||
const accessToken = await storage.getItem('local:accessToken');
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
@@ -23,10 +25,42 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData === null;
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked
|
||||
};
|
||||
// If vault is locked, we can't check for pending migrations
|
||||
if (isVaultLocked) {
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations: false
|
||||
};
|
||||
}
|
||||
|
||||
// If not logged in, no need to check migrations
|
||||
if (!isLoggedIn) {
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations: false
|
||||
};
|
||||
}
|
||||
|
||||
// Vault is unlocked, check for pending migrations
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const hasPendingMigrations = await sqliteClient.hasPendingMigrations();
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking pending migrations:', error);
|
||||
return {
|
||||
isLoggedIn,
|
||||
isVaultLocked,
|
||||
hasPendingMigrations: false,
|
||||
error: error instanceof Error ? error.message : 'An unknown error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,17 +70,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 +259,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' };
|
||||
@@ -260,18 +277,25 @@ export function handleGetDefaultEmailDomain(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity language.
|
||||
* Get the default identity settings.
|
||||
*/
|
||||
export async function handleGetDefaultIdentityLanguage(
|
||||
) : Promise<stringResponse> {
|
||||
export async function handleGetDefaultIdentitySettings(
|
||||
) : Promise<IdentitySettingsResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const settingValue = sqliteClient.getDefaultIdentityLanguage();
|
||||
const language = sqliteClient.getDefaultIdentityLanguage();
|
||||
const gender = sqliteClient.getDefaultIdentityGender();
|
||||
|
||||
return { success: true, value: settingValue };
|
||||
return {
|
||||
success: true,
|
||||
settings: {
|
||||
language,
|
||||
gender
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting default identity language:', error);
|
||||
return { success: false, error: 'Failed to get default identity language' };
|
||||
console.error('Error getting default identity settings:', error);
|
||||
return { success: false, error: 'Failed to get default identity settings' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,10 +324,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;
|
||||
|
||||
@@ -335,7 +431,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<void
|
||||
client: '', // Empty on purpose, API will not use this for vault updates.
|
||||
updatedAt: new Date().toISOString(),
|
||||
username: username,
|
||||
version: sqliteClient.getDatabaseVersion() ?? '0.0.0'
|
||||
version: sqliteClient.getDatabaseVersion().version
|
||||
};
|
||||
|
||||
const webApi = new WebApiService(() => {});
|
||||
@@ -347,6 +443,8 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<void
|
||||
} else {
|
||||
throw new Error('Failed to upload new vault to server');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,7 +453,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, createUpgradeRequiredPopup } 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,13 +58,18 @@ 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);
|
||||
|
||||
// Only show popup if debounce time has passed
|
||||
if (popupDebounceTimeHasPassed()) {
|
||||
openAutofillPopup(inputElement, container);
|
||||
await showPopupWithAuthCheck(inputElement, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,13 +125,55 @@ 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);
|
||||
await showPopupWithAuthCheck(inputElement, container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popup with auth check.
|
||||
*/
|
||||
async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement) : Promise<void> {
|
||||
try {
|
||||
// Check auth status and pending migrations in a single call
|
||||
const { sendMessage } = await import('webext-bridge/content-script');
|
||||
const authStatus = await sendMessage('CHECK_AUTH_STATUS', {}, 'background') as {
|
||||
isLoggedIn: boolean,
|
||||
isVaultLocked: boolean,
|
||||
hasPendingMigrations: boolean,
|
||||
error?: string
|
||||
};
|
||||
|
||||
if (authStatus.isVaultLocked) {
|
||||
// Vault is locked, show vault locked popup
|
||||
const { createVaultLockedPopup } = await import('@/entrypoints/contentScript/Popup');
|
||||
createVaultLockedPopup(inputElement, container);
|
||||
return;
|
||||
}
|
||||
|
||||
if (authStatus.hasPendingMigrations) {
|
||||
// Show upgrade required popup
|
||||
createUpgradeRequiredPopup(inputElement, container, 'Vault upgrade required.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (authStatus.error) {
|
||||
// Show upgrade required popup for version-related errors
|
||||
createUpgradeRequiredPopup(inputElement, container, authStatus.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// No upgrade required, show normal autofill popup
|
||||
openAutofillPopup(inputElement, container);
|
||||
} catch (error) {
|
||||
console.error('Error checking vault status:', error);
|
||||
// Fall back to normal autofill popup if check fails
|
||||
openAutofillPopup(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,20 @@
|
||||
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 { 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 { 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 { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
|
||||
import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
|
||||
import { StringResponse } from '@/utils/types/messaging/StringResponse';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* WeakMap to store event listeners for popup containers
|
||||
@@ -242,24 +245,22 @@ 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 identitySettings = await sendMessage('GET_DEFAULT_IDENTITY_SETTINGS', {}, 'background') as IdentitySettingsResponse;
|
||||
const identityGenerator = CreateIdentityGenerator(identitySettings.settings?.language ?? 'en');
|
||||
const identity = identityGenerator.generateRandomIdentity(identitySettings.settings?.gender);
|
||||
|
||||
// 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 +947,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 +977,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);
|
||||
|
||||
@@ -1455,3 +1463,92 @@ function addReliableClickHandler(element: HTMLElement, handler: (e: Event) => vo
|
||||
isMouseDown = false;
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create upgrade required popup.
|
||||
*/
|
||||
export function createUpgradeRequiredPopup(input: HTMLInputElement, rootContainer: HTMLElement, errorMessage: string): void {
|
||||
/**
|
||||
* Handle upgrade click.
|
||||
*/
|
||||
const handleUpgradeClick = () : void => {
|
||||
sendMessage('OPEN_POPUP', {}, 'background');
|
||||
removeExistingPopup(rootContainer);
|
||||
}
|
||||
|
||||
const popup = createBasePopup(input, rootContainer);
|
||||
popup.classList.add('av-upgrade-required');
|
||||
|
||||
// Create container for message and button
|
||||
const container = document.createElement('div');
|
||||
container.className = 'av-upgrade-required-container';
|
||||
|
||||
// Make the entire container clickable
|
||||
addReliableClickHandler(container, handleUpgradeClick);
|
||||
container.style.cursor = 'pointer';
|
||||
|
||||
// Add message
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.className = 'av-upgrade-required-message';
|
||||
messageElement.textContent = errorMessage;
|
||||
container.appendChild(messageElement);
|
||||
|
||||
// Add upgrade button with SVG icon
|
||||
const button = document.createElement('button');
|
||||
button.title = 'Open AliasVault to upgrade';
|
||||
button.className = 'av-upgrade-required-button';
|
||||
button.innerHTML = `
|
||||
<svg class="av-icon-upgrade" viewBox="0 0 24 24">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
||||
</svg>
|
||||
`;
|
||||
container.appendChild(button);
|
||||
|
||||
// Add the container to the popup
|
||||
popup.appendChild(container);
|
||||
|
||||
// Add close button as a separate element positioned to the right
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.className = 'av-button av-button-close av-upgrade-required-close';
|
||||
closeButton.title = 'Dismiss popup';
|
||||
closeButton.innerHTML = `
|
||||
<svg class="av-icon" viewBox="0 0 24 24">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Position the close button to the right of the container
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.right = '8px';
|
||||
closeButton.style.top = '50%';
|
||||
closeButton.style.transform = 'translateY(-50%)';
|
||||
|
||||
// Handle close button click
|
||||
addReliableClickHandler(closeButton, (e) => {
|
||||
e.stopPropagation(); // Prevent opening the upgrade popup
|
||||
removeExistingPopup(rootContainer);
|
||||
});
|
||||
|
||||
popup.appendChild(closeButton);
|
||||
|
||||
/**
|
||||
* Add event listener to document to close popup when clicking outside.
|
||||
*/
|
||||
const handleClickOutside = (event: MouseEvent): void => {
|
||||
const target = event.target as Node;
|
||||
const targetElement = event.target as HTMLElement;
|
||||
|
||||
// Check if the click is outside the popup and outside the shadow UI
|
||||
if (popup && !popup.contains(target) && !input.contains(target) && targetElement.tagName !== 'ALIASVAULT-UI') {
|
||||
removeExistingPopup(rootContainer);
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}, 100);
|
||||
|
||||
rootContainer.appendChild(popup);
|
||||
}
|
||||
|
||||
@@ -299,6 +299,71 @@ body {
|
||||
border: 1px solid #6f6f6f;
|
||||
}
|
||||
|
||||
/* Upgrade Required Popup */
|
||||
.av-upgrade-required {
|
||||
padding: 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.av-upgrade-required:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.av-upgrade-required-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
width: 100%;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.av-upgrade-required-message {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
flex-grow: 1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.av-upgrade-required-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding-right: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #f59e0b;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.av-upgrade-required-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
border: 1px solid #6f6f6f;
|
||||
}
|
||||
|
||||
.av-icon-upgrade {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
/* Create Name Popup */
|
||||
.av-create-popup-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
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 Header from '@/entrypoints/popup/components/Layout/Header';
|
||||
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 { 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 Index from '@/entrypoints/popup/pages/Index';
|
||||
import Login from '@/entrypoints/popup/pages/Login';
|
||||
import Logout from '@/entrypoints/popup/pages/Logout';
|
||||
import Reinitialize from '@/entrypoints/popup/pages/Reinitialize';
|
||||
import Settings from '@/entrypoints/popup/pages/Settings';
|
||||
import Unlock from '@/entrypoints/popup/pages/Unlock';
|
||||
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
|
||||
import Upgrade from '@/entrypoints/popup/pages/Upgrade';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
import '@/entrypoints/popup/style.css';
|
||||
|
||||
/**
|
||||
@@ -35,13 +45,21 @@ 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: '/', element: <Index />, showBackButton: false },
|
||||
{ path: '/reinitialize', element: <Reinitialize />, showBackButton: false },
|
||||
{ path: '/login', element: <Login />, showBackButton: false },
|
||||
{ path: '/unlock', element: <Unlock />, showBackButton: false },
|
||||
{ path: '/unlock-success', element: <UnlockSuccess />, showBackButton: false },
|
||||
{ path: '/upgrade', element: <Upgrade />, 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 +85,44 @@ 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}
|
||||
/>
|
||||
<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,40 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
/**
|
||||
* Global state change handler component which listens for global state changes and e.g. redirects user to login
|
||||
* page if login state changes.
|
||||
*/
|
||||
const GlobalStateChangeHandler: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const lastLoginState = useRef(authContext.isLoggedIn);
|
||||
const initialRender = useRef(true);
|
||||
|
||||
/**
|
||||
* Listen for auth logged in changes and redirect to home page if logged in state changes to handle logins and logouts.
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Only navigate when auth state is different from the last state we acted on.
|
||||
if (lastLoginState.current !== authContext.isLoggedIn) {
|
||||
lastLoginState.current = authContext.isLoggedIn;
|
||||
|
||||
/**
|
||||
* Skip the first auth state change to avoid redirecting when popup opens for the first time
|
||||
* which already causes the auth state to change from false to true.
|
||||
*/
|
||||
if (initialRender.current) {
|
||||
initialRender.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to home page if logged in state changes.
|
||||
navigate('/');
|
||||
}
|
||||
}, [authContext.isLoggedIn]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default GlobalStateChangeHandler;
|
||||
@@ -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,186 @@
|
||||
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',
|
||||
TAB = 'tab'
|
||||
}
|
||||
|
||||
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>
|
||||
),
|
||||
[HeaderIconType.TAB]: (
|
||||
<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="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
return icons[type] || null;
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
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';
|
||||
|
||||
type TabName = 'credentials' | 'emails' | 'settings';
|
||||
|
||||
@@ -9,17 +7,19 @@ type TabName = 'credentials' | 'emails' | 'settings';
|
||||
* Bottom nav component.
|
||||
*/
|
||||
const BottomNav: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [currentTab, setCurrentTab] = useState<TabName>('credentials');
|
||||
|
||||
// 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]);
|
||||
|
||||
@@ -31,7 +31,11 @@ const BottomNav: React.FC = () => {
|
||||
navigate(`/${tab}`);
|
||||
};
|
||||
|
||||
if (!authContext.isLoggedIn || !dbContext.dbAvailable) {
|
||||
// Auth pages that don't show bottom navigation but still show header
|
||||
const authPages = ['/login', '/auth-settings', '/unlock', '/unlock-success', '/upgrade'];
|
||||
const isAuthPage = authPages.includes(location.pathname);
|
||||
|
||||
if (isAuthPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -58,6 +45,11 @@ const Header: React.FC<HeaderProps> = ({
|
||||
* Handle logo click.
|
||||
*/
|
||||
const logoClick = () : void => {
|
||||
// Don't navigate if on upgrade page or login page
|
||||
if (location.pathname === '/upgrade' || location.pathname === '/login' || location.pathname === '/unlock') {
|
||||
return;
|
||||
}
|
||||
|
||||
// If logged in, navigate to credentials.
|
||||
if (authContext.isLoggedIn) {
|
||||
navigate('/credentials');
|
||||
@@ -105,33 +97,25 @@ const Header: React.FC<HeaderProps> = ({
|
||||
|
||||
<div className="flex-grow" />
|
||||
|
||||
<div className="flex items-center">
|
||||
{!currentRoute?.showBackButton ? (
|
||||
<button
|
||||
onClick={openClientTab}
|
||||
className="p-2"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (<></>)}
|
||||
<div className="flex items-center gap-2">
|
||||
{!authContext.isLoggedIn ? (
|
||||
<>
|
||||
{rightButtons}
|
||||
<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>
|
||||
</>
|
||||
) : (
|
||||
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,91 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } 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 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.
|
||||
*/
|
||||
const toggleUserMenu = () : void => {
|
||||
setIsUserMenuOpen(!isUserMenuOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logging out.
|
||||
*/
|
||||
const onLogout = async () : Promise<void> => {
|
||||
showLoading();
|
||||
navigate('/logout', { replace: true });
|
||||
hideLoading();
|
||||
};
|
||||
|
||||
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}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserMenu;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { storage } from '#imports';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
|
||||
/**
|
||||
* Component for displaying the login server information.
|
||||
*/
|
||||
const LoginServerInfo: React.FC = () => {
|
||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Loads the base URL for the login server.
|
||||
*/
|
||||
const loadApiUrl = async () : Promise<void> => {
|
||||
const apiUrl = await storage.getItem('local:apiUrl') as string;
|
||||
setBaseUrl(apiUrl ?? AppInfo.DEFAULT_API_URL);
|
||||
};
|
||||
loadApiUrl();
|
||||
}, []);
|
||||
|
||||
const isDefaultServer = !baseUrl || baseUrl === AppInfo.DEFAULT_API_URL;
|
||||
const displayUrl = isDefaultServer ? 'aliasvault.net' : new URL(baseUrl).hostname;
|
||||
}, [loadApiUrl]);
|
||||
|
||||
/**
|
||||
* Handles the click event for the login server information.
|
||||
@@ -39,7 +32,7 @@ const LoginServerInfo: React.FC = () => {
|
||||
type="button"
|
||||
className="text-orange-500 hover:text-orange-600 dark:text-orange-400 dark:hover:text-orange-500 underline"
|
||||
>
|
||||
{displayUrl}
|
||||
{getDisplayUrl()}
|
||||
</button>)
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
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 = '',
|
||||
cancelText = '',
|
||||
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">
|
||||
{confirmText && (
|
||||
<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>
|
||||
)}
|
||||
{cancelText && (
|
||||
<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,32 @@ 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 }> => {
|
||||
let isLoggedIn = false;
|
||||
|
||||
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);
|
||||
isLoggedIn = true;
|
||||
}
|
||||
setIsInitialized(true);
|
||||
|
||||
return { isLoggedIn };
|
||||
}, [setUsername, setIsLoggedIn]);
|
||||
|
||||
/**
|
||||
* 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 +111,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,19 +1,22 @@
|
||||
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;
|
||||
dbInitialized: boolean;
|
||||
dbAvailable: boolean;
|
||||
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<void>;
|
||||
initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise<SqliteClient>;
|
||||
clearDatabase: () => void;
|
||||
vaultRevision: number;
|
||||
publicEmailDomains: string[];
|
||||
privateEmailDomains: string[];
|
||||
getVaultMetadata: () => Promise<VaultMetadata | null>;
|
||||
setCurrentVaultRevisionNumber: (revisionNumber: number) => Promise<void>;
|
||||
hasPendingMigrations: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const DbContext = createContext<DbContextType | undefined>(undefined);
|
||||
@@ -37,20 +40,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 +59,26 @@ 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');
|
||||
|
||||
return client;
|
||||
}, []);
|
||||
|
||||
const checkStoredVault = useCallback(async () => {
|
||||
@@ -89,9 +91,12 @@ 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 +108,34 @@ 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 there are pending migrations.
|
||||
*/
|
||||
const hasPendingMigrations = useCallback(async () => {
|
||||
if (!sqliteClient) {
|
||||
return false;
|
||||
}
|
||||
return await sqliteClient.hasPendingMigrations();
|
||||
}, [sqliteClient]);
|
||||
|
||||
/**
|
||||
* Check if database is initialized and try to retrieve vault from background
|
||||
*/
|
||||
@@ -118,6 +151,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
const clearDatabase = useCallback(() : void => {
|
||||
setSqliteClient(null);
|
||||
setDbInitialized(false);
|
||||
setDbAvailable(false);
|
||||
sendMessage('CLEAR_VAULT', {}, 'background');
|
||||
}, []);
|
||||
|
||||
@@ -127,10 +161,10 @@ 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,
|
||||
hasPendingMigrations,
|
||||
}), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata, setCurrentVaultRevisionNumber, hasPendingMigrations]);
|
||||
|
||||
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,102 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
const LAST_VISITED_PAGE_KEY = 'session:lastVisitedPage';
|
||||
const LAST_VISITED_TIME_KEY = 'session:lastVisitedTime';
|
||||
const NAVIGATION_HISTORY_KEY = 'session:navigationHistory';
|
||||
|
||||
type NavigationHistoryEntry = {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
type NavigationContextType = {
|
||||
storeCurrentPage: () => Promise<void>;
|
||||
isFullyInitialized: boolean;
|
||||
requiresAuth: boolean;
|
||||
};
|
||||
|
||||
const NavigationContext = createContext<NavigationContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Navigation provider component that handles storing the last visited page.
|
||||
*/
|
||||
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
|
||||
// Auth and DB state
|
||||
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
|
||||
const { dbInitialized, dbAvailable, upgradeRequired } = useDb();
|
||||
|
||||
// Derived state
|
||||
const isFullyInitialized = authInitialized && dbInitialized;
|
||||
const requiresAuth = isFullyInitialized && (!isLoggedIn || (!dbAvailable && !upgradeRequired));
|
||||
|
||||
/**
|
||||
* 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 = ['/', '/reinitialize', '/login', '/unlock', '/unlock-success', '/auth-settings', '/upgrade', '/logout'];
|
||||
|
||||
// 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]);
|
||||
|
||||
// Store the current page whenever it changes
|
||||
useEffect(() => {
|
||||
if (isFullyInitialized) {
|
||||
storeCurrentPage();
|
||||
}
|
||||
}, [location.pathname, location.search, location.hash, isFullyInitialized, storeCurrentPage]);
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
storeCurrentPage,
|
||||
isFullyInitialized,
|
||||
requiresAuth
|
||||
}), [storeCurrentPage, 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,164 @@
|
||||
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;
|
||||
skipSyncCheck?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
|
||||
} else if (response.status === 2) {
|
||||
throw new Error('Your vault is outdated. Please login on the AliasVault website and follow the steps.');
|
||||
} else {
|
||||
throw new Error('Failed to upload vault to server. Please try again by re-opening the app.');
|
||||
}
|
||||
} 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');
|
||||
|
||||
// Skip sync check if requested (e.g., during upgrade operations)
|
||||
if (options.skipSyncCheck) {
|
||||
setSyncStatus('Executing operation...');
|
||||
await executeMutateOperation(operation, options);
|
||||
return;
|
||||
}
|
||||
|
||||
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,175 @@
|
||||
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;
|
||||
onUpgradeRequired?: () => 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, onUpgradeRequired } = 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;
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, passwordHashBase64);
|
||||
|
||||
// Check if the current vault version is known and up to date, if not known trigger an exception, if not up to date redirect to the upgrade page.
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
onUpgradeRequired?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Check if it's a version-related error (app needs to be updated)
|
||||
if (error instanceof Error && error.message.includes('This browser extension is outdated')) {
|
||||
await webApi.logout(error.message);
|
||||
onError?.(error.message);
|
||||
return false;
|
||||
}
|
||||
// Vault could not be decrypted, throw an error
|
||||
throw new Error('Vault could not be decrypted, if the problem persists please logout and login again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the vault is up to date, if not, redirect to the upgrade page.
|
||||
if (await dbContext.hasPendingMigrations()) {
|
||||
onUpgradeRequired?.();
|
||||
return false;
|
||||
}
|
||||
|
||||
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 version-related error (app needs to be updated)
|
||||
if (errorMessage.includes('This browser extension is outdated')) {
|
||||
await webApi.logout(errorMessage);
|
||||
onError?.(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 };
|
||||
};
|
||||
@@ -4,6 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AliasVault</title>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/icon/16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icon/32.png" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/icon/48.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/icon/192.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/icon/192.png" />
|
||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
|
||||
@@ -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,687 @@
|
||||
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();
|
||||
|
||||
// Get gender preference from database
|
||||
const genderPreference = dbContext.sqliteClient!.getDefaultIdentityGender();
|
||||
|
||||
// Generate identity with gender preference
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference);
|
||||
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"
|
||||
cancelText="Cancel"
|
||||
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,62 +9,42 @@ 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 { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
*/
|
||||
const isPopup = (): boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = (): void => {
|
||||
const width = 380;
|
||||
const height = 600;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`popup.html?expanded=true#/credentials/${id}`,
|
||||
'CredentialDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
window.close();
|
||||
};
|
||||
const openInNewPopup = useCallback((): void => {
|
||||
PopoutUtility.openInNewPopup(`/credentials/${id}`);
|
||||
}, [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()) {
|
||||
if (PopoutUtility.isPopup()) {
|
||||
window.history.replaceState({}, '', `popup.html#/credentials`);
|
||||
window.history.pushState({}, '', `popup.html#/credentials/${id}`);
|
||||
}
|
||||
@@ -89,23 +67,51 @@ 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">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<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,21 @@
|
||||
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 { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
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 +23,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 +50,88 @@ 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.');
|
||||
navigate('/logout');
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Refresh error:', err);
|
||||
console.error('Error refreshing credentials:', err);
|
||||
await webApi.logout('Error while syncing vault, please re-authenticate.');
|
||||
navigate('/logout');
|
||||
}
|
||||
}, [dbContext, webApi, hideLoading]);
|
||||
}, [dbContext, webApi, syncVault, navigate]);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}, [onRefresh, setIsLoading]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = (
|
||||
<div className="flex items-center gap-2">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
)}
|
||||
<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, setIsInitialLoading]);
|
||||
|
||||
// Add this function to filter credentials
|
||||
const filteredCredentials = credentials.filter(cred => {
|
||||
@@ -135,7 +157,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,27 @@
|
||||
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 { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
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,20 +29,14 @@ 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
|
||||
if (isPopup()) {
|
||||
if (PopoutUtility.isPopup()) {
|
||||
// Clear existing history and create fresh entries
|
||||
window.history.replaceState({}, '', `popup.html#/emails`);
|
||||
window.history.pushState({}, '', `popup.html#/emails/${id}`);
|
||||
@@ -62,58 +64,43 @@ 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 (PopoutUtility.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.
|
||||
* Open the email details in a new expanded popup.
|
||||
*/
|
||||
const isPopup = () : boolean => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('expanded') === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the credential details in a new expanded popup.
|
||||
*/
|
||||
const openInNewPopup = () : void => {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
window.open(
|
||||
`popup.html?expanded=true#/emails/${id}`,
|
||||
'EmailDetails',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
// Close the current tab
|
||||
window.close();
|
||||
};
|
||||
const openInNewPopup = useCallback((): void => {
|
||||
PopoutUtility.openInNewPopup(`/emails/${id}`);
|
||||
}, [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 +110,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 +140,39 @@ 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">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<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 +191,25 @@ 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"
|
||||
cancelText="Cancel"
|
||||
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,20 @@
|
||||
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 HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
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 { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { MailboxBulkRequest, MailboxBulkResponse, MailboxEmail } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
/**
|
||||
* Emails list page.
|
||||
@@ -15,8 +22,10 @@ import ReloadButton from '@/entrypoints/popup/components/ReloadButton';
|
||||
const EmailsList: React.FC = () => {
|
||||
const dbContext = useDb();
|
||||
const webApi = useWebApi();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
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,13 +70,31 @@ 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();
|
||||
}, [loadEmails]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Formats the date display for emails
|
||||
*/
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* Home page that shows the correct page based on the user's authentication state.
|
||||
*/
|
||||
const Home: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
|
||||
|
||||
// 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.
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsInitialLoading(false);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (!isDatabaseAvailable) {
|
||||
return <Unlock />;
|
||||
}
|
||||
|
||||
if (isInlineUnlockMode) {
|
||||
return <UnlockSuccess onClose={() => setIsInlineUnlockMode(false)} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
20
apps/browser-extension/src/entrypoints/popup/pages/Index.tsx
Normal file
20
apps/browser-extension/src/entrypoints/popup/pages/Index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
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 { isFullyInitialized } = useNavigation();
|
||||
|
||||
if (!isFullyInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Navigate to="/reinitialize" replace />;
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,30 +1,42 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Buffer } from 'buffer';
|
||||
import { storage } from '#imports';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
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 { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
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 { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
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
|
||||
*/
|
||||
const Login: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [credentials, setCredentials] = useState({
|
||||
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 +60,29 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
setClientUrl(clientUrl);
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
loadClientUrl();
|
||||
}, []);
|
||||
}, [setIsInitialLoading]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
@@ -66,7 +98,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 +116,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,14 +154,31 @@ 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);
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// If there are pending migrations, redirect to the upgrade page.
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : 'An error occurred while checking for pending migrations.');
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page which will take care of the proper redirect.
|
||||
navigate('/reinitialize', { replace: true });
|
||||
|
||||
// Show app.
|
||||
hideLoading();
|
||||
} catch (err) {
|
||||
@@ -164,7 +213,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
const validationResponse = await srpUtil.validateLogin2Fa(
|
||||
credentials.username,
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
loginResponse,
|
||||
@@ -189,14 +238,31 @@ 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);
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Set logged in status to true which refreshes the app.
|
||||
await authContext.login();
|
||||
|
||||
// If there are pending migrations, redirect to the upgrade page.
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await authContext.logout();
|
||||
setError(err instanceof Error ? err.message : 'An error occurred while checking for pending migrations.');
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page which will take care of the proper redirect.
|
||||
navigate('/reinitialize', { replace: true });
|
||||
|
||||
// Reset 2FA state and login response as it's no longer needed
|
||||
setTwoFactorRequired(false);
|
||||
setTwoFactorCode('');
|
||||
@@ -229,7 +295,7 @@ const Login: React.FC = () => {
|
||||
|
||||
if (twoFactorRequired) {
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<div>
|
||||
<form onSubmit={handleTwoFactorSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
@@ -286,7 +352,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -19,7 +20,7 @@ const Logout: React.FC = () => {
|
||||
*/
|
||||
const performLogout = async () : Promise<void> => {
|
||||
await webApi.logout();
|
||||
navigate('/');
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
performLogout();
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { 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 { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize component that handles initial application setup, authentication checks,
|
||||
* vault synchronization, and state restoration.
|
||||
*/
|
||||
const Reinitialize: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { syncVault } = useVaultSync();
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// Auth and DB state
|
||||
const { isInitialized: authInitialized, isLoggedIn } = useAuth();
|
||||
const { dbInitialized, dbAvailable } = useDb();
|
||||
|
||||
// Derived state
|
||||
const isFullyInitialized = authInitialized && dbInitialized;
|
||||
const requiresAuth = isFullyInitialized && (!isLoggedIn || !dbAvailable);
|
||||
|
||||
/**
|
||||
* Restore the last visited page and navigation history if it was visited within the memory duration.
|
||||
*/
|
||||
const restoreLastPage = useCallback(async (): Promise<void> => {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for inline unlock mode
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const inlineUnlock = urlParams.get('mode') === 'inline_unlock';
|
||||
|
||||
if (isFullyInitialized) {
|
||||
// Prevent multiple vault syncs (only run sync once)
|
||||
const shouldRunSync = !hasInitialized.current;
|
||||
|
||||
if (requiresAuth) {
|
||||
setIsInitialLoading(false);
|
||||
|
||||
// Determine which auth page to show
|
||||
if (!isLoggedIn) {
|
||||
navigate('/login', { replace: true });
|
||||
} else if (!dbAvailable) {
|
||||
navigate('/unlock', { replace: true });
|
||||
}
|
||||
} else if (shouldRunSync) {
|
||||
// Only perform vault sync once during initialization
|
||||
hasInitialized.current = true;
|
||||
|
||||
// Perform vault sync and restore state
|
||||
syncVault({
|
||||
initialSync: false,
|
||||
/**
|
||||
* Handle successful vault sync.
|
||||
*/
|
||||
onSuccess: async () => {
|
||||
// After successful sync, try to restore last page or go to credentials
|
||||
if (inlineUnlock) {
|
||||
setIsInitialLoading(false);
|
||||
navigate('/unlock-success', { replace: true });
|
||||
} else {
|
||||
await restoreLastPage();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handle vault sync error.
|
||||
* @param error Error message
|
||||
*/
|
||||
onError: (error) => {
|
||||
console.error('Vault sync error during initialization:', error);
|
||||
// Even if sync fails, continue with initialization
|
||||
restoreLastPage().then(() => {
|
||||
setIsInitialLoading(false);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Handle upgrade required.
|
||||
*/
|
||||
onUpgradeRequired: () => {
|
||||
navigate('/upgrade', { replace: true });
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// User is logged in and db is available, navigate to appropriate page
|
||||
setIsInitialLoading(false);
|
||||
restoreLastPage();
|
||||
}
|
||||
}
|
||||
}, [isFullyInitialized, requiresAuth, isLoggedIn, dbAvailable, navigate, setIsInitialLoading, syncVault, restoreLastPage]);
|
||||
|
||||
// This component doesn't render anything visible - it just handles initialization
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Reinitialize;
|
||||
@@ -1,10 +1,20 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { storage } from "#imports";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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 { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useTheme } from '@/entrypoints/popup/context/ThemeContext';
|
||||
import { browser } from "#imports";
|
||||
import { useApiUrl } from '@/entrypoints/popup/utils/ApiUrlUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
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 { storage, browser } from "#imports";
|
||||
|
||||
/**
|
||||
* Popup settings type.
|
||||
@@ -23,6 +33,11 @@ type PopupSettings = {
|
||||
*/
|
||||
const Settings: React.FC = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const authContext = useAuth();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
const [settings, setSettings] = useState<PopupSettings>({
|
||||
disabledUrls: [],
|
||||
temporaryDisabledUrls: {},
|
||||
@@ -41,6 +56,44 @@ 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">
|
||||
{!PopoutUtility.isPopup() && (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={openClientTab}
|
||||
title="Open web app"
|
||||
iconType={HeaderIconType.EXTERNAL_LINK}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
return () => setHeaderButtons(null);
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Load settings.
|
||||
*/
|
||||
@@ -64,6 +117,9 @@ const Settings: React.FC = () => {
|
||||
await storage.setItem(TEMPORARY_DISABLED_SITES_KEY, cleanedTemporaryDisabledUrls);
|
||||
}
|
||||
|
||||
// Load API URL
|
||||
await loadApiUrl();
|
||||
|
||||
setSettings({
|
||||
disabledUrls,
|
||||
temporaryDisabledUrls: cleanedTemporaryDisabledUrls,
|
||||
@@ -72,7 +128,8 @@ const Settings: React.FC = () => {
|
||||
isGloballyEnabled,
|
||||
isContextMenuEnabled
|
||||
});
|
||||
}, []);
|
||||
setIsInitialLoading(false);
|
||||
}, [setIsInitialLoading, loadApiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
@@ -188,12 +245,52 @@ const Settings: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
navigate('/logout', { replace: true });
|
||||
};
|
||||
|
||||
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>
|
||||
@@ -261,7 +358,7 @@ const Settings: React.FC = () => {
|
||||
{settings.isGloballyEnabled && (
|
||||
<button
|
||||
onClick={toggleCurrentSite}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
className={`px-4 py-2 ml-1 rounded-md transition-colors ${
|
||||
settings.isEnabled
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
@@ -355,7 +452,7 @@ const Settings: React.FC = () => {
|
||||
)}
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
Version: {AppInfo.VERSION}
|
||||
Version {AppInfo.VERSION} ({getDisplayUrl()})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
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 HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
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 { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
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
|
||||
@@ -19,13 +27,14 @@ const Unlock: React.FC = () => {
|
||||
const authContext = useAuth();
|
||||
const dbContext = useDb();
|
||||
const navigate = useNavigate();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { showLoading, hideLoading } = useLoading();
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -36,11 +45,30 @@ const Unlock: React.FC = () => {
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
await webApi.logout(statusError);
|
||||
navigate('/logout');
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
}, [webApi, authContext]);
|
||||
}, [webApi, authContext, setIsInitialLoading, navigate]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
@@ -80,6 +108,9 @@ const Unlock: React.FC = () => {
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
|
||||
// Redirect to reinitialize page
|
||||
navigate('/reinitialize', { replace: true });
|
||||
} catch (err) {
|
||||
setError('Failed to unlock vault. Please check your password and try again.');
|
||||
console.error('Unlock error:', err);
|
||||
@@ -96,13 +127,31 @@ const Unlock: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white break-all overflow-hidden mb-4">{authContext.username}</h2>
|
||||
{/* User Avatar and Username Section */}
|
||||
<div className="flex items-center space-x-3 mb-6">
|
||||
<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>
|
||||
|
||||
<p className="text-base text-gray-500 dark:text-gray-200 mb-6">
|
||||
Enter your master password to unlock your vault.
|
||||
</p>
|
||||
{/* Instruction Title */}
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Unlock your vault
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
@@ -110,7 +159,7 @@ const Unlock: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="mb-2">
|
||||
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
|
||||
@@ -1,45 +1,55 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Unlock success component shown when the vault is successfully unlocked in a separate popup
|
||||
* asking the user if they want to close the popup.
|
||||
*/
|
||||
const UnlockSuccess: React.FC<{
|
||||
onClose: () => void;
|
||||
}> = ({ onClose }) => (
|
||||
<div className="flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="mb-4 text-green-600 dark:text-green-400">
|
||||
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
const UnlockSuccess: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Handle browsing vault contents - navigate to credentials page and reset mode parameter
|
||||
*/
|
||||
const handleBrowseVaultContents = (): void => {
|
||||
// Remove mode=inline from URL before navigating
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('mode');
|
||||
window.history.replaceState({}, '', url);
|
||||
|
||||
// Navigate to credentials page
|
||||
navigate('/credentials');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="mb-4 text-green-600 dark:text-green-400">
|
||||
<svg className="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
Your vault is successfully unlocked
|
||||
</h2>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-400">
|
||||
You can now use autofill in login forms in your browser.
|
||||
</p>
|
||||
<div className="space-y-3 w-full">
|
||||
<button
|
||||
onClick={() => window.close()}
|
||||
className="w-full px-4 py-2 text-white bg-primary-600 rounded hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
Close this popup
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBrowseVaultContents}
|
||||
className="w-full px-4 py-2 text-gray-700 bg-gray-100 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
>
|
||||
Browse vault contents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
|
||||
Your vault is successfully unlocked
|
||||
</h2>
|
||||
<p className="mb-6 text-gray-600 dark:text-gray-400">
|
||||
You can now use autofill in login forms in your browser.
|
||||
</p>
|
||||
<div className="space-y-3 w-full">
|
||||
<button
|
||||
onClick={() => window.close()}
|
||||
className="w-full px-4 py-2 text-white bg-primary-600 rounded hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||
>
|
||||
Close this popup
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Remove mode=inline from URL before closing
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('mode');
|
||||
window.history.replaceState({}, '', url);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full px-4 py-2 text-gray-700 bg-gray-100 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
>
|
||||
Browse vault contents
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default UnlockSuccess;
|
||||
|
||||
339
apps/browser-extension/src/entrypoints/popup/pages/Upgrade.tsx
Normal file
339
apps/browser-extension/src/entrypoints/popup/pages/Upgrade.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import Modal from '@/entrypoints/popup/components/Modal';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
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 { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
|
||||
/**
|
||||
* Upgrade page for handling vault version upgrades.
|
||||
*/
|
||||
const Upgrade: React.FC = () => {
|
||||
const { username } = useAuth();
|
||||
const dbContext = useDb();
|
||||
const { sqliteClient } = dbContext;
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentVersion, setCurrentVersion] = useState<VaultVersion | null>(null);
|
||||
const [latestVersion, setLatestVersion] = useState<VaultVersion | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showSelfHostedWarning, setShowSelfHostedWarning] = useState(false);
|
||||
const [showVersionInfo, setShowVersionInfo] = useState(false);
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const webApi = useWebApi();
|
||||
const { executeVaultMutation, isLoading: isVaultMutationLoading, syncStatus } = useVaultMutate();
|
||||
const { syncVault } = useVaultSync();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
const headerButtonsJSX = !PopoutUtility.isPopup() ? (
|
||||
<>
|
||||
<HeaderButton
|
||||
onClick={() => PopoutUtility.openInNewPopup()}
|
||||
title="Open in new window"
|
||||
iconType={HeaderIconType.EXPAND}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
setHeaderButtons(headerButtonsJSX);
|
||||
|
||||
return () => {
|
||||
setHeaderButtons(null);
|
||||
};
|
||||
}, [setHeaderButtons]);
|
||||
|
||||
/**
|
||||
* Load version information from the database.
|
||||
*/
|
||||
const loadVersionInfo = useCallback(async () => {
|
||||
try {
|
||||
if (sqliteClient) {
|
||||
const current = sqliteClient.getDatabaseVersion();
|
||||
const latest = await sqliteClient.getLatestDatabaseVersion();
|
||||
setCurrentVersion(current);
|
||||
setLatestVersion(latest);
|
||||
}
|
||||
setIsInitialLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to load version information:', error);
|
||||
setError('Failed to load version information. Please try again.');
|
||||
}
|
||||
}, [sqliteClient, setIsInitialLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadVersionInfo();
|
||||
}, [loadVersionInfo]);
|
||||
|
||||
/**
|
||||
* Handle the vault upgrade.
|
||||
*/
|
||||
const handleUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
setError('Unable to get version information. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a self-hosted instance and show warning if needed
|
||||
if (await webApi.isSelfHosted()) {
|
||||
setShowSelfHostedWarning(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await performUpgrade();
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform the actual vault upgrade.
|
||||
*/
|
||||
const performUpgrade = async (): Promise<void> => {
|
||||
if (!sqliteClient || !currentVersion || !latestVersion) {
|
||||
setError('Unable to get version information. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Get upgrade SQL commands from vault-sql shared library
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const upgradeResult = vaultSqlGenerator.getUpgradeVaultSql(currentVersion.revision, latestVersion.revision);
|
||||
|
||||
if (!upgradeResult.success) {
|
||||
throw new Error(upgradeResult.error ?? 'Failed to generate upgrade SQL');
|
||||
}
|
||||
|
||||
if (upgradeResult.sqlCommands.length === 0) {
|
||||
// No upgrade needed, vault is already up to date
|
||||
await handleUpgradeSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the useVaultMutate hook to handle the upgrade and vault upload
|
||||
console.debug('executeVaultMutation');
|
||||
await executeVaultMutation(async () => {
|
||||
// Begin transaction
|
||||
console.debug('beginTransaction');
|
||||
sqliteClient.beginTransaction();
|
||||
|
||||
// Execute each SQL command
|
||||
console.debug('executeRaw', upgradeResult.sqlCommands.length);
|
||||
for (let i = 0; i < upgradeResult.sqlCommands.length; i++) {
|
||||
const sqlCommand = upgradeResult.sqlCommands[i];
|
||||
|
||||
try {
|
||||
console.debug('executeRaw', sqlCommand);
|
||||
sqliteClient.executeRaw(sqlCommand);
|
||||
} catch (error) {
|
||||
console.debug('error', error);
|
||||
console.error(`Error executing SQL command ${i + 1}:`, sqlCommand, error);
|
||||
sqliteClient.rollbackTransaction();
|
||||
throw new Error(`Failed to apply migration ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
console.debug('commitTransaction');
|
||||
sqliteClient.commitTransaction();
|
||||
}, {
|
||||
skipSyncCheck: true, // Skip sync check during upgrade to prevent loop
|
||||
/**
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
console.debug('onSuccess');
|
||||
void handleUpgradeSuccess();
|
||||
},
|
||||
/**
|
||||
* Handle upgrade error.
|
||||
*/
|
||||
onError: (error: Error) => {
|
||||
console.debug('onError');
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error.message);
|
||||
}
|
||||
});
|
||||
console.debug('executeVaultMutation done?');
|
||||
} catch (error) {
|
||||
console.error('Upgrade failed:', error);
|
||||
setError(error instanceof Error ? error.message : 'An unknown error occurred during the upgrade. Please try again.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle successful upgrade completion.
|
||||
*/
|
||||
const handleUpgradeSuccess = async (): Promise<void> => {
|
||||
try {
|
||||
// Sync vault to ensure we have the latest data
|
||||
await syncVault({
|
||||
/**
|
||||
* Handle successful sync completion.
|
||||
*/
|
||||
onSuccess: () => {
|
||||
// Navigate to credentials page
|
||||
navigate('/credentials');
|
||||
},
|
||||
/**
|
||||
* Handle sync error.
|
||||
* @param error Error message
|
||||
*/
|
||||
onError: (error: string) => {
|
||||
console.error('Sync error after upgrade:', error);
|
||||
// Still navigate to credentials even if sync fails
|
||||
navigate('/credentials');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during post-upgrade sync:', error);
|
||||
// Navigate to credentials even if sync fails
|
||||
navigate('/credentials');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the logout.
|
||||
*/
|
||||
const handleLogout = async (): Promise<void> => {
|
||||
navigate('/logout');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show version description dialog.
|
||||
*/
|
||||
const showVersionDialog = (): void => {
|
||||
setShowVersionInfo(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Full loading screen overlay */}
|
||||
{(isLoading || isVaultMutationLoading) && (
|
||||
<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 || 'Upgrading vault...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Self-hosted warning modal */}
|
||||
<Modal
|
||||
isOpen={showSelfHostedWarning}
|
||||
onClose={() => setShowSelfHostedWarning(false)}
|
||||
onConfirm={() => {
|
||||
setShowSelfHostedWarning(false);
|
||||
void performUpgrade();
|
||||
}}
|
||||
title="Self-Hosted Server"
|
||||
message="If you're using a self-hosted server, make sure to also update your self-hosted instance as otherwise logging in to the web client will stop working. Do you want to continue with the upgrade?"
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
/>
|
||||
|
||||
{/* Version info modal */}
|
||||
<Modal
|
||||
isOpen={showVersionInfo}
|
||||
onClose={() => setShowVersionInfo(false)}
|
||||
onConfirm={() => setShowVersionInfo(false)}
|
||||
title="What's New"
|
||||
message={`An upgrade is required to support the following changes:\n\n${latestVersion?.description ?? 'No description available for this version.'}`}
|
||||
/>
|
||||
|
||||
<form className="w-full px-2 pt-2 pb-2 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User display section like settings page */}
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<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">
|
||||
{username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-bold dark:text-gray-200 mb-4">Upgrade Vault</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-700 dark:text-gray-200 text-sm mb-4">
|
||||
AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">Version Information</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={showVersionDialog}
|
||||
className="bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold hover:bg-gray-300 dark:hover:bg-gray-500"
|
||||
title="Show version details"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Your vault:</span>
|
||||
<span className="text-sm font-bold text-orange-600 dark:text-orange-400">
|
||||
{currentVersion?.releaseVersion ?? '...'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">New version:</span>
|
||||
<span className="text-sm font-bold text-green-600 dark:text-green-400">
|
||||
{latestVersion?.releaseVersion ?? '...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full space-y-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleUpgrade}
|
||||
>
|
||||
{isLoading || isVaultMutationLoading ? (syncStatus || 'Upgrading...') : 'Upgrade Vault'}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm font-medium py-2"
|
||||
disabled={isLoading || isVaultMutationLoading}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Upgrade;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Hook to manage API URL state and display logic.
|
||||
* @returns Object containing apiUrl state and utility functions
|
||||
*/
|
||||
export const useApiUrl = (): {
|
||||
apiUrl: string;
|
||||
setApiUrl: (url: string) => void;
|
||||
loadApiUrl: () => Promise<void>;
|
||||
getDisplayUrl: () => string;
|
||||
} => {
|
||||
const [apiUrl, setApiUrl] = useState<string>(AppInfo.DEFAULT_API_URL);
|
||||
|
||||
/**
|
||||
* Load the API URL from storage.
|
||||
*/
|
||||
const loadApiUrl = async (): Promise<void> => {
|
||||
const storedUrl = await storage.getItem('local:apiUrl') as string;
|
||||
if (storedUrl && storedUrl.length > 0) {
|
||||
setApiUrl(storedUrl);
|
||||
} else {
|
||||
setApiUrl(AppInfo.DEFAULT_API_URL);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the display URL for UI presentation.
|
||||
* @returns Formatted display URL
|
||||
*/
|
||||
const getDisplayUrl = (): string => {
|
||||
const cleanUrl = apiUrl.replace('https://', '').replace('http://', '').replace(':443', '').replace('/api', '');
|
||||
return cleanUrl === 'app.aliasvault.net' ? 'aliasvault.net' : cleanUrl;
|
||||
};
|
||||
|
||||
return {
|
||||
apiUrl,
|
||||
setApiUrl,
|
||||
loadApiUrl,
|
||||
getDisplayUrl,
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Utility class for handling popup window operations
|
||||
*/
|
||||
export class PopoutUtility {
|
||||
/**
|
||||
* Check if the current page is an expanded popup.
|
||||
* Uses both URL parameter detection and window width as fallback.
|
||||
*/
|
||||
public static isPopup(): boolean {
|
||||
// Primary method: Check URL parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('expanded') === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback method: Check window width (popout windows are 800px wide)
|
||||
* Regular popup extension windows are typically narrower (around 375-400px)
|
||||
*/
|
||||
return window.innerWidth > 390;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the current page in a new expanded popup window.
|
||||
* @param path - The path to open in the popup (defaults to current path)
|
||||
*/
|
||||
public static openInNewPopup(path?: string): void {
|
||||
const width = 800;
|
||||
const height = 1000;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
|
||||
const currentPath = path || window.location.hash.replace('#', '');
|
||||
const popupUrl = `popup.html?expanded=true#${currentPath}`;
|
||||
|
||||
window.open(
|
||||
popupUrl,
|
||||
'AliasVaultPopup',
|
||||
`width=${width},height=${height},left=${left},top=${top},popup=true`
|
||||
);
|
||||
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
@@ -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.20.2';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault server (API) version. If the server version is below this, the
|
||||
@@ -14,11 +14,6 @@ export class AppInfo {
|
||||
*/
|
||||
public static readonly MIN_SERVER_VERSION = '0.12.0-dev';
|
||||
|
||||
/**
|
||||
* The minimum supported AliasVault client vault version.
|
||||
*/
|
||||
public static readonly MIN_VAULT_VERSION = '1.4.1';
|
||||
|
||||
/**
|
||||
* The client name to use in the X-AliasVault-Client header.
|
||||
* Detects the specific browser being used.
|
||||
@@ -61,15 +56,6 @@ export class AppInfo {
|
||||
*/
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Checks if a given vault version is supported
|
||||
* @param vaultVersion The version to check
|
||||
* @returns boolean indicating if the version is supported
|
||||
*/
|
||||
public static isVaultVersionSupported(vaultVersion: string): boolean {
|
||||
return this.versionGreaterThanOrEqualTo(vaultVersion, this.MIN_VAULT_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given server version is supported
|
||||
* @param serverVersion The version to check
|
||||
|
||||
@@ -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,8 @@
|
||||
import initSqlJs, { Database } from 'sql.js';
|
||||
import { Credential } from './types/Credential';
|
||||
import { EncryptionKey } from './types/EncryptionKey';
|
||||
import { TotpCode } from './types/TotpCode';
|
||||
import { PasswordSettings } from './types/PasswordSettings';
|
||||
|
||||
import type { Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
import type { VaultVersion } from '@/utils/dist/shared/vault-sql';
|
||||
import { VaultSqlGenerator } from '@/utils/dist/shared/vault-sql';
|
||||
|
||||
/**
|
||||
* Placeholder base64 image for credentials without a logo.
|
||||
@@ -14,6 +14,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 +46,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 +343,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,6 +387,13 @@ export class SqliteClient {
|
||||
return this.getSetting('DefaultIdentityLanguage', 'en');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity gender preference from the database.
|
||||
*/
|
||||
public getDefaultIdentityGender(): string {
|
||||
return this.getSetting('DefaultIdentityGender', 'random');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password settings from the database.
|
||||
*/
|
||||
@@ -321,15 +424,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 +520,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;
|
||||
}
|
||||
@@ -432,7 +535,7 @@ export class SqliteClient {
|
||||
* Returns the semantic version (e.g., "1.4.1") from the latest migration.
|
||||
* Returns null if no migrations are found.
|
||||
*/
|
||||
public getDatabaseVersion(): string | null {
|
||||
public getDatabaseVersion(): VaultVersion {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
@@ -446,7 +549,7 @@ export class SqliteClient {
|
||||
LIMIT 1`);
|
||||
|
||||
if (results.length === 0) {
|
||||
return null;
|
||||
throw new Error('No migrations found in the database.');
|
||||
}
|
||||
|
||||
// Extract version using regex - matches patterns like "20240917191243_1.4.1-RenameAttachmentsPlural"
|
||||
@@ -454,17 +557,53 @@ export class SqliteClient {
|
||||
const versionRegex = /_(\d+\.\d+\.\d+)-/;
|
||||
const versionMatch = versionRegex.exec(migrationId);
|
||||
|
||||
let currentVersion = null;
|
||||
if (versionMatch?.[1]) {
|
||||
return versionMatch[1];
|
||||
currentVersion = versionMatch[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
// Get all available vault versions to get the revision number of the current version.
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const allVersions = vaultSqlGenerator.getAllVersions();
|
||||
const currentVersionRevision = allVersions.find(v => v.version === currentVersion);
|
||||
|
||||
if (!currentVersionRevision) {
|
||||
throw new Error('This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.');
|
||||
}
|
||||
|
||||
return currentVersionRevision;
|
||||
} catch (error) {
|
||||
console.error('Error getting database version:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest available database version
|
||||
* @returns The latest VaultVersion
|
||||
*/
|
||||
public async getLatestDatabaseVersion(): Promise<VaultVersion> {
|
||||
const vaultSqlGenerator = new VaultSqlGenerator();
|
||||
const allVersions = vaultSqlGenerator.getAllVersions();
|
||||
return allVersions[allVersions.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are pending migrations
|
||||
* @returns True if there are pending migrations, false otherwise
|
||||
*/
|
||||
public async hasPendingMigrations(): Promise<boolean> {
|
||||
try {
|
||||
const currentVersion = this.getDatabaseVersion();
|
||||
const latestVersion = await this.getLatestDatabaseVersion();
|
||||
|
||||
return currentVersion.revision < latestVersion.revision;
|
||||
} catch (error) {
|
||||
console.error('Error checking pending migrations:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TOTP codes for a credential
|
||||
* @param credentialId - The ID of the credential to get TOTP codes for
|
||||
@@ -502,6 +641,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.
|
||||
*/
|
||||
@@ -618,6 +976,38 @@ export class SqliteClient {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute raw SQL command
|
||||
* @param query - The SQL command to execute
|
||||
*/
|
||||
public executeRaw(query: string): void {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
// Split the query by semicolons to handle multiple statements
|
||||
const statements = query.split(';');
|
||||
|
||||
for (const statement of statements) {
|
||||
const trimmedStatement = statement.trim();
|
||||
|
||||
// Skip empty statements and transaction control statements (handled externally)
|
||||
if (trimmedStatement.length === 0 ||
|
||||
trimmedStatement.toUpperCase().startsWith('BEGIN TRANSACTION') ||
|
||||
trimmedStatement.toUpperCase().startsWith('COMMIT') ||
|
||||
trimmedStatement.toUpperCase().startsWith('ROLLBACK')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.db.run(trimmedStatement);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing raw SQL:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SqliteClient;
|
||||
@@ -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;
|
||||
@@ -28,12 +29,16 @@ export class WebApiService {
|
||||
* Get the base URL for the API from settings.
|
||||
*/
|
||||
private async getBaseUrl(): Promise<string> {
|
||||
const result = await storage.getItem('local:apiUrl') as string;
|
||||
if (result && result.length > 0) {
|
||||
return result.replace(/\/$/, '') + '/v1/';
|
||||
}
|
||||
const apiUrl = await this.getApiUrl();
|
||||
return apiUrl.replace(/\/$/, '') + '/v1/';
|
||||
}
|
||||
|
||||
return AppInfo.DEFAULT_API_URL.replace(/\/$/, '') + '/v1/';
|
||||
/**
|
||||
* Check if the current server is self-hosted.
|
||||
*/
|
||||
public async isSelfHosted(): Promise<boolean> {
|
||||
const apiUrl = await this.getApiUrl();
|
||||
return apiUrl !== AppInfo.DEFAULT_API_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +47,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 +86,7 @@ export class WebApiService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (!response.ok && throwOnError) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -162,9 +168,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 +179,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;
|
||||
}
|
||||
}
|
||||
@@ -226,14 +232,12 @@ export class WebApiService {
|
||||
// Logout and revoke tokens via WebApi.
|
||||
try {
|
||||
const refreshToken = await this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return;
|
||||
if (refreshToken) {
|
||||
await this.post('Auth/revoke', {
|
||||
token: await this.getAccessToken(),
|
||||
refreshToken: refreshToken,
|
||||
}, false);
|
||||
}
|
||||
|
||||
await this.post('Auth/revoke', {
|
||||
token: await this.getAccessToken(),
|
||||
refreshToken: refreshToken,
|
||||
}, false);
|
||||
} catch (err) {
|
||||
console.error('WebApi logout error:', err);
|
||||
}
|
||||
@@ -288,18 +292,19 @@ export class WebApiService {
|
||||
* Status 0 = OK, vault is ready.
|
||||
* Status 1 = Merge required, which only the web client supports.
|
||||
*/
|
||||
if (vaultResponseJson.status !== 0) {
|
||||
if (vaultResponseJson.status === 1) {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
return 'Your vault needs to be updated. Please login on the AliasVault website and follow the steps.';
|
||||
}
|
||||
|
||||
if (vaultResponseJson.status === 2) {
|
||||
return 'Your vault is outdated. Please login on the AliasVault website and follow the steps.';
|
||||
}
|
||||
|
||||
if (!vaultResponseJson.vault?.blob) {
|
||||
return 'Your account does not have a vault yet. Please complete the tutorial in the AliasVault web client before using the browser extension.';
|
||||
}
|
||||
|
||||
if (!AppInfo.isVaultVersionSupported(vaultResponseJson.vault.version)) {
|
||||
return 'Your vault is outdated. Please login via the web client to update your vault.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -328,31 +333,14 @@ export class WebApiService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Blob to a Base64 string.
|
||||
* Get the API URL from settings.
|
||||
*/
|
||||
private async blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
private async getApiUrl(): Promise<string> {
|
||||
const result = await storage.getItem('local:apiUrl') as string;
|
||||
if (!result || result.length === 0) {
|
||||
return AppInfo.DEFAULT_API_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the reader has finished loading, convert the result to a Base64 string.
|
||||
*/
|
||||
reader.onloadend = (): void => {
|
||||
const result = reader.result;
|
||||
if (typeof result === 'string') {
|
||||
resolve(result.split(',')[1]); // Remove the data URL prefix
|
||||
} else {
|
||||
reject(new Error('Failed to convert Blob to Base64.'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* If the reader encounters an error, reject the promise with a proper Error object.
|
||||
*/
|
||||
reader.onerror = (): void => {
|
||||
reject(new Error('Failed to read blob as Data URL'));
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(gender?: string | 'random'): 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(gender?: string | 'random'): 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(gender) {
|
||||
const identity = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@@ -173,12 +181,26 @@ var BaseIdentityGenerator = class {
|
||||
emailPrefix: "",
|
||||
nickName: ""
|
||||
};
|
||||
if (this.random() < 0.5) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
identity.gender = "Male" /* Male */;
|
||||
let selectedGender;
|
||||
if (gender === "random" || gender === void 0) {
|
||||
selectedGender = this.random() < 0.5 ? "Male" /* Male */ : "Female" /* Female */;
|
||||
} else {
|
||||
if (gender === "male") {
|
||||
selectedGender = "Male" /* Male */;
|
||||
} else if (gender === "female") {
|
||||
selectedGender = "Female" /* Female */;
|
||||
} else {
|
||||
selectedGender = "Male" /* Male */;
|
||||
}
|
||||
}
|
||||
identity.gender = selectedGender;
|
||||
if (selectedGender === "Male" /* Male */) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
} else if (selectedGender === "Female" /* Female */) {
|
||||
identity.firstName = this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
identity.gender = "Female" /* Female */;
|
||||
} else {
|
||||
const usesMaleNames = this.random() < 0.5;
|
||||
identity.firstName = usesMaleNames ? this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)] : this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
}
|
||||
identity.lastName = this.lastNames[Math.floor(this.random() * this.lastNames.length)];
|
||||
identity.birthDate = this.generateRandomDateOfBirth();
|
||||
@@ -979,7 +1001,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 +1665,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 +1734,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(gender) {
|
||||
const identity = {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
@@ -142,12 +149,26 @@ var BaseIdentityGenerator = class {
|
||||
emailPrefix: "",
|
||||
nickName: ""
|
||||
};
|
||||
if (this.random() < 0.5) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
identity.gender = "Male" /* Male */;
|
||||
let selectedGender;
|
||||
if (gender === "random" || gender === void 0) {
|
||||
selectedGender = this.random() < 0.5 ? "Male" /* Male */ : "Female" /* Female */;
|
||||
} else {
|
||||
if (gender === "male") {
|
||||
selectedGender = "Male" /* Male */;
|
||||
} else if (gender === "female") {
|
||||
selectedGender = "Female" /* Female */;
|
||||
} else {
|
||||
selectedGender = "Male" /* Male */;
|
||||
}
|
||||
}
|
||||
identity.gender = selectedGender;
|
||||
if (selectedGender === "Male" /* Male */) {
|
||||
identity.firstName = this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)];
|
||||
} else if (selectedGender === "Female" /* Female */) {
|
||||
identity.firstName = this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
identity.gender = "Female" /* Female */;
|
||||
} else {
|
||||
const usesMaleNames = this.random() < 0.5;
|
||||
identity.firstName = usesMaleNames ? this.firstNamesMale[Math.floor(this.random() * this.firstNamesMale.length)] : this.firstNamesFemale[Math.floor(this.random() * this.firstNamesFemale.length)];
|
||||
}
|
||||
identity.lastName = this.lastNames[Math.floor(this.random() * this.lastNames.length)];
|
||||
identity.birthDate = this.generateRandomDateOfBirth();
|
||||
@@ -948,7 +969,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 +1633,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 +1702,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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user