Compare commits

...

163 Commits

Author SHA1 Message Date
Leendert de Borst
48414dcae4 Bump install.sh version (#1254) 2025-09-19 14:39:13 +02:00
Leendert de Borst
151548f6f7 Bump versions (#1254) 2025-09-19 14:39:13 +02:00
Leendert de Borst
fd5c8096ad New Crowdin updates (#1222)
* New translations start.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* Add Ukrainian language (#1183)

* Add Hebrew language to all apps (#1182)

* New translations emailmodal.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations register.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations passwordstep.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations termsandconditionsstep.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations defaultpasswordsettings.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations activesessionssection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations deleteaccountsection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations passwordchangesection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations recentauthlogssection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations searchwidget.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations logout.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations delete.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations security.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations creating.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Portuguese, Brazilian)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]
2025-09-19 14:38:19 +02:00
Leendert de Borst
09cfee2888 Add test case for nested form elements, refactor logic (#1252) 2025-09-19 12:48:02 +02:00
Leendert de Borst
74cb2eae7d Update password autofill to prevent duplicate character entry (#1252) 2025-09-19 12:48:02 +02:00
Leendert de Borst
35b8f0abae Prepopulate service title and URL based on current tab in browser extension (#1250) 2025-09-18 18:58:20 +02:00
Leendert de Borst
08517e3469 Add credential create popout icon in inline credential create as fallback (#1247) 2025-09-18 17:07:25 +02:00
Leendert de Borst
f3dabc3a39 Update last email/username placeholder to work like suggestions (#1247) 2025-09-18 17:07:25 +02:00
Leendert de Borst
d98f047963 Fix missing translations in confirm modals (#1244) 2025-09-18 13:30:18 +02:00
Leendert de Borst
599966996e Add liquid glass design optimized app icon to iOS app (#1239) 2025-09-18 12:45:33 +02:00
Leendert de Borst
952cfd9a28 Add argon2kt native implementation to Android (#1241) 2025-09-18 10:09:38 +02:00
Leendert de Borst
81a5155734 Replace argon2id react native with native iOS implementation to satisfy Xcode 26 reqs (#1241) 2025-09-18 10:09:38 +02:00
Leendert de Borst
3a953ec7c8 Add monochrome icon support to Android app (#1229) 2025-09-18 08:00:36 +02:00
dependabot[bot]
392dbd626c Bump rexml in /docs in the bundler group across 1 directory
Bumps the bundler group with 1 update in the /docs directory: [rexml](https://github.com/ruby/rexml).


Updates `rexml` from 3.3.9 to 3.4.2
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.9...v3.4.2)

---
updated-dependencies:
- dependency-name: rexml
  dependency-version: 3.4.2
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-18 08:00:24 +02:00
Leendert de Borst
b6d3f9e70f Run automatic Docker image cleanup after build and update (#1232) 2025-09-17 20:23:04 +02:00
Leendert de Borst
c2f2511f6a Delete CNAME 2025-09-17 19:09:27 +02:00
Leendert de Borst
ce2e21900f Add plausible to docs 2025-09-17 19:06:53 +02:00
Leendert de Borst
660b286ee9 Add clear alias fields button to web app (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
133037dcd8 Do not pregenerate password on credential create screen initialize (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
03b65a63ba Only overwrite email/username/pass if values were autogenerated during alias generation (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
f7a8189b86 Fix password field settings initialization (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
38973de6f1 Add clear alias fields button to mobile app (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
9ddd00bfa4 Add clear alias fields button to browser extension (#1235) 2025-09-17 19:00:31 +02:00
Leendert de Borst
88013161d1 Update email domain field behavior in browser extension and mobile app (#1231) 2025-09-17 12:58:04 +02:00
Leendert de Borst
b0da0d8590 Create CNAME 2025-09-17 09:50:56 +02:00
Leendert de Borst
7dcfd6bfd1 Delete CNAME 2025-09-17 09:41:31 +02:00
Leendert de Borst
586b0a3495 Update volume bind mounts to use local folder mounts 2025-09-17 09:14:33 +02:00
Leendert de Borst
30a009c5c4 Add docs local production docker-compose.yml 2025-09-17 09:10:10 +02:00
Leendert de Borst
7d73222ee1 Create SECURITY.txt 2025-09-16 15:10:11 +02:00
Leendert de Borst
6d191a1bd5 Rename SECURITY.md to ARCHITECTURE.md 2025-09-16 14:30:12 +02:00
Leendert de Borst
e5c68c6c6e Bump version to 0.23.1 (#1227) 2025-09-16 13:43:20 +02:00
Leendert de Borst
58c39815e4 Add more browser like behavior to improve FaviconExtractor success rate (#1225) 2025-09-16 13:19:22 +02:00
Leendert de Borst
4b706f466f Improve favicon extractor request handling (#1225) 2025-09-16 13:19:22 +02:00
Leendert de Borst
19f72b1386 Update self-signed SSL cert logic to use correct IP vs DNS name labels (#1223) 2025-09-16 11:40:00 +02:00
Leendert de Borst
b4d883dbf0 New Crowdin updates (#1220)
* New translations start.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Dutch)
Update translations from Crowdin [ci skip]
2025-09-15 19:24:50 +02:00
Leendert de Borst
86f8f4ebdf Bump version to 0.23.0 (#1218) 2025-09-15 19:16:28 +02:00
Leendert de Borst
b5df1ed8dd Rebuild CSS (#1218) 2025-09-15 19:16:28 +02:00
Leendert de Borst
b2c25db5d9 Merge pull request #1185 from aliasvault/1181-optimize-all-in-one-docker-container-config-and-add-documentation
Optimize all in one docker container config and add documentation
2025-09-15 18:50:40 +02:00
Leendert de Borst
c0c876c694 Merge branch 'main' into 1181-optimize-all-in-one-docker-container-config-and-add-documentation 2025-09-15 18:49:42 +02:00
Leendert de Borst
b832d19e0e New translations en.json (Chinese Simplified) (#1217)
Update translations from Crowdin [ci skip]
2025-09-15 18:48:12 +02:00
Leendert de Borst
68214becad Add v0.23.0 update docs with new docker image locations (#1181) 2025-09-15 18:48:02 +02:00
Leendert de Borst
0971922518 New Crowdin updates (#1216)
* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations sharedresources.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Spanish)
Update translations from Crowdin [ci skip]

* New translations en.json (Catalan)
Update translations from Crowdin [ci skip]

* New translations en.json (German)
Update translations from Crowdin [ci skip]

* New translations en.json (Finnish)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Italian)
Update translations from Crowdin [ci skip]

* New translations en.json (Dutch)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Swedish)
Update translations from Crowdin [ci skip]

* New translations en.json (Turkish)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations en.json (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations login.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations register.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations register.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations register.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations passwordstep.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations passwordstep.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations passwordstep.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations passwordstep.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations termsandconditionsstep.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations termsandconditionsstep.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations termsandconditionsstep.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations termsandconditionsstep.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations usernamestep.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations totpcodes.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations totpviewer.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations emailmodal.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations emailpreview.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations emailrow.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations recentemails.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations editemailformrow.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations defaultpasswordsettings.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations defaultpasswordsettings.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations defaultpasswordsettings.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations importservicecard.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations importservices.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations passwordsettingspopup.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations activesessionssection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations activesessionssection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations activesessionssection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations deleteaccountsection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations deleteaccountsection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations deleteaccountsection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations passwordchangesection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations passwordchangesection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations passwordchangesection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations quickvaultunlocksection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations recentauthlogssection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations recentauthlogssection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations recentauthlogssection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations showrecoverycodes.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations twofactorauthenticationsection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations createnewidentitywidget.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations searchwidget.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations searchwidget.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations searchwidget.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations footer.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations topmenu.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations forgotpassword.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations logout.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations logout.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations setup.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations start.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations unlock.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations addedit.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations delete.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations view.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations home.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations apps.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations general.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (French)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Spanish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Catalan)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (German)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Finnish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Dutch)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Swedish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Turkish)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations importexport.en.resx (Chinese Simplified)
Update translations from Crowdin [ci skip]

* New translations security.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations security.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations creating.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations creating.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations errorvaultdecrypt.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Italian)
Update translations from Crowdin [ci skip]

* New translations vaultdecryptionprogress.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations sync.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations welcome.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations apierrors.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations changepassword.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations deleteaccount.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations enable2fa.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations validationmessages.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (French)
Update translations from Crowdin [ci skip]

* New translations en.json (Hebrew)
Update translations from Crowdin [ci skip]

* New translations en.json (Russian)
Update translations from Crowdin [ci skip]

* New translations en.json (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations resetvault.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations resetvaultsection.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Hebrew)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Russian)
Update translations from Crowdin [ci skip]

* New translations disable2fa.en.resx (Ukrainian)
Update translations from Crowdin [ci skip]

* New translations infoplist.strings (Hebrew)
Update translations from Crowdin [ci skip]

* New translations strings.xml (French)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Hebrew)
Update translations from Crowdin [ci skip]

* New translations strings.xml (Russian)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (French)
Update translations from Crowdin [ci skip]

* New translations localizable.strings (Hebrew)
Update translations from Crowdin [ci skip]
2025-09-15 17:20:27 +02:00
Leendert de Borst
1e9767b0bb Update fastlane descriptions 2025-09-15 16:37:34 +02:00
Leendert de Borst
3f12bdad9d Add instructions for using self-signed SSL cert with mobile apps (#1181) 2025-09-15 16:05:22 +02:00
Leendert de Borst
0ee17cc0ee Enable Android app local user added CA root cert compatibility (#1214) 2025-09-15 15:29:13 +02:00
Leendert de Borst
c7448f7e99 Fix mobile app login error to use correct translation key 2025-09-15 14:41:43 +02:00
Leendert de Borst
835b350d53 Improve self-signed SSL cert generation to take into account HOSTNAME env var (#1181) 2025-09-15 14:39:40 +02:00
Leendert de Borst
b7cbecc61d Add DateTime to/from conversion for all known formats to fix parsing and CSV export 2025-09-14 19:56:40 +02:00
Leendert de Borst
5e2f950b7e Force dates to be saved into vault with colons instead of periods for time separators (#1211) 2025-09-14 17:38:18 +02:00
Leendert de Borst
9a97a904fb Add credentials alphabetical sort option to web app (#1207) 2025-09-14 16:54:14 +02:00
Leendert de Borst
56b6753320 Remove hardcoded breadcrumb paths from breadcrumb component (#1208) 2025-09-14 16:20:50 +02:00
Leendert de Borst
f7675c0279 Remove duplicate translations 2025-09-14 13:57:42 +02:00
Leendert de Borst
961d237d42 Refine translation sources (#1204) 2025-09-13 18:22:32 +02:00
Leendert de Borst
47c2ae1e56 Refactor password-generator.tsx to fix Android freeze (#1204) 2025-09-13 18:22:32 +02:00
Leendert de Borst
9658a40c76 Update password-generator.tsx preview bg color (#1204) 2025-09-13 18:22:32 +02:00
Leendert de Borst
752ddaea9c Add password generator settings page to mobile app (#1204) 2025-09-13 18:22:32 +02:00
Leendert de Borst
5efc277316 Simplify AdvancedPasswordField.tsx (#1204) 2025-09-13 18:22:32 +02:00
Leendert de Borst
88b32efa97 Reflect password length in the hidden asterisks password display (#1204) 2025-09-13 18:22:32 +02:00
Leendert de Borst
03f692a62f Update short_description.txt 2025-09-13 10:30:51 +02:00
Leendert de Borst
bca8ffe676 Update import-export.tsx (#1103) 2025-09-12 22:33:47 +02:00
Leendert de Borst
d2590f4222 Add offline banner translations (#1103) 2025-09-12 22:33:47 +02:00
Leendert de Borst
ef245b2566 Add mobile app export import unit test (#1103) 2025-09-12 22:33:47 +02:00
Leendert de Borst
9ae92962d3 Add vault export to CSV option to mobile app (#1103) 2025-09-12 22:33:47 +02:00
Leendert de Borst
e52cd927a5 Update ResponsivePaginator.razor to take up less space (#1200) 2025-09-11 19:56:45 +02:00
Leendert de Borst
582f7c2ebc Add task runner tests for user active/inactive email cleanup task (#1200) 2025-09-11 19:56:45 +02:00
Leendert de Borst
ce5e5df644 Enable information logging for admin, smtp and task runner services (#1200) 2025-09-11 19:56:45 +02:00
Leendert de Borst
6a2e663c57 Update server settings UI (#1200) 2025-09-11 19:56:45 +02:00
Leendert de Borst
f6adb93518 Remove number of emails received from user listing page (#1200) 2025-09-11 19:56:45 +02:00
Leendert de Borst
077a4fb3ee Add user last active day tracking and email cleanup task (#1200) 2025-09-11 19:56:45 +02:00
Leendert de Borst
dc4fa1b487 Remove unused import (#1169) 2025-09-11 17:37:40 +02:00
Leendert de Borst
949b51defd Add password visibility toggle to client login and unlock pages (#1169) 2025-09-11 17:37:40 +02:00
Leendert de Borst
c2b824c31e Add password visibility toggle to browser extension login/unlock (#1169) 2025-09-11 17:37:40 +02:00
Leendert de Borst
cc846830fe Add password visibility toggle to mobile app login/unlock (#1197) 2025-09-11 17:37:40 +02:00
Leendert de Borst
f6ab23fa03 Linting refactor (#1197) 2025-09-11 14:48:20 +02:00
Leendert de Borst
44d84187c8 Update save header icon (#1197) 2025-09-11 14:48:20 +02:00
Leendert de Borst
fe78524e41 Update browser extension UI, standardize font sizes (#1197) 2025-09-11 14:48:20 +02:00
Leendert de Borst
adc0e8227f Hide email from/to information behind toggle to save on UI space (#1197) 2025-09-11 14:48:20 +02:00
Leendert de Borst
55cb24be68 Update browser extension folder structure (#1197) 2025-09-11 14:48:20 +02:00
Leendert de Borst
8efc021bd7 Make whole email row clickable in RecentEmails.razor (#1195) 2025-09-11 11:21:06 +02:00
Leendert de Borst
b649bdeb2e Update login page UI to show footer with app version (#1193) 2025-09-11 11:11:20 +02:00
Leendert de Borst
af4ca2e018 Hide language switcher in registration flow on small screens (#1191) 2025-09-11 10:02:31 +02:00
dependabot[bot]
1fa9606491 Bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /apps/browser-extension directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /shared/vault-sql directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.3.5 to 6.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

Updates `vite` from 6.3.5 to 7.1.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 22:21:41 +02:00
Leendert de Borst
7620fa8186 Fix clipboard copy animation warnings (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
4a5d42d65b Update keyboard margin on add-edit.tsx (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
af0f582090 Add explicit background color to Android native autofill rows (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
4f91ae7f1c Simplify translations (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
67c4b55cbb Update AdvancedPasswordField to prevent freezes on Android (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
7ff608b08c Refactor AdvancedPasswordField (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
4ebbea7825 Update Android build dependencies (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
1260e94199 Update linting (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
3b8d0d3a8a Add expo dev client package (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
2725646a6a Update KeyboardAwareScrollView (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
89cddcc626 Import buffer explicitly (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
f7d9d2a47c Update Android gradle files (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
60833efcda Update identity generator settings to only persist when navigating away (#1188) 2025-09-10 22:21:33 +02:00
Leendert de Borst
70208eb81a Fix package.json (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
ae6e734dc9 Mobile app replace screen after credential edit to preserve stack (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
f1fc2a5f96 Refactor to use RobustPressable to replace standard methods (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
b62621c9c6 Add type declaration to prevent lint warning (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
a372348dbf Add RobustPressable component (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
779d2a6b43 Update React Native Android (#1187) 2025-09-10 22:21:33 +02:00
Leendert de Borst
9510c0232f Update React Native to 0.79 and update iOS dependencies (#1187) 2025-09-10 22:21:33 +02:00
dependabot[bot]
1e97960eab Bump the npm_and_yarn group across 2 directories with 1 update
Bumps the npm_and_yarn group with 1 update in the /shared/identity-generator directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).
Bumps the npm_and_yarn group with 1 update in the /shared/password-generator directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.3.4 to 6.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

Updates `vite` from 6.3.4 to 6.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 14:23:14 +02:00
Leendert de Borst
c756156e0d Update README.md 2025-09-09 16:22:15 +02:00
Leendert de Borst
af98a252c8 Update funding.json 2025-09-09 16:16:06 +02:00
Leendert de Borst
a7f016d73f Update FUNDING.yml 2025-09-09 16:12:48 +02:00
Leendert de Borst
3a287ebc77 Add NAS specific docker compose template (#1181) 2025-09-09 15:39:44 +02:00
Leendert de Borst
65c1a60447 Add filewatcher to reverse-proxy container to restart when SSL cert is updated (#635) 2025-09-09 14:29:06 +02:00
Leendert de Borst
c6906c8caf Update README.md (#1181) 2025-09-09 14:03:22 +02:00
Leendert de Borst
ace1bd7b0f Update docker-compose.all-in-one.yml (#1181) 2025-09-09 13:03:56 +02:00
Leendert de Borst
56e82cd046 Add optional FORCE_HTTPS_REDIRECT flag to install.sh method (#1181) 2025-09-09 10:50:59 +02:00
Leendert de Borst
58d6b4c67c Update instructions (#1181) 2025-09-09 10:08:57 +02:00
Leendert de Borst
7e4a0f6e07 Delete SolarLint.xml (#1181) 2025-09-09 07:40:48 +02:00
Leendert de Borst
b543696fa9 Update Admin login.razor (#1181) 2025-09-09 07:34:04 +02:00
Leendert de Borst
e669738e38 Update docker-build.yml (#1181) 2025-09-08 19:11:30 +02:00
Leendert de Borst
961977c9e2 Update docs (#1181) 2025-09-08 18:51:39 +02:00
Leendert de Borst
e3d2bec203 Update database import/export compatibility (#1181) 2025-09-08 18:51:18 +02:00
Leendert de Borst
75d9249577 Update styling (#1181) 2025-09-08 18:09:02 +02:00
Leendert de Borst
016a7e7559 Add aliasvault wrapper script to all-in-one Docker image (#1181) 2025-09-08 17:41:11 +02:00
Leendert de Borst
b6e7a2e77a Update admin first login message (#1181) 2025-09-08 17:40:47 +02:00
Leendert de Borst
fd9e62591e Add optional http to https redirect env setting (#1181) 2025-09-08 15:59:21 +02:00
Leendert de Borst
fd485b979c Add 301 redirects to jekyll docs (#1181) 2025-09-08 15:58:52 +02:00
Leendert de Borst
410e845811 Update self-host install titles (#1181) 2025-09-08 15:09:44 +02:00
Leendert de Borst
b5207d97fb Add comparison table to install method index page (#1181) 2025-09-08 14:03:54 +02:00
Leendert de Borst
3122dc4807 Update doc self-host titles (#1181) 2025-09-08 11:38:12 +02:00
Leendert de Borst
e010f0f57b Update ImportServices.en.resx 2025-09-08 11:24:34 +02:00
Leendert de Borst
864a7630d5 Tweak HTTPS required message, tweak crypto.js error handling (#1181) 2025-09-07 12:00:45 +02:00
Leendert de Borst
b603a177e2 Add update and troubleshooting docs (#1181) 2025-09-07 11:39:21 +02:00
Leendert de Borst
ee2fd9f9ae Add 404 and sitemap handler (#1181) 2025-09-07 10:52:40 +02:00
Leendert de Borst
a14066c43f Add database and uninstall docs for manual setup (#1181) 2025-09-07 10:35:39 +02:00
Leendert de Borst
1bcd088782 Add advanced and troubleshooting steps per self-host method (#1181) 2025-09-06 20:50:49 +02:00
Leendert de Borst
4ff937feec Update doc headings (#1181) 2025-09-06 15:02:51 +02:00
Leendert de Borst
77d49c52f0 Self-host docs refactor (#1181) 2025-09-06 10:33:54 +02:00
Leendert de Borst
f09cfecb13 Add HTTP warning for non-localhost hostnames (#1181) 2025-09-05 20:21:49 +02:00
Leendert de Borst
8655f15731 Support both HTTP and HTTPS in all in one docker image (#1181) 2025-09-05 19:05:45 +02:00
Leendert de Borst
d629ffb6e5 Update all-in-one build to prevent lock contention (#1181) 2025-09-05 17:49:37 +02:00
Leendert de Borst
21e0ad5017 Update all-in-one image to run in HTTP 80 mode (#1181) 2025-09-05 16:35:02 +02:00
Leendert de Borst
279a1f2ab2 Update docker-compose.all-in-one.yml (#1181) 2025-09-05 15:19:10 +02:00
Leendert de Borst
957be55927 Update funding.json 2025-09-05 09:18:19 +02:00
Leendert de Borst
63a8be657c Update docs HTML link 2025-09-04 22:22:27 +02:00
Leendert de Borst
7559f0aff4 Define labels and annotations per Docker image (#1179) 2025-09-04 15:37:26 +02:00
Leendert de Borst
c89afa613f Add annotations (#1179) 2025-09-04 15:37:26 +02:00
Leendert de Borst
7f449694c8 Add explicit title and description to release.yml to avoid it being overridden (#1179) 2025-09-04 15:37:26 +02:00
Leendert de Borst
8797b3b360 Add opencontainer labels to Dockerfile (#1179) 2025-09-04 15:37:26 +02:00
Leendert de Borst
4af333e22d Update manual docker publish release docs (#1179) 2025-09-04 15:37:26 +02:00
Leendert de Borst
17e8b6c16c Update release.yml (#1179) 2025-09-04 15:37:26 +02:00
Leendert de Borst
694f1d5e8f Update ghcr.io namespace to new aliasvault organization (#1177) 2025-09-04 13:58:04 +02:00
Leendert de Borst
6f32692342 Add ghcr.io namespace migration to install.sh (#1177) 2025-09-04 13:58:04 +02:00
Leendert de Borst
358d838f3b Update release.yml to publish images to new organization namespace (#1175) 2025-09-03 23:08:10 +02:00
Leendert de Borst
2e47486195 Add migrate-images.sh 2025-09-03 22:22:31 +02:00
Leendert de Borst
6936d4da3b Update docker container registry names 2025-09-03 22:01:59 +02:00
Leendert de Borst
17a7a57136 Remove sonarcloud analysis as new project settings are too restricted 2025-09-03 16:51:53 +02:00
Leendert de Borst
a3552471af Refactor (#1173) 2025-09-03 15:57:53 +02:00
Leendert de Borst
886208460b Update sonarcloud-code-analysis.yml with new organization name (#1171) 2025-09-03 14:59:14 +02:00
Leendert de Borst
a6fea3a60a Make curl follow redirects (#1171) 2025-09-03 14:59:14 +02:00
Leendert de Borst
fb9c2e1494 Update copyright header (#1171) 2025-09-03 14:59:14 +02:00
Leendert de Borst
2b259eee0c Update install.sh (#1171) 2025-09-03 14:59:14 +02:00
Leendert de Borst
d9a8e671a1 Update all repo URLS to point to new aliasvault organization (#1171) 2025-09-03 14:59:14 +02:00
Leendert de Borst
f9a9cb83c4 Update AllTimeStats.razor (#1167) 2025-09-03 09:07:44 +02:00
Leendert de Borst
3eae4b478f Make admin UI more responsive for mobile devices, update paginator (#1167) 2025-09-03 09:07:44 +02:00
Leendert de Borst
06dc2eadae Update release docs 2025-09-02 17:10:25 +02:00
Leendert de Borst
2fa11dab67 Update release.yml 2025-09-02 12:05:05 +02:00
Leendert de Borst
c73e3a489c Logging cleanup 2025-09-02 09:13:46 +02:00
977 changed files with 40255 additions and 22967 deletions

View File

@@ -26,6 +26,9 @@ HTTPS_PORT=443
SMTP_PORT=25
SMTP_TLS_PORT=587
# Whether to force redirect all HTTP traffic (80) to HTTPS (443). Defaults to true.
FORCE_HTTPS_REDIRECT=true
# ===========================================
# EMAIL SERVER CONFIGURATION
# ===========================================

1
.github/FUNDING.yml vendored
View File

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

View File

@@ -109,8 +109,8 @@ jobs:
echo "🔧 Testing admin password reset flow..."
# Run the reset password script with auto-confirm
echo "Running reset-admin-password.sh script..."
password_output=$(docker exec aliasvault-test reset-admin-password.sh -y 2>&1)
echo "Running reset-admin-password command..."
password_output=$(docker exec aliasvault-test aliasvault reset-admin-password -y 2>&1)
echo "Script output:"
echo "$password_output"
@@ -174,10 +174,10 @@ jobs:
- 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
if grep -E "ghcr\.io/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
grep -E "ghcr\.io/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."

View File

@@ -26,10 +26,6 @@ on:
default: true
type: boolean
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
upload-install-script:
runs-on: ubuntu-latest
@@ -127,123 +123,253 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Convert repository name to lowercase
run: |
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
- name: Log in to the Container registry
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for multi-container images
id: meta
- name: Extract metadata for Postgres image
id: postgres-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}
images: ghcr.io/aliasvault/postgres
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault PostgreSQL
org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
annotations: |
org.opencontainers.image.description=PostgreSQL database for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
- name: Generate tags for containers
id: tags
run: |
# Transform base tags to include suffixes for each container
TAGS="${{ steps.meta.outputs.tags }}"
- name: Extract metadata for API image
id: api-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/api
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault API
org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
annotations: |
org.opencontainers.image.description=REST API backend for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
- name: Extract metadata for Client image
id: client-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/client
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault Client
org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
annotations: |
org.opencontainers.image.description=Blazor WebAssembly client UI for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
- name: Extract metadata for Admin image
id: admin-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/admin
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault Admin
org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
annotations: |
org.opencontainers.image.description=Admin portal for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
- name: Extract metadata for Reverse Proxy image
id: reverse-proxy-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/reverse-proxy
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault Reverse Proxy
org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
annotations: |
org.opencontainers.image.description=Nginx reverse proxy for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
- name: Extract metadata for SMTP image
id: smtp-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/smtp
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault SMTP Service
org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
annotations: |
org.opencontainers.image.description=SMTP service for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
- name: Extract metadata for TaskRunner image
id: task-runner-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/task-runner
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault TaskRunner
org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
annotations: |
org.opencontainers.image.description=Background task runner for AliasVault. Part of multi-container setup and can be deployed via install.sh (see docs.aliasvault.net)
- name: Extract metadata for InstallCLI image
id: installcli-meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ghcr.io/aliasvault/installcli
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault Install CLI
org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack
annotations: |
org.opencontainers.image.description=Installation and configuration CLI for AliasVault. Used by install.sh for setup and configuration, not deployed as part of the application stack
# Generate tags for each container by replacing the base image name with suffixed versions
echo "postgres=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-postgres|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
echo "api=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-api|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
echo "client=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-client|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
echo "admin=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-admin|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
echo "reverse-proxy=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-reverse-proxy|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
echo "smtp=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-smtp|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
echo "task-runner=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-task-runner|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
echo "installcli=$(echo "$TAGS" | sed "s|${{ env.REPO_LOWER }}|${{ env.REPO_LOWER }}-installcli|g" | tr '\n' ',')" >> $GITHUB_OUTPUT
- name: Build and push Postgres image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/Databases/AliasServerDb/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.postgres }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.postgres-meta.outputs.tags }}
labels: ${{ steps.postgres-meta.outputs.labels }}
annotations: ${{ steps.postgres-meta.outputs.annotations }}
- name: Build and push API image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/AliasVault.Api/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.api }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.api-meta.outputs.tags }}
labels: ${{ steps.api-meta.outputs.labels }}
annotations: ${{ steps.api-meta.outputs.annotations }}
- name: Build and push Client image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/AliasVault.Client/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.client }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.client-meta.outputs.tags }}
labels: ${{ steps.client-meta.outputs.labels }}
annotations: ${{ steps.client-meta.outputs.annotations }}
- name: Build and push Admin image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/AliasVault.Admin/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.admin }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.admin-meta.outputs.tags }}
labels: ${{ steps.admin-meta.outputs.labels }}
annotations: ${{ steps.admin-meta.outputs.annotations }}
- name: Build and push Reverse Proxy image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.reverse-proxy }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.reverse-proxy-meta.outputs.tags }}
labels: ${{ steps.reverse-proxy-meta.outputs.labels }}
annotations: ${{ steps.reverse-proxy-meta.outputs.annotations }}
- name: Build and push SMTP image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/Services/AliasVault.SmtpService/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.smtp }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.smtp-meta.outputs.tags }}
labels: ${{ steps.smtp-meta.outputs.labels }}
annotations: ${{ steps.smtp-meta.outputs.annotations }}
- name: Build and push TaskRunner image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/Services/AliasVault.TaskRunner/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.task-runner }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.task-runner-meta.outputs.tags }}
labels: ${{ steps.task-runner-meta.outputs.labels }}
annotations: ${{ steps.task-runner-meta.outputs.annotations }}
- name: Build and push InstallCli image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: apps/server/Utilities/AliasVault.InstallCli/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.tags.outputs.installcli }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.installcli-meta.outputs.tags }}
labels: ${{ steps.installcli-meta.outputs.labels }}
annotations: ${{ steps.installcli-meta.outputs.annotations }}
build-and-push-docker-all-in-one:
if: github.event_name == 'release' || inputs.build_all_in_one
@@ -262,10 +388,6 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Convert repository name to lowercase
run: |
echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >>${GITHUB_ENV}
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -282,25 +404,35 @@ jobs:
- name: Extract metadata for all-in-one image
id: meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: |
ghcr.io/${{ env.REPO_LOWER }}
ghcr.io/aliasvault/aliasvault
aliasvault/aliasvault
tags: |
# For release events with latest tag (only for non-prerelease)
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
# semver tags for releases (works for prerelease and normal release)
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
type=semver,pattern={{version}},enable=${{ github.event_name == 'release' || github.ref_type == 'tag' }}
# For tags, use tag name
type=ref,event=tag,enable=${{ github.ref_type == 'tag' }}
# For branches, use branch name and branch name + short SHA for uniqueness
type=ref,event=branch,enable=${{ github.ref_type == 'branch' }}
type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' }}
labels: |
org.opencontainers.image.title=AliasVault All-in-One
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net).
annotations: |
org.opencontainers.image.description=Self-contained AliasVault server including web app, with all services bundled using s6-overlay. Single container solution for easy deployment (see docs.aliasvault.net).
- name: Build and push all-in-one image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
file: dockerfiles/all-in-one/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}

View File

@@ -1,80 +0,0 @@
# This workflow will perform a SonarCloud code analysis on every push to the main branch or
# when a pull request is opened, synchronized, or reopened. The "pull_request_target" event is
# used to ensure that the analysis is done on the source branch of the pull request which has
# access to the SonarCloud token secret.
name: SonarCloud code analysis
on:
push:
branches:
- main
pull_request_target:
types: [opened, synchronize, reopened]
# Cancel in-progress jobs when new commits are pushed
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref_name }}
cancel-in-progress: true
jobs:
build:
name: Build and analyze
runs-on: windows-latest
steps:
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0.x'
- name: Install WASM workload
run: dotnet workload install wasm-tools
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'zulu'
- name: Checkout code of PR branch
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~\sonar\cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache SonarCloud scanner
id: cache-sonar-scanner
uses: actions/cache@v3
with:
path: .\.sonar\scanner
key: ${{ runner.os }}-sonar-scanner
restore-keys: ${{ runner.os }}-sonar-scanner
- name: Install SonarCloud scanner
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
shell: powershell
run: |
New-Item -Path .\.sonar\scanner -ItemType Directory
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
- name: Build and analyze
working-directory: apps/server
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
shell: powershell
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,**/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,**/dist/shared/**,**/*.sql"
}
dotnet build
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
& $scanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"

1
.gitignore vendored
View File

@@ -404,6 +404,7 @@ certificates/**/*.crt
certificates/**/*.key
certificates/**/*.pfx
certificates/**/*.pem
certificates/**/.hostname_marker
certificates/letsencrypt/**
# Secrets

2
.vscode/tasks.json vendored
View File

@@ -199,7 +199,7 @@
{
"label": "Build and watch Docs",
"type": "shell",
"command": "docker compose up",
"command": "docker compose -f docker-compose.dev.yml build && docker compose -f docker-compose.dev.yml up",
"problemMatcher": [],
"group": {
"kind": "build",

View File

View File

@@ -1,9 +1,8 @@
# <img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="35" alt="AliasVault"> AliasVault
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)
[![.NET E2E Tests (with Sharding)](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-tests.yml/badge.svg)](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-e2e-tests.yml)
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=Sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
[<img src="https://img.shields.io/github/v/release/aliasvault/aliasvault?include_prereleases&logo=github&label=Release">](https://github.com/aliasvault/aliasvault/releases)
[![.NET E2E Tests (with Sharding)](https://github.com/aliasvault/aliasvault/actions/workflows/dotnet-e2e-tests.yml/badge.svg)](https://github.com/aliasvault/aliasvault/actions/workflows/dotnet-e2e-tests.yml)
[<img src="https://badges.crowdin.net/aliasvault/localized.svg">](https://crowdin.com/project/aliasvault)
[<img alt="Discord" src="https://img.shields.io/discord/1309300619026235422?logo=discord&logoColor=%237289da&label=Discord&color=%237289da">](https://discord.gg/DsaXMTEtpF)
@@ -66,33 +65,31 @@ AliasVault is available on:
[<img width="700" alt="Screenshot of AliasVault" src="docs/assets/img/screenshot.png">](https://app.aliasvault.net)
## Self-hosting
For full control over your own data you can self-host and install AliasVault on your own servers.
> [!NOTE]
> **Requirements:** 1 vCPU, 1GB RAM, 16GB disk, Docker ≥ 20.10, 64-bit Linux
### Install using install script
AliasVault can be self-hosted on your own servers using two different installation methods. Both use Docker, but they differ in how much is automated versus how much you manage yourself.
This method uses pre-built Docker images and works on minimal hardware specifications:
- **Option 1: Install Script** - Managed solution with automatic SSL (recommended for VPS/cloud)
- 64-bit Linux VM (Ubuntu/AlmaLinux) or Raspberry Pi, with root access
- Minimum: 1 vCPU, 1GB RAM, 16GB disk
- Docker ≥ 20.10 and Docker Compose ≥ 2.0
- **Option 2: Docker Compose** - Single container with manual setup for use with existing SSL infrastructure (NAS, homelab)
### Quick Start (Install Script)
```bash
# Download install script from latest stable release
curl -L -o install.sh https://github.com/lanedirt/AliasVault/releases/latest/download/install.sh
# Download and run install script
curl -L -o install.sh https://github.com/aliasvault/aliasvault/releases/latest/download/install.sh
# Make install script executable and run it. This will create the .env file, pull the Docker images, and start the AliasVault containers.
chmod +x install.sh
./install.sh install
```
The install script will output the URL where the app is available. By default this is:
- Client: https://localhost
- Admin portal: https://localhost/admin
For other installation methods and more detailed steps, please read the [full installation guide](https://docs.aliasvault.net/installation) in the official docs.
> Note: If you want to change the default AliasVault ports you can do so in the `.env` file.
## Technical documentation
## Documentation
For more information about the installation process, manual setup instructions and other topics, please see the official documentation website:
- [Documentation website (docs.aliasvault.net) 📚](https://docs.aliasvault.net)
## Security Architecture
@@ -133,12 +130,12 @@ Core features that are being worked on:
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
- [ ] Adding support for family/team sharing (organization features)
👉 [View the full AliasVault roadmap here](https://github.com/lanedirt/AliasVault/issues/731)
👉 [View the full AliasVault roadmap here](https://github.com/aliasvault/aliasvault/issues/731)
### Got feedback or ideas?
Feel free to open an issue or join our [Discord](https://discord.gg/DsaXMTEtpF)! Contributions are warmly welcomed—whether in feature development, testing, or spreading the word. Get in touch on Discord if you're interested in contributing.
Feel free to open an issue or discussion on GitHub. We warmly welcome all contributions: whether its translating, testing, helping to build features, sharing feedback - or helping spread the word about AliasVault. Every bit of support helps the project grow, so dont hesitate to jump in and [say hi to us on 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!
AliasVault is open-source and community-driven. If you like what were building, consider supporting us through [Open Collective](https://opencollective.com/aliasvault) or through:
<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>

17
SECURITY.txt Normal file
View File

@@ -0,0 +1,17 @@
Contact: mailto:security@support.aliasvault.net
Expires: 2026-09-16T12:00:00.000Z
Preferred-Languages: en
Canonical: https://raw.githubusercontent.com/aliasvault/aliasvault/main/SECURITY.txt
# Security Policy for AliasVault
#
# We take security seriously and appreciate responsible disclosure of vulnerabilities.
# Please report security issues to the email above rather than opening public issues.
#
# Include the following information in your report:
# - Description of the vulnerability
# - Steps to reproduce
# - Potential impact
# - Suggested remediation (if any)
#
# We will acknowledge receipt within 48 hours and provide updates as we investigate.

View File

@@ -1,13 +0,0 @@
<SonarLint>
<Rules>
<Rule>
<Key>S1135</Key>
<Parameters>
<Parameter>
<Name>sonarlint.rule.enabled</Name>
<Value>false</Value>
</Parameter>
</Parameters>
</Rule>
</Rules>
</SonarLint>

View File

@@ -1,12 +1,12 @@
{
"name": "aliasvault-browser-extension",
"version": "0.21.2",
"version": "0.22.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aliasvault-browser-extension",
"version": "0.21.2",
"version": "0.22.0",
"hasInstallScript": true,
"dependencies": {
"@hookform/resolvers": "^5.1.1",
@@ -13064,9 +13064,9 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View File

@@ -2,7 +2,7 @@
"name": "aliasvault-browser-extension",
"description": "AliasVault Browser Extension",
"private": true,
"version": "0.22.0",
"version": "0.23.2",
"type": "module",
"scripts": {
"dev:chrome": "wxt -b chrome",

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 500 500" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<path d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z" fill="#EEC170"/>
<path d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z" fill="#EEC170"/>
<path d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z" fill="#EEC170"/>
<path d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z" fill="#EEC170"/>
<path d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z" fill="#EEC170"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -447,7 +447,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 220001;
CURRENT_PROJECT_VERSION = 230200;
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 = 0.22.0;
MARKETING_VERSION = 0.23.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 = 220001;
CURRENT_PROJECT_VERSION = 230200;
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 = 0.22.0;
MARKETING_VERSION = 0.23.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 = 220001;
CURRENT_PROJECT_VERSION = 230200;
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.22.0;
MARKETING_VERSION = 0.23.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 = 220001;
CURRENT_PROJECT_VERSION = 230200;
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.22.0;
MARKETING_VERSION = 0.23.2;
OTHER_LDFLAGS = (
"-framework",
SafariServices,

View File

@@ -3,7 +3,7 @@ import { onMessage, sendMessage } from "webext-bridge/background";
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler';
import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler';
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { handleOpenPopup, handlePopupWithCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { handleOpenPopup, handlePopupWithCredential, handleOpenPopupCreateCredential, handleToggleContextMenu } from '@/entrypoints/background/PopupMessageHandler';
import { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDefaultIdentitySettings, handleGetEncryptionKey, handleGetEncryptionKeyDerivationParams, handleGetPasswordSettings, handleGetPersistedFormValues, handleGetVault, handlePersistFormValues, handleStoreEncryptionKey, handleStoreEncryptionKeyDerivationParams, handleStoreVault, handleSyncVault, handleUploadVault } from '@/entrypoints/background/VaultMessageHandler';
import { GLOBAL_CONTEXT_MENU_ENABLED_KEY } from '@/utils/Constants';
@@ -40,6 +40,7 @@ export default defineBackground({
onMessage('OPEN_POPUP', () => handleOpenPopup());
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));
onMessage('OPEN_POPUP_CREATE_CREDENTIAL', ({ data }) => handleOpenPopupCreateCredential(data));
onMessage('TOGGLE_CONTEXT_MENU', ({ data }) => handleToggleContextMenu(data));
onMessage('PERSIST_FORM_VALUES', ({ data }) => handlePersistFormValues(data));

View File

@@ -108,6 +108,4 @@ async function extendAutoLockTimer(): Promise<void> {
console.error('[AUTO_LOCK] Error locking vault:', error);
}
}, timeout * 1000);
console.info(`[AUTO_LOCK] Timer extended (popup heartbeat)`);
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
import { BoolResponse } from '@/utils/types/messaging/BoolResponse';
import { browser } from '#imports';
@@ -37,6 +38,53 @@ export function handlePopupWithCredential(message: any) : Promise<BoolResponse>
})();
}
/**
* Handle opening the popup on create credential page with prefilled service name.
*/
export function handleOpenPopupCreateCredential(message: any) : Promise<BoolResponse> {
return (async () : Promise<BoolResponse> => {
const serviceName = encodeURIComponent(message.serviceName || '');
// Use the URL passed from the content script (current page URL)
let serviceUrl = '';
if (message.currentUrl) {
try {
const url = new URL(message.currentUrl);
// Only include http/https URLs
if (url.protocol === 'http:' || url.protocol === 'https:') {
serviceUrl = encodeURIComponent(url.origin + url.pathname);
}
} catch (error) {
console.error('Error parsing current URL:', error);
}
}
// Set a localStorage flag to skip restoring previously persisted form values as we want to start fresh with this explicit create credential request.
await browser.storage.local.set({ [SKIP_FORM_RESTORE_KEY]: true });
const urlParams = new URLSearchParams();
urlParams.set('expanded', 'true');
if (serviceName) {
urlParams.set('serviceName', serviceName);
}
if (serviceUrl) {
urlParams.set('serviceUrl', serviceUrl);
}
if (message.currentUrl) {
urlParams.set('currentUrl', message.currentUrl);
}
browser.windows.create({
url: browser.runtime.getURL(`/popup.html?${urlParams.toString()}#/credentials/add`),
type: 'popup',
width: 400,
height: 600,
focused: true
});
return { success: true };
})();
}
/**
* Handle toggling the context menu.
*/

View File

@@ -3,12 +3,12 @@ import { sendMessage } from 'webext-bridge/content-script';
import { filterCredentials, AutofillMatchingMode } from '@/entrypoints/contentScript/Filter';
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, AUTOFILL_MATCHING_MODE_KEY } from '@/utils/Constants';
import { DISABLED_SITES_KEY, TEMPORARY_DISABLED_SITES_KEY, GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants';
import { CreateIdentityGenerator } from '@/utils/dist/shared/identity-generator';
import type { Credential } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator, PasswordGenerator, PasswordSettings } from '@/utils/dist/shared/password-generator';
import { FormDetector } from '@/utils/formDetector/FormDetector';
import { ClickValidator } from '@/utils/security/ClickValidator';
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
import { SqliteClient } from '@/utils/SqliteClient';
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
import { IdentitySettingsResponse } from '@/utils/types/messaging/IdentitySettingsResponse';
@@ -227,8 +227,8 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
e.stopPropagation();
e.stopImmediatePropagation();
const suggestedNames = FormDetector.getSuggestedServiceName(document, window.location);
const result = await createAliasCreationPopup(suggestedNames, rootContainer);
const serviceInfo = ServiceDetectionUtility.getServiceInfo(document, window.location);
const result = await createAliasCreationPopup(serviceInfo.suggestedNames, rootContainer);
if (!result) {
// User cancelled
@@ -762,9 +762,9 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
// Close existing popup
removeExistingPopup(rootContainer);
// Load last used values
const lastEmail = await storage.getItem(LAST_CUSTOM_EMAIL_KEY) as string ?? '';
const lastUsername = await storage.getItem(LAST_CUSTOM_USERNAME_KEY) as string ?? '';
// Load history
const emailHistory = await storage.getItem(CUSTOM_EMAIL_HISTORY_KEY) as string[] ?? [];
const usernameHistory = await storage.getItem(CUSTOM_USERNAME_HISTORY_KEY) as string[] ?? [];
return new Promise((resolve) => {
(async (): Promise<void> => {
@@ -829,11 +829,20 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
${randomIdentityIcon}
<h3 class="av-create-popup-title">${randomIdentityTitle}</h3>
</div>
<button class="av-create-popup-mode-dropdown">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div class="av-create-popup-header-buttons">
<button class="av-create-popup-mode-dropdown">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<button class="av-create-popup-popout" title="Open in main popup">
<svg class="av-icon" viewBox="0 0 24 24">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</button>
</div>
</div>
</div>
@@ -888,8 +897,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
id="custom-email"
class="av-create-popup-input"
placeholder="${enterEmailAddressText}"
data-default-value="${lastEmail}"
>
<div class="av-field-suggestions" id="email-suggestions"></div>
</div>
<div class="av-create-popup-field-group">
<label for="custom-username">${usernameText}</label>
@@ -898,8 +907,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
id="custom-username"
class="av-create-popup-input"
placeholder="${enterUsernameText}"
data-default-value="${lastUsername}"
>
<div class="av-field-suggestions" id="username-suggestions"></div>
</div>
<div class="av-create-popup-field-group">
<label>${passwordText}</label>
@@ -960,6 +969,7 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
const customMode = popup.querySelector('.av-create-popup-custom-mode') as HTMLElement;
const dropdownMenu = popup.querySelector('.av-create-popup-mode-dropdown-menu') as HTMLElement;
const titleContainer = popup.querySelector('.av-create-popup-title-container') as HTMLElement;
const popoutBtn = popup.querySelector('.av-create-popup-popout') as HTMLButtonElement;
const cancelBtn = popup.querySelector('#cancel-btn') as HTMLButtonElement;
const customCancelBtn = popup.querySelector('#custom-cancel-btn') as HTMLButtonElement;
const saveBtn = popup.querySelector('#save-btn') as HTMLButtonElement;
@@ -970,41 +980,154 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
const passwordPreview = popup.querySelector('#password-preview') as HTMLInputElement;
const regenerateBtn = popup.querySelector('#regenerate-password') as HTMLButtonElement;
const toggleVisibilityBtn = popup.querySelector('#toggle-password-visibility') as HTMLButtonElement;
const emailSuggestions = popup.querySelector('#email-suggestions') as HTMLElement;
const usernameSuggestions = popup.querySelector('#username-suggestions') as HTMLElement;
/**
* Setup default value for input with placeholder styling.
* Update history with new value (max 2 unique entries)
*/
const setupDefaultValue = (input: HTMLInputElement) : void => {
const defaultValue = input.dataset.defaultValue;
if (defaultValue) {
input.value = defaultValue;
input.classList.add('av-create-popup-input-default');
const updateHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY, maxItems: number = 2): Promise<string[]> => {
const history = await storage.getItem(historyKey) as string[] ?? [];
// Remove the value if it already exists
const filteredHistory = history.filter((item: string) => item !== value);
// Add the new value at the beginning
if (value.trim()) {
filteredHistory.unshift(value);
}
// Keep only the first maxItems
const updatedHistory = filteredHistory.slice(0, maxItems);
// Save the updated history
await storage.setItem(historyKey, updatedHistory);
return updatedHistory;
};
setupDefaultValue(customEmail);
setupDefaultValue(customUsername);
/**
* Remove item from history
*/
const removeFromHistory = async (value: string, historyKey: typeof CUSTOM_EMAIL_HISTORY_KEY | typeof CUSTOM_USERNAME_HISTORY_KEY): Promise<string[]> => {
const history = await storage.getItem(historyKey) as string[] ?? [];
const updatedHistory = history.filter((item: string) => item !== value);
await storage.setItem(historyKey, updatedHistory);
return updatedHistory;
};
// Handle input changes
customEmail.addEventListener('input', () => {
const value = customEmail.value.trim();
if (value || value === '') {
customEmail.classList.remove('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_EMAIL_KEY, value);
/**
* Format suggestions HTML as pill-style buttons
*/
const formatSuggestionsHtml = async (history: string[], currentValue: string): Promise<string> => {
// Filter out the current value from history and limit to 2 items
const filteredHistory = history
.filter(item => item.toLowerCase() !== currentValue.toLowerCase())
.slice(0, 2);
if (filteredHistory.length === 0) {
return '';
}
// Build HTML with pill-style buttons
return filteredHistory.map(item =>
`<span class="av-suggestion-pill">
<span class="av-suggestion-pill-text" data-value="${item}">${item}</span>
<span class="av-suggestion-pill-delete" data-value="${item}" title="Remove">×</span>
</span>`
).join(' ');
};
/**
* Update suggestions display
*/
const updateSuggestions = async (input: HTMLInputElement, suggestionsContainer: HTMLElement, history: string[]): Promise<void> => {
const currentValue = input.value.trim();
const html = await formatSuggestionsHtml(history, currentValue);
suggestionsContainer.innerHTML = html;
suggestionsContainer.style.display = html ? 'flex' : 'none';
};
// Initial display of suggestions
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
// Handle popout button click
popoutBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const serviceName = inputServiceName.value.trim();
const encodedServiceInfo = ServiceDetectionUtility.getEncodedServiceInfo(document, window.location);
sendMessage('OPEN_POPUP_CREATE_CREDENTIAL', {
serviceName: serviceName || encodedServiceInfo.serviceName,
currentUrl: encodedServiceInfo.currentUrl
}, 'background');
closePopup(null);
});
// Handle email input
customEmail.addEventListener('input', async () => {
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
});
// Handle username input
customUsername.addEventListener('input', async () => {
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
});
// Handle suggestion clicks for email
emailSuggestions.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLElement;
// Check if delete button was clicked
if (target.classList.contains('av-suggestion-pill-delete')) {
const value = target.dataset.value;
if (value) {
const updatedHistory = await removeFromHistory(value, CUSTOM_EMAIL_HISTORY_KEY);
emailHistory.splice(0, emailHistory.length, ...updatedHistory);
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
}
} else {
customEmail.classList.add('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_EMAIL_KEY, '');
// Check if pill or pill text was clicked
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
if (pillElement) {
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
const value = textElement?.dataset.value;
if (value) {
customEmail.value = value;
await updateSuggestions(customEmail, emailSuggestions, emailHistory);
}
}
}
});
customUsername.addEventListener('input', () => {
const value = customUsername.value.trim();
if (value || value === '') {
customUsername.classList.remove('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_USERNAME_KEY, value);
// Handle suggestion clicks for username
usernameSuggestions.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const target = e.target as HTMLElement;
// Check if delete button was clicked
if (target.classList.contains('av-suggestion-pill-delete')) {
const value = target.dataset.value;
if (value) {
const updatedHistory = await removeFromHistory(value, CUSTOM_USERNAME_HISTORY_KEY);
usernameHistory.splice(0, usernameHistory.length, ...updatedHistory);
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
}
} else {
customUsername.classList.add('av-create-popup-input-default');
storage.setItem(LAST_CUSTOM_USERNAME_KEY, '');
// Check if pill or pill text was clicked
let pillElement = target.closest('.av-suggestion-pill') as HTMLElement;
if (pillElement) {
const textElement = pillElement.querySelector('.av-suggestion-pill-text') as HTMLElement;
const value = textElement?.dataset.value;
if (value) {
customUsername.value = value;
await updateSuggestions(customUsername, usernameSuggestions, usernameHistory);
}
}
}
});
@@ -1372,12 +1495,8 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
if (serviceName) {
const email = customEmail.value.trim();
const username = customUsername.value.trim();
const hasDefaultEmail = customEmail.classList.contains('av-create-popup-input-default');
const hasDefaultUsername = customUsername.classList.contains('av-create-popup-input-default');
// If using default values, use the dataset values
const finalEmail = hasDefaultEmail ? customEmail.dataset.defaultValue : email;
const finalUsername = hasDefaultUsername ? customUsername.dataset.defaultValue : username;
const finalEmail = email;
const finalUsername = username;
if (!finalEmail && !finalUsername) {
// Add error styling to fields
@@ -1424,6 +1543,14 @@ export async function createAliasCreationPopup(suggestedNames: string[], rootCon
return;
}
// Update history when saving
if (finalEmail) {
await updateHistory(finalEmail, CUSTOM_EMAIL_HISTORY_KEY);
}
if (finalUsername) {
await updateHistory(finalUsername, CUSTOM_USERNAME_HISTORY_KEY);
}
closePopup({
serviceName,
isCustomCredential: true,

View File

@@ -539,6 +539,62 @@ body {
box-shadow: 0 0 0 1px #ef4444 !important;
}
/* Field Suggestions - Pill Style */
.av-field-suggestions {
margin-top: 8px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.av-suggestion-pill {
display: inline-flex;
align-items: center;
background: #4b5563;
border: 1px solid #6b7280;
border-radius: 16px;
padding: 4px 8px 4px 12px;
font-size: 13px;
color: #e5e7eb;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.av-suggestion-pill:hover {
background: #6b7280;
border-color: #9ca3af;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.av-suggestion-pill-text {
display: inline-block;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.av-suggestion-pill-delete {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 6px;
padding: 0 2px;
color: #9ca3af;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
border-left: 1px solid #6b7280;
padding-left: 6px;
}
.av-suggestion-pill-delete:hover {
color: #ef4444;
}
.av-create-popup-error-text {
color: #ef4444;
font-size: 0.875rem;
@@ -728,28 +784,41 @@ body {
.av-create-popup-title-container {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
justify-content: center;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
position: relative;
}
.av-create-popup-title-wrapper {
position: absolute;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #d68338;
pointer-events: none;
}
.av-create-popup-header-buttons {
display: flex;
align-items: center;
gap: 4px;
margin-left: auto;
}
.av-create-popup-title-wrapper .av-icon {
width: 20px;
height: 20px;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: auto;
}
.av-create-popup-title-wrapper .av-create-popup-title {
@@ -757,6 +826,7 @@ body {
font-size: 18px;
font-weight: 600;
color: #f8f9fa;
pointer-events: auto;
}
.av-create-popup-title-container:hover {
@@ -785,6 +855,34 @@ body {
height: 16px;
}
.av-create-popup-popout {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: #9ca3af;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s, color 0.2s;
}
.av-create-popup-popout:hover {
background-color: #4b5563;
color: #d68338;
}
.av-create-popup-popout .av-icon {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.av-create-popup-mode-dropdown-menu {
position: absolute;
left: 50%;

View File

@@ -11,25 +11,25 @@ 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 AuthSettings from '@/entrypoints/popup/pages/auth/AuthSettings';
import Login from '@/entrypoints/popup/pages/auth/Login';
import Logout from '@/entrypoints/popup/pages/auth/Logout';
import Unlock from '@/entrypoints/popup/pages/auth/Unlock';
import UnlockSuccess from '@/entrypoints/popup/pages/auth/UnlockSuccess';
import Upgrade from '@/entrypoints/popup/pages/auth/Upgrade';
import CredentialAddEdit from '@/entrypoints/popup/pages/credentials/CredentialAddEdit';
import CredentialDetails from '@/entrypoints/popup/pages/credentials/CredentialDetails';
import CredentialsList from '@/entrypoints/popup/pages/credentials/CredentialsList';
import EmailDetails from '@/entrypoints/popup/pages/emails/EmailDetails';
import EmailsList from '@/entrypoints/popup/pages/emails/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 AutofillSettings from '@/entrypoints/popup/pages/settings/AutofillSettings';
import AutoLockSettings from '@/entrypoints/popup/pages/settings/AutoLockSettings';
import ClipboardSettings from '@/entrypoints/popup/pages/settings/ClipboardSettings';
import ContextMenuSettings from '@/entrypoints/popup/pages/settings/ContextMenuSettings';
import LanguageSettings from '@/entrypoints/popup/pages/settings/LanguageSettings';
import Unlock from '@/entrypoints/popup/pages/Unlock';
import UnlockSuccess from '@/entrypoints/popup/pages/UnlockSuccess';
import Upgrade from '@/entrypoints/popup/pages/Upgrade';
import Settings from '@/entrypoints/popup/pages/settings/Settings';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';

View File

@@ -90,7 +90,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setIsCustomDomain(!isKnownDomain);
} else {
setLocalPart(value);
setIsCustomDomain(false);
// Don't reset isCustomDomain here - preserve the current mode
// Set default domain if not already set
if (!selectedDomain && !value.includes('@')) {
@@ -107,6 +107,13 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newLocalPart = e.target.value;
// If in custom domain mode, always pass through the full value
if (isCustomDomain) {
onChange(newLocalPart);
// Stay in custom domain mode - don't auto-switch back
return;
}
// Check if new value contains '@' symbol, if so, switch to custom domain mode
if (newLocalPart.includes('@')) {
setIsCustomDomain(true);
@@ -115,10 +122,11 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
}
setLocalPart(newLocalPart);
if (!isCustomDomain && selectedDomain) {
// If the local part is empty, treat the whole field as empty
if (!newLocalPart || newLocalPart.trim() === '') {
onChange('');
} else if (selectedDomain) {
onChange(`${newLocalPart}@${selectedDomain}`);
} else {
onChange(newLocalPart);
}
}, [isCustomDomain, selectedDomain, onChange]);
@@ -126,7 +134,12 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const selectDomain = useCallback((domain: string) => {
setSelectedDomain(domain);
const cleanLocalPart = localPart.includes('@') ? localPart.split('@')[0] : localPart;
onChange(`${cleanLocalPart}@${domain}`);
// If the local part is empty, treat the whole field as empty
if (!cleanLocalPart || cleanLocalPart.trim() === '') {
onChange('');
} else {
onChange(`${cleanLocalPart}@${domain}`);
}
setIsCustomDomain(false);
setIsPopupVisible(false);
}, [localPart, onChange]);
@@ -136,13 +149,30 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const newIsCustom = !isCustomDomain;
setIsCustomDomain(newIsCustom);
if (!newIsCustom && !value.includes('@')) {
// Switching to domain chooser mode, add default domain
if (newIsCustom) {
/*
* Switching to custom domain mode
* If we have a domain-based value, extract just the local part
*/
if (value && value.includes('@')) {
const [local] = value.split('@');
onChange(local);
setLocalPart(local);
}
} else {
// Switching to domain chooser mode
const defaultDomain = showPrivateDomains && privateEmailDomains[0]
? privateEmailDomains[0]
: PUBLIC_EMAIL_DOMAINS[0];
onChange(`${localPart}@${defaultDomain}`);
setSelectedDomain(defaultDomain);
// Only add domain if we have a local part
if (localPart && localPart.trim()) {
onChange(`${localPart}@${defaultDomain}`);
} else if (value && !value.includes('@')) {
// If we have a value without @, add the domain
onChange(`${value}@${defaultDomain}`);
}
}
}, [isCustomDomain, value, localPart, showPrivateDomains, privateEmailDomains, onChange]);
@@ -167,7 +197,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
return (
<div className="space-y-2">
<label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
<label htmlFor={id} className="block font-medium text-gray-700 dark:text-gray-300">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
@@ -177,7 +207,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
<input
type="text"
id={id}
className={`flex-1 min-w-0 px-3 py-2 border ${
className={`flex-1 min-w-0 px-3 py-2 border text-sm ${
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} ${
!isCustomDomain ? 'rounded-l-md' : 'rounded-md'
@@ -209,9 +239,9 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
{showPrivateDomains && (
<div className="mb-4">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
{t('credentials.privateEmailTitle')} <span className="text-xs text-gray-500 dark:text-gray-400">({t('credentials.privateEmailAliasVaultServer')})</span>
{t('credentials.privateEmailTitle')} <span className="text-gray-500 dark:text-gray-400">({t('credentials.privateEmailAliasVaultServer')})</span>
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
<p className="text-gray-500 dark:text-gray-400 mb-3">
{t('credentials.privateEmailDescription')}
</p>
<div className="flex flex-wrap gap-2">

View File

@@ -213,7 +213,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
}
if (emails.length === 0) {
return (
<div className="text-gray-500 dark:text-gray-400 mb-4">
<div className="text-gray-500 dark:text-gray-400 mb-4 text-sm">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">{t('common.recentEmails')}</h2>
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />

View File

@@ -109,7 +109,7 @@ export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(({
}
};
const inputClasses = `mt-1 block w-full rounded-md ${
const inputClasses = `mt-1 block text-sm w-full rounded-md ${
error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
} 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`;

View File

@@ -82,7 +82,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
try {
await navigator.clipboard.writeText(value);
clipboardService.setCopied(id);
// Notify background script that clipboard was copied
await sendMessage('CLIPBOARD_COPIED', { value }, 'background');
@@ -111,7 +111,7 @@ export const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> =
onClick={copyToClipboard}
className={`w-full px-3 py-2.5 bg-white border ${
copied ? 'border-green-500 border-2' : 'border-gray-300'
} 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`}
} text-gray-900 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 ? (

View File

@@ -9,7 +9,9 @@ export enum HeaderIconType {
EXTERNAL_LINK = 'external_link',
SAVE = 'save',
PLUS = 'plus',
TAB = 'tab'
TAB = 'tab',
EYE = 'eye',
EYE_OFF = 'eye_off'
}
type HeaderIconProps = {
@@ -131,19 +133,7 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
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"
d="M5 13l4 4L19 7"
/>
</svg>
),
@@ -179,6 +169,44 @@ export const HeaderIcon: React.FC<HeaderIconProps> = ({ type, className = 'w-5 h
d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"
/>
</svg>
),
[HeaderIconType.EYE]: (
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
/>
</svg>
),
[HeaderIconType.EYE_OFF]: (
<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="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
)
};

View File

@@ -62,7 +62,7 @@ const BottomNav: React.FC = () => {
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<span className="text-xs mt-1">{t('menu.credentials')}</span>
<span className="text-sm mt-1">{t('menu.credentials')}</span>
</button>
<button
onClick={() => handleTabChange('emails')}
@@ -73,7 +73,7 @@ const BottomNav: React.FC = () => {
<svg className="w-6 h-6" 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 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span className="text-xs mt-1">{t('menu.emails')}</span>
<span className="text-sm mt-1">{t('menu.emails')}</span>
</button>
<button
onClick={() => handleTabChange('settings')}
@@ -85,7 +85,7 @@ const BottomNav: React.FC = () => {
<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>
<span className="text-xs mt-1">{t('menu.settings')}</span>
<span className="text-sm mt-1">{t('menu.settings')}</span>
</button>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useLocation } from 'react-router-dom';
import Logo from '@/entrypoints/popup/components/Logo';
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
/**
@@ -87,11 +88,15 @@ const Header: React.FC<HeaderProps> = ({
onClick={() => logoClick()}
className="flex items-center hover:opacity-80 transition-opacity"
>
<img src="/assets/images/logo.svg" alt="AliasVault" className="h-8 w-8 mr-2" />
<h1 className="text-gray-900 dark:text-white text-xl font-bold">{t('common.appName')}</h1>
<Logo
width={125}
height={40}
showText={true}
className="text-gray-900 dark:text-white"
/>
{/* Hide beta badge on Safari as it's not allowed to show non-production badges */}
{!import.meta.env.SAFARI && (
<span className="text-primary-500 text-[10px] ml-1 font-normal">BETA</span>
<span className="text-primary-500 text-[10px] font-normal">BETA</span>
)}
</button>
</div>

View File

@@ -27,7 +27,7 @@ const LoginServerInfo: React.FC = () => {
};
return (
<div className="text-xs text-gray-600 dark:text-gray-400 mb-4">
<div className="text-sm text-gray-600 dark:text-gray-400 mb-4">
({t('auth.connectingTo')}{' '}
<button
onClick={handleClick}

View File

@@ -0,0 +1,71 @@
import React from 'react';
type LogoProps = {
className?: string;
width?: number;
height?: number;
showText?: boolean;
color?: string;
}
/**
* Logo component.
*/
const Logo: React.FC<LogoProps> = ({
className = '',
width = 200,
height = 50,
showText = true,
color = 'currentColor'
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
version="1.1"
viewBox="0 0 2000 500"
width={width}
height={height}
className={className}
>
{/* Logo mark */}
<path
d="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z"
fill="#EEC170"
/>
<path
d="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z"
fill="#EEC170"
/>
<path
d="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z"
fill="#EEC170"
/>
<path
d="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z"
fill="#EEC170"
/>
<path
d="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z"
fill="#EEC170"
/>
{/* Wordmark - only show if showText is true */}
{showText && (
<text
x="550"
y="355"
fontFamily="Arial, Helvetica, sans-serif"
fontWeight="700"
fontSize="290"
letterSpacing="-7"
fill={color}
>
AliasVault
</text>
)}
</svg>
);
};
export default Logo;

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDb } from '@/entrypoints/popup/context/DbContext';
import type { PasswordSettings } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
@@ -15,7 +17,6 @@ interface IPasswordFieldProps {
error?: string;
showPassword?: boolean;
onShowPasswordChange?: (show: boolean) => void;
initialSettings: PasswordSettings;
}
/**
@@ -29,13 +30,14 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
placeholder,
error,
showPassword: controlledShowPassword,
onShowPasswordChange,
initialSettings
onShowPasswordChange
}) => {
const { t } = useTranslation();
const dbContext = useDb();
const [internalShowPassword, setInternalShowPassword] = useState(false);
const [showConfigDialog, setShowConfigDialog] = useState(false);
const [currentSettings, setCurrentSettings] = useState<PasswordSettings>(initialSettings);
const [currentSettings, setCurrentSettings] = useState<PasswordSettings | null>(null);
const [isLoaded, setIsLoaded] = useState(false);
// Use controlled or uncontrolled showPassword state
const showPassword = controlledShowPassword !== undefined ? controlledShowPassword : internalShowPassword;
@@ -51,11 +53,24 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
}
}, [controlledShowPassword, onShowPasswordChange]);
// Initialize settings only once when component mounts
// Load password settings from database
useEffect(() => {
setCurrentSettings({ ...initialSettings });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run on mount to avoid resetting user changes
/**
* Load password settings from the database.
*/
const loadSettings = async (): Promise<void> => {
try {
if (dbContext.sqliteClient) {
const settings = dbContext.sqliteClient.getPasswordSettings();
setCurrentSettings(settings);
setIsLoaded(true);
}
} catch (error) {
console.error('Error loading password settings:', error);
}
};
void loadSettings();
}, [dbContext.sqliteClient]);
const generatePassword = useCallback((settings: PasswordSettings) => {
try {
@@ -69,6 +84,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
}, [onChange, setShowPassword]);
const handleLengthChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (!currentSettings) {
return;
}
const length = parseInt(e.target.value, 10);
const newSettings = { ...currentSettings, Length: length };
setCurrentSettings(newSettings);
@@ -78,6 +96,9 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
}, [currentSettings, generatePassword]);
const handleRegeneratePassword = useCallback(() => {
if (!currentSettings) {
return;
}
generatePassword(currentSettings);
}, [generatePassword, currentSettings]);
@@ -98,6 +119,18 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
setShowConfigDialog(true);
}, []);
// Don't render until settings are loaded
if (!currentSettings || !isLoaded) {
return (
<div className="space-y-2">
<label htmlFor={id} className="block text-sm font-medium text-gray-900 dark:text-white">
{label}
</label>
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 h-10 rounded-lg"></div>
</div>
);
}
return (
<div className="space-y-2">
{/* Label */}
@@ -114,7 +147,7 @@ const PasswordField: React.FC<IPasswordFieldProps> = ({
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="outline-0 shadow-sm border border-gray-300 bg-gray-50 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
className="outline-0 text-sm shadow-sm border border-gray-300 bg-gray-50 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<div className="flex">

View File

@@ -49,7 +49,7 @@ const UsernameField: React.FC<IUsernameFieldProps> = ({
value={value}
onChange={handleInputChange}
placeholder={placeholder}
className="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
className="outline-0 text-sm shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-l-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<div className="flex">

View File

@@ -51,9 +51,15 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
const segments = location.pathname.split('/').filter(Boolean);
const historyEntries: NavigationHistoryEntry[] = [];
// Build history entries for each segment
let currentPath = '';
for (const segment of segments) {
currentPath += '/' + segment;
for (let i = 0; i < segments.length; i++) {
currentPath += '/' + segments[i];
/*
* For settings subpages, include both /settings and the subpage
* For email details, include both /emails and the specific email
*/
historyEntries.push({
pathname: currentPath,
search: location.search,

View File

@@ -51,21 +51,16 @@ const Reinitialize: React.FC = () => {
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;
// For nested routes, build up the navigation history properly
if (savedHistory?.length > 1) {
// Navigate to the base route first
navigate(savedHistory[0].pathname, { replace: true });
// Then navigate to the final destination
navigate(lastPage, { replace: false });
} else {
// Simple navigation for non-nested routes
navigate(lastPage, { replace: true });
}
// Fallback to simple navigation if no history
navigate('/credentials', { replace: true });
navigate(lastPage, { replace: true });
return;
}
}

View File

@@ -176,13 +176,13 @@ const AuthSettings: React.FC = () => {
{/* Language Settings Section */}
<div className="mb-6">
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
<p className="font-medium text-gray-900 dark:text-white">{t('common.language')}</p>
<LanguageSwitcher variant="dropdown" size="sm" />
</div>
</div>
<div className="mb-6">
<label htmlFor="api-connection" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
<label htmlFor="api-connection" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
{t('settings.serverUrl')}
</label>
<select
@@ -201,7 +201,7 @@ const AuthSettings: React.FC = () => {
{selectedOption === 'custom' && (
<>
<div className="mb-6">
<label htmlFor="custom-client-url" className="block text-sm font-medium text-gray-700 dark:text-gray-200 mb-2">
<label htmlFor="custom-client-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
Custom client URL
</label>
<input
@@ -217,7 +217,7 @@ const AuthSettings: React.FC = () => {
)}
</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">
<label htmlFor="custom-api-url" className="block font-medium text-gray-700 dark:text-gray-200 mb-2">
Custom API URL
</label>
<input
@@ -238,7 +238,7 @@ const AuthSettings: React.FC = () => {
{/* Autofill Popup Settings Section */}
<div className="mb-6">
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillEnabled')}</p>
<button
onClick={toggleGlobalPopup}
className={`px-4 py-2 rounded-md transition-colors ${

View File

@@ -6,13 +6,14 @@ 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 { HeaderIcon, 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 { 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 SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
@@ -21,8 +22,6 @@ import type { VaultResponse, LoginResponse } from '@/utils/dist/shared/models/we
import EncryptionUtility from '@/utils/EncryptionUtility';
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
import ConversionUtility from '../utils/ConversionUtility';
import { storage } from '#imports';
/**
@@ -40,6 +39,7 @@ const Login: React.FC = () => {
});
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
const [rememberMe, setRememberMe] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
const [passwordHashBase64, setPasswordHashBase64] = useState<string | null>(null);
@@ -362,11 +362,11 @@ const Login: React.FC = () => {
<h2 className="text-xl font-bold dark:text-gray-200">{t('auth.loginTitle')}</h2>
<LoginServerInfo />
<div className="mb-4">
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="username">
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="username">
{t('auth.username')}
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
className="shadow text-sm appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 leading-tight focus:outline-none focus:shadow-outline"
id="username"
type="text"
name="username"
@@ -377,19 +377,29 @@ const Login: React.FC = () => {
/>
</div>
<div className="mb-4">
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
{t('auth.password')}
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type="password"
name="password"
placeholder={t('auth.passwordPlaceholder')}
value={credentials.password}
onChange={handleChange}
required
/>
<div className="relative">
<input
className="shadow text-sm appearance-none border rounded w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type={showPassword ? "text" : "password"}
name="password"
placeholder={t('auth.passwordPlaceholder')}
value={credentials.password}
onChange={handleChange}
required
/>
<button
type="button"
className="absolute right-2 top-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
onClick={() => setShowPassword(!showPassword)}
tabIndex={-1}
>
<HeaderIcon type={showPassword ? HeaderIconType.EYE_OFF : HeaderIconType.EYE} className="w-5 h-5 text-gray-400 dark:text-gray-500" />
</button>
</div>
</div>
<div className="mb-6">
<label className="flex items-center">
@@ -408,7 +418,7 @@ const Login: React.FC = () => {
</Button>
</div>
</form>
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
<div className="text-center text-gray-600 dark:text-gray-400">
{t('auth.noAccount')}{' '}
<a
href={clientUrl ?? ''}

View File

@@ -6,7 +6,7 @@ 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 { HeaderIcon, 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';
@@ -35,6 +35,7 @@ const Unlock: React.FC = () => {
const srpUtil = new SrpUtility(webApi);
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null);
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
@@ -144,10 +145,10 @@ const Unlock: React.FC = () => {
</div>
</div>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
<p className="font-medium text-gray-900 dark:text-white">
{authContext.username}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('auth.loggedIn')}
</p>
</div>
@@ -159,32 +160,42 @@ const Unlock: React.FC = () => {
</h2>
{error && (
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
<div className="mb-4 text-red-500 dark:text-red-400">
{error}
</div>
)}
<div className="mb-2">
<label className="block text-gray-700 dark:text-gray-200 text-sm font-bold mb-2" htmlFor="password">
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
{t('auth.masterPassword')}
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t('auth.passwordPlaceholder')}
required
autoFocus
/>
<div className="relative">
<input
className="shadow appearance-none border rounded w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-800 dark:border-gray-600 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t('auth.passwordPlaceholder')}
required
autoFocus
/>
<button
type="button"
className="absolute right-2 top-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
onClick={() => setShowPassword(!showPassword)}
tabIndex={-1}
>
<HeaderIcon type={showPassword ? HeaderIconType.EYE_OFF : HeaderIconType.EYE} className="w-5 h-5 text-gray-400 dark:text-gray-500" />
</button>
</div>
</div>
<Button type="submit">
{t('auth.unlockVault')}
</Button>
<div className="text-sm font-medium text-gray-500 dark:text-gray-200 mt-6">
<div className="font-medium text-gray-500 dark:text-gray-200 mt-6">
{t('auth.switchAccounts')} <button onClick={handleLogout} className="text-primary-700 hover:underline dark:text-primary-500">{t('auth.logout')}</button>
</div>
</form>

View File

@@ -239,7 +239,7 @@ const Upgrade: React.FC = () => {
title={t('upgrade.alerts.selfHostedServer')}
message={t('upgrade.alerts.selfHostedWarning')}
confirmText={t('upgrade.alerts.continueUpgrade')}
cancelText={t('upgrade.alerts.cancel')}
cancelText={t('common.cancel')}
/>
{/* Version info modal */}
@@ -253,7 +253,7 @@ const Upgrade: React.FC = () => {
<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">
<div className="mb-4 text-red-500 dark:text-red-400">
{error}
</div>
)}
@@ -268,7 +268,7 @@ const Upgrade: React.FC = () => {
</div>
</div>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
<p className="font-medium text-gray-900 dark:text-white">
{username}
</p>
</div>
@@ -277,12 +277,12 @@ const Upgrade: React.FC = () => {
<h2 className="text-xl font-bold dark:text-gray-200 mb-4">{t('upgrade.title')}</h2>
<div className="mb-6">
<p className="text-gray-700 dark:text-gray-200 text-sm mb-4">
<p className="text-gray-700 dark:text-gray-200 mb-4">
{t('upgrade.subtitle')}
</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">{t('upgrade.versionInformation')}</span>
<span className="font-medium text-gray-700 dark:text-gray-200">{t('upgrade.versionInformation')}</span>
<button
type="button"
onClick={showVersionDialog}

View File

@@ -23,9 +23,13 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
import { SKIP_FORM_RESTORE_KEY } from '@/utils/Constants';
import { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender } from '@/utils/dist/shared/identity-generator';
import type { Attachment, Credential } from '@/utils/dist/shared/models/vault';
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
import { browser } from '#imports';
type CredentialMode = 'random' | 'manual';
@@ -90,6 +94,13 @@ const CredentialAddEdit: React.FC = () => {
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
const webApi = useWebApi();
// Track last generated values to avoid overwriting manual entries
const [lastGeneratedValues, setLastGeneratedValues] = useState<{
username: string | null;
password: string | null;
email: string | null;
}>({ username: null, password: null, email: null });
const serviceNameRef = useRef<HTMLInputElement>(null);
const { handleSubmit, setValue, watch, formState: { errors } } = useForm<Credential>({
@@ -223,20 +234,80 @@ const CredentialAddEdit: React.FC = () => {
}
if (!id) {
// On create mode, focus the service name field after a short delay to ensure the component is mounted.
// On create mode, check for URL parameters first, then fallback to tab detection
const urlParams = new URLSearchParams(window.location.search);
const serviceName = urlParams.get('serviceName');
const serviceUrl = urlParams.get('serviceUrl');
const currentUrl = urlParams.get('currentUrl');
/**
* Initialize service detection from URL parameters or current tab
*/
const initializeServiceDetection = async (): Promise<void> => {
try {
// If URL parameters are present (e.g., from content script popout), use them
if (serviceName || serviceUrl || currentUrl) {
if (serviceName) {
setValue('ServiceName', decodeURIComponent(serviceName));
}
if (serviceUrl) {
setValue('ServiceUrl', decodeURIComponent(serviceUrl));
}
// If we have currentUrl but missing serviceName or serviceUrl, derive them
if (currentUrl && (!serviceName || !serviceUrl)) {
const decodedCurrentUrl = decodeURIComponent(currentUrl);
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(decodedCurrentUrl);
if (!serviceName && serviceInfo.suggestedNames.length > 0) {
setValue('ServiceName', serviceInfo.suggestedNames[0]);
}
if (!serviceUrl && serviceInfo.serviceUrl) {
setValue('ServiceUrl', serviceInfo.serviceUrl);
}
}
return;
}
// Otherwise, detect from current active tab (for dashboard case)
const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true });
if (activeTab?.url) {
const serviceInfo = ServiceDetectionUtility.getServiceInfoFromTab(
activeTab.url,
activeTab.title
);
if (serviceInfo.suggestedNames.length > 0) {
setValue('ServiceName', serviceInfo.suggestedNames[0]);
}
if (serviceInfo.serviceUrl) {
setValue('ServiceUrl', serviceInfo.serviceUrl);
}
}
} catch (error) {
console.error('Error detecting service information:', error);
}
};
initializeServiceDetection();
// 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().then(() => {
// Generate default password if no persisted password exists
if (!watch('Password')) {
const passwordSettings = dbContext.sqliteClient!.getPasswordSettings();
const passwordGenerator = CreatePasswordGenerator(passwordSettings);
const defaultPassword = passwordGenerator.generateRandomPassword();
setValue('Password', defaultPassword);
// Check if we should skip form restoration (e.g., when opened from popout button)
browser.storage.local.get([SKIP_FORM_RESTORE_KEY]).then((result) => {
if (result[SKIP_FORM_RESTORE_KEY]) {
// Clear the flag after using it
browser.storage.local.remove([SKIP_FORM_RESTORE_KEY]);
// Don't load persisted values, but set local loading to false
setLocalLoading(false);
} else {
// Load persisted form values normally
loadPersistedValues();
}
});
return;
@@ -271,7 +342,7 @@ const CredentialAddEdit: React.FC = () => {
console.error('Error loading credential:', err);
setIsInitialLoading(false);
}
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, watch]);
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue, loadPersistedValues, clearPersistedValues]);
/**
* Handle the delete button click.
@@ -331,35 +402,63 @@ const CredentialAddEdit: React.FC = () => {
const defaultEmailDomain = dbContext.sqliteClient!.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
setValue('Alias.Email', email);
// Check current values
const currentUsername = watch('Username') ?? '';
const currentPassword = watch('Password') ?? '';
const currentEmail = watch('Alias.Email') ?? '';
// Only overwrite email if it's empty or matches the last generated value
if (!currentEmail || currentEmail === lastGeneratedValues.email) {
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
// Only overwrite username if it's empty or matches the last generated value
if (!currentUsername || currentUsername === lastGeneratedValues.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
// Only overwrite password if it's empty or matches the last generated value
if (!currentPassword || currentPassword === lastGeneratedValues.password) {
setValue('Password', password);
}
}, [isEditMode, watch, setValue, initializeGenerators, dbContext]);
// Update tracking with new generated values
setLastGeneratedValues({
username: identity.nickName,
password: password,
email: email
});
}, [watch, setValue, initializeGenerators, dbContext, lastGeneratedValues, setLastGeneratedValues]);
/**
* Clear all alias fields.
*/
const clearAliasFields = useCallback(() => {
setValue('Alias.FirstName', '');
setValue('Alias.LastName', '');
setValue('Alias.NickName', '');
setValue('Alias.Gender', '');
setValue('Alias.BirthDate', '');
}, [setValue]);
// Check if any alias fields have values.
const hasAliasValues = !!(watch('Alias.FirstName') || watch('Alias.LastName') || watch('Alias.NickName') || watch('Alias.Gender') || watch('Alias.BirthDate'));
/**
* Handle the generate random alias button press.
*/
const handleGenerateRandomAlias = useCallback(() => {
void generateRandomAlias();
}, [generateRandomAlias]);
if (hasAliasValues) {
clearAliasFields();
} else {
void generateRandomAlias();
}
}, [generateRandomAlias, clearAliasFields, hasAliasValues]);
const generateRandomUsername = useCallback(async () => {
try {
@@ -382,15 +481,17 @@ const CredentialAddEdit: React.FC = () => {
};
const username = usernameEmailGenerator.generateUsername(identity);
setValue('Username', username);
const currentUsername = watch('Username') ?? '';
// Only overwrite username if it's empty or matches the last generated value
if (!currentUsername || currentUsername === lastGeneratedValues.username) {
setValue('Username', username);
// Update the tracking for username
setLastGeneratedValues(prev => ({ ...prev, username: username }));
}
} catch (error) {
console.error('Error generating random username:', error);
}
}, [setValue, watch]);
const initialPasswordSettings = useMemo(() => {
return dbContext.sqliteClient?.getPasswordSettings();
}, [dbContext.sqliteClient]);
}, [setValue, watch, lastGeneratedValues, setLastGeneratedValues]);
/**
* Handle form submission.
@@ -536,8 +637,8 @@ const CredentialAddEdit: React.FC = () => {
<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'
className={`flex-1 py-2 text-sm px-4 rounded flex items-center justify-center gap-2 ${
mode === 'random' ? 'bg-primary-500 text-white font-medium' : '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">
@@ -553,8 +654,8 @@ const CredentialAddEdit: React.FC = () => {
<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'
className={`flex-1 py-2 text-sm px-4 rounded flex items-center justify-center gap-2 ${
mode === 'manual' ? 'bg-primary-500 text-white font-medium' : '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">
@@ -609,18 +710,15 @@ const CredentialAddEdit: React.FC = () => {
error={errors.Username?.message}
onRegenerate={generateRandomUsername}
/>
{initialPasswordSettings && (
<PasswordField
id="password"
label={t('common.password')}
value={watch('Password') ?? ''}
onChange={(value) => setValue('Password', value)}
error={errors.Password?.message}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
initialSettings={initialPasswordSettings}
/>
)}
<PasswordField
id="password"
label={t('common.password')}
value={watch('Password') ?? ''}
onChange={(value) => setValue('Password', value)}
error={errors.Password?.message}
showPassword={showPassword}
onShowPasswordChange={setShowPassword}
/>
</div>
</div>
@@ -630,17 +728,33 @@ const CredentialAddEdit: React.FC = () => {
<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 flex items-center justify-center gap-2"
className={`w-full text-sm py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-offset-2 flex items-center justify-center gap-2 ${
hasAliasValues
? 'bg-gray-500 text-white hover:bg-gray-600 focus:ring-gray-500'
: 'bg-primary-500 text-white hover:bg-primary-600 focus:ring-primary-500'
}`}
>
<svg className='w-5 h-5 inline-block' 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>
<span>{t('credentials.generateRandomAlias')}</span>
{hasAliasValues ? (
<>
<svg className='w-5 h-5 inline-block' viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
<span>{t('credentials.clearAliasFields')}</span>
</>
) : (
<>
<svg className='w-5 h-5 inline-block' 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>
<span>{t('credentials.generateRandomAlias')}</span>
</>
)}
</button>
<FormInput
id="firstName"

View File

@@ -188,10 +188,10 @@ const CredentialsList: React.FC = () => {
{credentials.length === 0 ? (
<div className="text-gray-500 dark:text-gray-400 space-y-2 mb-10">
<p className="text-sm">
<p>
{t('credentials.welcomeTitle')}
</p>
<p className="text-sm">
<p>
{t('credentials.welcomeDescription')}
</p>
</div>

View File

@@ -16,8 +16,8 @@ import EncryptionUtility from '@/utils/EncryptionUtility';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import HeaderButton from '../components/HeaderButton';
import { HeaderIconType } from '../components/Icons/HeaderIcons';
import HeaderButton from '../../components/HeaderButton';
import { HeaderIconType } from '../../components/Icons/HeaderIcons';
/**
* Email details page.
@@ -32,6 +32,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
const [email, setEmail] = useState<Email | null>(null);
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showMetadata, setShowMetadata] = useState(false);
const { setIsInitialLoading } = useLoading();
const { setHeaderButtons } = useHeaderButtons();
const [headerButtonsConfigured, setHeaderButtonsConfigured] = useState(false);
@@ -207,21 +208,44 @@ const EmailDetails: React.FC = (): React.ReactElement => {
variant="danger"
/>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md">
<div>
{/* 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>
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<p>{t('emails.from')} {email.fromDisplay} ({email.fromLocal}@{email.fromDomain})</p>
<p>{t('emails.to')} {email.toLocal}@{email.toDomain}</p>
<p>{t('emails.date')} {new Date(email.dateSystem).toLocaleString()}</p>
<div>
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<h1 className="text-lg font-bold text-gray-900 dark:text-white">{email.subject}</h1>
<button
onClick={() => setShowMetadata(!showMetadata)}
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title={showMetadata ? t('common.hideDetails') : t('common.showDetails')}
>
<svg
className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${showMetadata ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
</div>
</div>
{showMetadata && (
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400 mt-2">
<p><span className="font-bold">{t('emails.from')}</span> <span title={email.fromLocal + "@" + email.fromDomain}>{email.fromDisplay}</span></p>
<p><span className="font-bold">{t('emails.to')}</span> <span title={email.toLocal + "@" + email.toDomain}>{email.toLocal}@{email.toDomain}</span></p>
<p><span className="font-bold">{t('emails.date')}</span> {new Date(email.dateSystem).toLocaleString()}</p>
</div>
)}
</div>
{/* Email Body */}
<div className="bg-white">
<div className="bg-white mt-4">
{email.messageHtml ? (
<iframe
srcDoc={ConversionUtility.convertAnchorTagsToOpenInNewTab(email.messageHtml)}

View File

@@ -177,14 +177,14 @@ const EmailsList: React.FC = () => {
className="block p-4 bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<div className="flex justify-between items-start mb-2">
<div className="text-sm text-gray-900 dark:text-white mb-1 font-bold">
<div className="text-gray-900 dark:text-white mb-1 font-bold">
{email.subject}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{formatEmailDate(email.dateSystem)}
</div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
<div className="text-gray-600 text-sm dark:text-gray-300 line-clamp-2">
{email.messagePreview}
</div>
</Link>

View File

@@ -48,14 +48,14 @@ const AutoLockSettings: React.FC = () => {
<div className="p-4">
<div>
<div className="flex items-center mb-2">
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</p>
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</p>
<HelpModal
titleKey="settings.autoLockTimeout"
contentKey="settings.autoLockTimeoutHelp"
className="ml-2"
/>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3">{t('settings.autoLockTimeoutDescription')}</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.autoLockTimeoutDescription')}</p>
<select
value={autoLockTimeout}
onChange={(e) => setAutoLockTimeoutSetting(Number(e.target.value))}

View File

@@ -167,8 +167,8 @@ const AutofillSettings: React.FC = () => {
<div className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopup')}</p>
<p className={`text-xs mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillPopup')}</p>
<p className={`text-sm mt-1 ${settings.isGloballyEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
{settings.isGloballyEnabled ? t('settings.activeOnAllSites') : t('settings.disabledOnAllSites')}
</p>
</div>
@@ -195,12 +195,12 @@ const AutofillSettings: React.FC = () => {
<div className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.autofillPopupOn')}{settings.currentUrl}</p>
<p className={`text-xs mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autofillPopupOn')}{settings.currentUrl}</p>
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
{settings.isEnabled ? t('settings.enabledForThisSite') : t('settings.disabledForThisSite')}
</p>
{!settings.isEnabled && settings.temporaryDisabledUrls[settings.currentUrl] && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{t('settings.temporarilyDisabledUntil')}{new Date(settings.temporaryDisabledUrls[settings.currentUrl]).toLocaleTimeString()}
</p>
)}
@@ -238,8 +238,8 @@ const AutofillSettings: React.FC = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.autofillMatchingMode')}</p>
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3">{t('settings.autofillMatchingModeDescription')}</p>
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.autofillMatchingMode')}</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.autofillMatchingModeDescription')}</p>
<select
value={autofillMatchingMode}
onChange={(e) => setAutofillMatchingModeSetting(e.target.value as AutofillMatchingMode)}

View File

@@ -46,8 +46,8 @@ const ClipboardSettings: React.FC = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.clipboardClearTimeout')}</p>
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3">{t('settings.clipboardClearTimeoutDescription')}</p>
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.clipboardClearTimeout')}</p>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{t('settings.clipboardClearTimeoutDescription')}</p>
<select
value={clipboardTimeout}
onChange={(e) => setClipboardClearTimeout(Number(e.target.value))}

View File

@@ -49,11 +49,11 @@ const ContextMenuSettings: React.FC = () => {
<div className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.rightClickContextMenu')}</p>
<p className={`text-xs mt-1 ${isContextMenuEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
<p className="font-medium text-gray-900 dark:text-white">{t('settings.rightClickContextMenu')}</p>
<p className={`text-sm mt-1 ${isContextMenuEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
{isContextMenuEnabled ? t('settings.contextMenuEnabled') : t('settings.contextMenuDisabled')}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
{t('settings.contextMenuDescription')}
</p>
</div>

View File

@@ -1,13 +1,20 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import LanguageSwitcher from '@/entrypoints/popup/components/LanguageSwitcher';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
/**
* Language settings page component.
*/
const LanguageSettings: React.FC = () => {
const { t } = useTranslation();
const { setIsInitialLoading } = useLoading();
useEffect(() => {
// Mark initial loading as complete
setIsInitialLoading(false);
}, [setIsInitialLoading]);
return (
<div className="space-y-6">
@@ -16,7 +23,7 @@ const LanguageSettings: React.FC = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-3">{t('settings.selectLanguage')}</p>
<p className="font-medium text-gray-900 dark:text-white mb-3">{t('settings.selectLanguage')}</p>
<LanguageSwitcher variant="dropdown" size="sm" />
</div>
</div>

View File

@@ -167,10 +167,10 @@ const Settings: React.FC = () => {
</div>
</div>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
<p className="text font-medium text-gray-900 dark:text-white">
{authContext.username}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('settings.loggedIn')}
</p>
</div>
@@ -224,7 +224,7 @@ const Settings: React.FC = () => {
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<span className="text-sm text-gray-900 dark:text-white">{t('settings.autofillSettings')}</span>
<span className="text-gray-900 dark:text-white">{t('settings.autofillSettings')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
@@ -256,7 +256,7 @@ const Settings: React.FC = () => {
d="M4 6h16M4 12h16m-7 6h7"
/>
</svg>
<span className="text-sm text-gray-900 dark:text-white">{t('settings.contextMenuSettings')}</span>
<span className="text-gray-900 dark:text-white">{t('settings.contextMenuSettings')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
@@ -288,7 +288,7 @@ const Settings: React.FC = () => {
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<span className="text-sm text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</span>
<span className="text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
@@ -320,7 +320,7 @@ const Settings: React.FC = () => {
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
/>
</svg>
<span className="text-sm text-gray-900 dark:text-white">{t('settings.clipboardSettings')}</span>
<span className="text-gray-900 dark:text-white">{t('settings.clipboardSettings')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
@@ -352,7 +352,7 @@ const Settings: React.FC = () => {
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
/>
</svg>
<span className="text-sm text-gray-900 dark:text-white">{t('settings.language')}</span>
<span className="text-gray-900 dark:text-white">{t('settings.language')}</span>
</div>
<svg
className="w-4 h-4 text-gray-400"
@@ -374,7 +374,7 @@ const Settings: React.FC = () => {
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-4">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white mb-2">{t('settings.theme')}</p>
<p className="font-medium text-gray-900 dark:text-white mb-2">{t('settings.theme')}</p>
<div className="flex flex-col space-y-2">
<label className="flex items-center">
<input
@@ -385,7 +385,7 @@ const Settings: React.FC = () => {
onChange={() => setThemePreference('system')}
className="mr-2"
/>
<span className="text-xs text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
</label>
<label className="flex items-center">
<input
@@ -396,7 +396,7 @@ const Settings: React.FC = () => {
onChange={() => setThemePreference('light')}
className="mr-2"
/>
<span className="text-xs text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
</label>
<label className="flex items-center">
<input
@@ -407,7 +407,7 @@ const Settings: React.FC = () => {
onChange={() => setThemePreference('dark')}
className="mr-2"
/>
<span className="text-xs text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
</label>
</div>
</div>
@@ -423,7 +423,7 @@ const Settings: React.FC = () => {
<div className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
<p className="font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
</div>
<button
onClick={openKeyboardShortcuts}

View File

@@ -1,3 +1,7 @@
body {
font-size: 75%;
html {
font-size: 14px;
}
body {
font-size: 100%;
}

View File

@@ -6,8 +6,10 @@
import deTranslations from './locales/de.json';
import enTranslations from './locales/en.json';
import fiTranslations from './locales/fi.json';
import heTranslations from './locales/he.json';
import itTranslations from './locales/it.json';
import nlTranslations from './locales/nl.json';
import ukTranslations from './locales/uk.json';
import zhTranslations from './locales/zh.json';
/**
@@ -24,12 +26,18 @@ export const LANGUAGE_RESOURCES = {
fi: {
translation: fiTranslations
},
he: {
translation: heTranslations
},
it: {
translation: itTranslations
},
nl: {
translation: nlTranslations
},
uk: {
translation: ukTranslations
},
zh: {
translation: zhTranslations
},
@@ -58,6 +66,12 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
nativeName: 'Suomi',
flag: '🇫🇮'
},
{
code: 'he',
name: 'Hebrew',
nativeName: 'עברית',
flag: '🇮🇱'
},
{
code: 'it',
name: 'Italian',
@@ -70,6 +84,12 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
nativeName: 'Nederlands',
flag: '🇳🇱'
},
{
code: 'uk',
name: 'Ukrainian',
nativeName: 'Українська',
flag: '🇺🇦'
},
{
code: 'zh',
name: 'Chinese',
@@ -77,12 +97,6 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
flag: '🇨🇳'
},
/*
* {
* code: 'de',
* name: 'German',
* nativeName: 'Deutsch',
* flag: '🇩🇪'
* },
* {
* code: 'es',
* name: 'Spanish',
@@ -95,12 +109,6 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
* nativeName: 'Français',
* flag: '🇫🇷'
* },
* {
* code: 'uk',
* name: 'Ukrainian',
* nativeName: 'Українська',
* flag: '🇺🇦'
* }
*/
];

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Mehrdeutige Zeichen (1, l, I, 0, O, etc.) vermeiden",
"generateNewPreview": "Neue Vorschau erstellen",
"generateRandomAlias": "Zufälligen Alias generieren",
"clearAliasFields": "Alias-Felder löschen",
"alias": "Alias",
"firstName": "Vorname",
"lastName": "Nachname",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Vältä epäselviä merkkejä (o, 0 jne.)",
"generateNewPreview": "Luo uusi esikatselu",
"generateRandomAlias": "Luo sattumanvarainen alias",
"clearAliasFields": "Tyhjennä aliaksen kentät",
"alias": "Alias",
"firstName": "Etunimi",
"lastName": "Sukunimi",

View File

@@ -1,392 +1,393 @@
{
"auth": {
"loginTitle": "Log in to AliasVault",
"username": "Username or email",
"usernamePlaceholder": "name / name@company.com",
"password": "Password",
"passwordPlaceholder": "Enter your password",
"rememberMe": "Remember me",
"loginButton": "Login",
"noAccount": "No account yet?",
"createVault": "Create new vault",
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
"authCode": "Authentication Code",
"authCodePlaceholder": "Enter 6-digit code",
"verify": "Verify",
"cancel": "Cancel",
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
"masterPassword": "Master Password",
"unlockVault": "Unlock Vault",
"unlockTitle": "Unlock Your Vault",
"unlockDescription": "Enter your master password to unlock your vault.",
"logout": "Logout",
"logoutConfirm": "Are you sure you want to logout?",
"sessionExpired": "Your session has expired. Please log in again.",
"unlockSuccess": "Vault unlocked successfully!",
"unlockSuccessTitle": "Your vault is successfully unlocked",
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
"closePopup": "Close this popup",
"browseVault": "Browse vault contents",
"connectingTo": "Connecting to",
"switchAccounts": "Switch accounts?",
"loggedIn": "Logged in",
"loginTitle": "Se connecter à AliasVault",
"username": "Nom d'utilisateur ou email",
"usernamePlaceholder": "nom / nom@entreprise.com",
"password": "Mot de passe",
"passwordPlaceholder": "Saisissez votre mot de passe",
"rememberMe": "Se souvenir de moi",
"loginButton": "Se connecter",
"noAccount": "Pas de compte?",
"createVault": "Créer un nouveau coffre",
"twoFactorTitle": "Veuillez entrer le code d'authentification de votre application d'authentification.",
"authCode": "Code d'authentification",
"authCodePlaceholder": "Saisissez le code à 6 chiffres",
"verify": "Vérifier",
"cancel": "Annuler",
"twoFactorNote": "Remarque : si vous n'avez pas accès à votre appareil d'authentification, vous pouvez réinitialiser votre authentification à double facteur avec un code de récupération en vous connectant via le site web.",
"masterPassword": "Mot de passe principal",
"unlockVault": "Déverrouiller le coffre",
"unlockTitle": "Déverrouiller votre coffre",
"unlockDescription": "Entrez votre mot de passe principal pour déverrouiller votre coffre-fort.",
"logout": "Se déconnecter",
"logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ?",
"sessionExpired": "Votre session a expiré. Veuillez vous reconnecter.",
"unlockSuccess": "Parcourir le contenu du coffre",
"unlockSuccessTitle": "Votre coffre a été déverrouillé avec succès",
"unlockSuccessDescription": "Vous pouvez maintenant utiliser le remplissage automatique des formulaires de connexion dans votre navigateur.",
"closePopup": "Fermer cette popup",
"browseVault": "Parcourir le contenu du coffre",
"connectingTo": "Connexion à",
"switchAccounts": "Changer de compte ?",
"loggedIn": "Connecté(e)",
"errors": {
"invalidCode": "Please enter a valid 6-digit authentication code.",
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
"noToken": "Login failed -- no token returned",
"migrationError": "An error occurred while checking for pending migrations.",
"wrongPassword": "Incorrect password. Please try again.",
"accountLocked": "Account temporarily locked due to too many failed attempts.",
"networkError": "Network error. Please check your connection and try again.",
"loginDataMissing": "Login session expired. Please try again."
"invalidCode": "Veuillez entrer un code d'authentification valide à 6 chiffres.",
"serverError": "Impossible d'accéder au serveur AliasVault. Veuillez réessayer plus tard ou contacter le support si le problème persiste.",
"noToken": "Échec de la connexion -- aucun jeton retourné",
"migrationError": "Une erreur s'est produite lors de la vérification des migrations en attente.",
"wrongPassword": "Mot de passe incorrect, veuillez réessayer.",
"accountLocked": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives échouées.",
"networkError": "Erreur réseau. Vérifiez votre connexion et réessayez.",
"loginDataMissing": "La session a expiré. Veuillez réessayer."
}
},
"menu": {
"credentials": "Credentials",
"credentials": "Identifiants",
"emails": "Emails",
"settings": "Settings"
"settings": "Réglages"
},
"common": {
"appName": "AliasVault",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"use": "Use",
"delete": "Delete",
"close": "Close",
"copied": "Copied!",
"openInNewWindow": "Open in new window",
"loading": "Chargement...",
"error": "Erreur",
"success": "Succès",
"cancel": "Annuler",
"use": "Utiliser",
"delete": "Supprimer",
"close": "Fermer",
"copied": "Copié !",
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
"language": "Language",
"enabled": "Enabled",
"disabled": "Disabled",
"showPassword": "Show password",
"hidePassword": "Hide password",
"copyToClipboard": "Copy to clipboard",
"loadingEmails": "Loading emails...",
"loadingTotpCodes": "Loading TOTP codes...",
"attachments": "Attachments",
"loadingAttachments": "Loading attachments...",
"settings": "Settings",
"recentEmails": "Recent emails",
"loginCredentials": "Login credentials",
"twoFactorAuthentication": "Two-factor authentication",
"enabled": "Activé",
"disabled": "Désactivé",
"showPassword": "Afficher le mot de passe",
"hidePassword": "Cacher le mot de passe",
"copyToClipboard": "Copier dans le presse-papiers",
"loadingEmails": "Chargement des emails...",
"loadingTotpCodes": "Chargement des codes TOTP...",
"attachments": "Pièces jointes",
"loadingAttachments": "Chargement des pièces jointes...",
"settings": "Réglages",
"recentEmails": "Emails récents",
"loginCredentials": "Identifiants de connexion",
"twoFactorAuthentication": "Authentification à double facteur",
"alias": "Alias",
"notes": "Notes",
"fullName": "Full Name",
"firstName": "First Name",
"lastName": "Last Name",
"birthDate": "Birth Date",
"nickname": "Nickname",
"fullName": "Nom complet",
"firstName": "Prénom",
"lastName": "Nom",
"birthDate": "Date de naissance",
"nickname": "Surnom",
"email": "Email",
"username": "Username",
"password": "Password",
"syncingVault": "Syncing vault",
"savingChangesToVault": "Saving changes to vault",
"uploadingVaultToServer": "Uploading vault to server",
"checkingVaultUpdates": "Checking for vault updates",
"syncingUpdatedVault": "Syncing updated vault",
"executingOperation": "Executing operation...",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"syncingVault": "Synchronisation du coffre",
"savingChangesToVault": "Enregistrement des modifications dans le coffre",
"uploadingVaultToServer": "Envoi du coffre vers le serveur",
"checkingVaultUpdates": "Vérification des mises à jour du coffre",
"syncingUpdatedVault": "Synchronisation du coffre mis à jour",
"executingOperation": "Exécution de l'opération...",
"loadMore": "Voir plus",
"errors": {
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
"unknownError": "An unknown error occurred",
"failedToStoreVault": "Failed to store vault",
"vaultNotAvailable": "Vault not available",
"failedToRetrieveData": "Failed to retrieve data",
"vaultIsLocked": "Vault is locked",
"failedToUploadVault": "Failed to upload vault",
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
"VaultOutdated": "Votre coffre est obsolète. Veuillez vous connecter sur le site AliasVault et suivre les étapes.",
"serverNotAvailable": "Le serveur d'AliasVault n'est pas disponible. Veuillez réessayer plus tard ou contacter le support si le problème persiste.",
"clientVersionNotSupported": "Cette version de l'extension de navigateur AliasVault n'est plus prise en charge par le serveur. Veuillez mettre à jour votre extension de navigateur à la dernière version.",
"serverVersionNotSupported": "Le serveur d'AliasVault doit être mis à jour vers une version plus récente afin d'utiliser cette extension de navigateur. Veuillez contacter le support si vous avez besoin d'aide.",
"unknownError": "Une erreur inconnue s'est produite",
"failedToStoreVault": "Échec du stockage du coffre",
"vaultNotAvailable": "Coffre non disponible",
"failedToRetrieveData": "Échec de la récupération des données",
"vaultIsLocked": "Le coffre est verrouillé",
"failedToUploadVault": "Échec du téléchargement du coffre",
"passwordChanged": "Votre mot de passe a changé depuis la dernière fois que vous vous êtes connecté. Veuillez vous reconnecter pour des raisons de sécurité."
},
"apiErrors": {
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
"USERNAME_REQUIRED": "Username is required.",
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
"USERNAME_AVAILABLE": "Username is available.",
"USERNAME_MISMATCH": "Username does not match the current user.",
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
"USERNAME_INVALID_EMAIL": "Invalid email address.",
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
"INTERNAL_SERVER_ERROR": "Internal server error.",
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite. Merci de réessayer.",
"ACCOUNT_LOCKED": "Compte temporairement verrouillé en raison d'un trop grand nombre de tentatives infructueuses. Veuillez réessayer plus tard.",
"ACCOUNT_BLOCKED": "Votre compte a été désactivé. Si vous pensez que c'est une erreur, veuillez contacter le support.",
"USER_NOT_FOUND": "Nom d'utilisateur ou mot de passe invalide. Veuillez réessayer.",
"INVALID_AUTHENTICATOR_CODE": "Code d'authentification invalide. Veuillez réessayer.",
"INVALID_RECOVERY_CODE": "Code de récupération invalide. Veuillez réessayer.",
"REFRESH_TOKEN_REQUIRED": "Un jeton d'actualisation est requis.",
"INVALID_REFRESH_TOKEN": "Jeton d'actualisation invalide.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Le jeton d'actualisation a été révoqué.",
"PUBLIC_REGISTRATION_DISABLED": "L'enregistrement d'un nouveau compte est actuellement désactivé sur ce serveur. Veuillez contacter l'administrateur.",
"USERNAME_REQUIRED": "Nom dutilisateur requis.",
"USERNAME_ALREADY_IN_USE": "Nom d'utilisateur déjà utilisé.",
"USERNAME_AVAILABLE": "Ce nom d'utilisateur est disponible.",
"USERNAME_MISMATCH": "Le nom d'utilisateur ne correspond pas à l'utilisateur actuel.",
"PASSWORD_MISMATCH": "Le mot de passe indiqué ne correspond pas à votre mot de passe actuel.",
"ACCOUNT_SUCCESSFULLY_DELETED": "Compte supprimé avec succès.",
"USERNAME_EMPTY_OR_WHITESPACE": "Le nom d'utilisateur ne peut pas être vide ou contenir un espace.",
"USERNAME_TOO_SHORT": "Le nom d'utilisateur est trop court : il doit comporter au moins 3 caractères.",
"USERNAME_TOO_LONG": "Le nom d'utilisateur est trop long : il ne peut pas contenir plus de 40 caractères.",
"USERNAME_INVALID_EMAIL": "Adresse e-mail invalide.",
"USERNAME_INVALID_CHARACTERS": "Le nom d'utilisateur n'est pas valide, il ne peut contenir que des lettres ou des chiffres.",
"VAULT_NOT_UP_TO_DATE": "Votre coffre n'est pas à jour. Veuillez synchroniser votre coffre et réessayer.",
"INTERNAL_SERVER_ERROR": "Erreur interne du serveur.",
"VAULT_ERROR": "Le coffre local n'est pas à jour. Veuillez synchroniser votre coffre en rafraîchissant la page et réessayez."
}
},
"content": {
"or": "or",
"new": "New",
"cancel": "Cancel",
"search": "Search",
"vaultLocked": "AliasVault is locked.",
"creatingNewAlias": "Creating new alias...",
"noMatchesFound": "No matches found",
"searchVault": "Search vault...",
"serviceName": "Service name",
"or": "ou",
"new": "Nouveautés",
"cancel": "Annuler",
"search": "Rechercher",
"vaultLocked": "AliasVault est verrouillé.",
"creatingNewAlias": "Création de nouveaux alias...",
"noMatchesFound": "Aucun résultat trouvé",
"searchVault": "Rechercher dans le coffre...",
"serviceName": "Nom du service",
"email": "Email",
"username": "Username",
"password": "Password",
"enterServiceName": "Enter service name",
"enterEmailAddress": "Enter email address",
"enterUsername": "Enter username",
"hideFor1Hour": "Hide for 1 hour (current site)",
"hidePermanently": "Hide permanently (current site)",
"createRandomAlias": "Create random alias",
"createUsernamePassword": "Create username/password",
"randomAlias": "Random alias",
"usernamePassword": "Username/password",
"createAndSaveAlias": "Create and save alias",
"createAndSaveCredential": "Create and save credential",
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
"randomIdentityDescriptionDropdown": "Random identity with random email",
"manualCredentialDescription": "Specify your own email address and username.",
"manualCredentialDescriptionDropdown": "Manual username and password",
"failedToCreateIdentity": "Failed to create identity. Please try again.",
"enterEmailAndOrUsername": "Enter email and/or username",
"autofillWithAliasVault": "Autofill with AliasVault",
"generateRandomPassword": "Generate random password (copy to clipboard)",
"generateNewPassword": "Generate new password",
"togglePasswordVisibility": "Toggle password visibility",
"passwordCopiedToClipboard": "Password copied to clipboard",
"enterEmailAndOrUsernameError": "Enter email and/or username",
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
"vaultUpgradeRequired": "Vault upgrade required.",
"dismissPopup": "Dismiss popup"
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"enterServiceName": "Entrez le nom du service",
"enterEmailAddress": "Entrer l'adresse email",
"enterUsername": "Entrez le nom d'utilisateur",
"hideFor1Hour": "Cacher pendant 1 heure (site actuel)",
"hidePermanently": "Masquer définitivement (site actuel)",
"createRandomAlias": "Créer un alias aléatoire",
"createUsernamePassword": "Créer un nom d'utilisateur/mot de passe",
"randomAlias": "Alias aléatoire",
"usernamePassword": "Nom dutilisateur / mot de passe",
"createAndSaveAlias": "Créer et enregistrer l'alias",
"createAndSaveCredential": "Créer et enregistrer les identifiants",
"randomIdentityDescription": "Générer une identité aléatoire avec une adresse email aléatoire accessible dans AliasVault.",
"randomIdentityDescriptionDropdown": "Identité aléatoire avec email aléatoire",
"manualCredentialDescription": "Spécifiez votre propre adresse email et nom d'utilisateur.",
"manualCredentialDescriptionDropdown": "Identifiant et mot de passe manuels",
"failedToCreateIdentity": "Échec de la création de l'identité. Veuillez réessayer.",
"enterEmailAndOrUsername": "Entrez l'adresse email et/ou le nom d'utilisateur",
"autofillWithAliasVault": "Remplissage automatique avec AliasVault",
"generateRandomPassword": "Générer un mot de passe aléatoire (copier dans le presse-papier)",
"generateNewPassword": "Générer un nouveau mot de passe",
"togglePasswordVisibility": "Afficher ou masquer le mot de passe",
"passwordCopiedToClipboard": "Mot de passe copié dans le presse-papiers",
"enterEmailAndOrUsernameError": "Entrez l'adresse email et/ou le nom d'utilisateur",
"openAliasVaultToUpgrade": "Ouvrez AliasVault pour améliorer",
"vaultUpgradeRequired": "Mise à niveau du coffre requise.",
"dismissPopup": "Fermer"
},
"credentials": {
"title": "Credentials",
"addCredential": "Add Credential",
"editCredential": "Edit Credential",
"deleteCredential": "Delete Credential",
"credentialDetails": "Credential Details",
"serviceName": "Service Name",
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
"website": "Website",
"title": "Identifiants",
"addCredential": "Ajouter des identifiants",
"editCredential": "Modifier les identifiants",
"deleteCredential": "Supprimer les identifiants",
"credentialDetails": "Informations sur les identifiants",
"serviceName": "Nom du service",
"serviceNamePlaceholder": "ex: Gmail, Facebook, Banque",
"website": "Site Internet",
"websitePlaceholder": "https://example.com",
"username": "Username",
"usernamePlaceholder": "Enter username",
"password": "Password",
"passwordPlaceholder": "Enter password",
"generatePassword": "Generate Password",
"copyPassword": "Copy Password",
"showPassword": "Show Password",
"hidePassword": "Hide Password",
"username": "Nom d'utilisateur",
"usernamePlaceholder": "Entrez le nom d'utilisateur",
"password": "Mot de passe",
"passwordPlaceholder": "Saisir le mot de passe",
"generatePassword": "Générer le mot de passe",
"copyPassword": "Copier le mot de passe",
"showPassword": "Afficher le mot de passe",
"hidePassword": "Masquer le mot de passe",
"notes": "Notes",
"notesPlaceholder": "Additional notes...",
"totp": "Two-Factor Authentication",
"totpCode": "TOTP Code",
"copyTotp": "Copy TOTP",
"totpSecret": "TOTP Secret",
"totpSecretPlaceholder": "Enter TOTP secret key",
"noCredentials": "No credentials found",
"noCredentialsDescription": "Add your first credential to get started",
"searchPlaceholder": "Search credentials...",
"welcomeTitle": "Welcome to AliasVault!",
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
"createdAt": "Created",
"updatedAt": "Last updated",
"autofill": "Autofill",
"fillForm": "Fill Form",
"deleteConfirm": "Are you sure you want to delete this credential?",
"saveSuccess": "Credential saved successfully",
"tags": "Tags",
"addTag": "Add Tag",
"removeTag": "Remove Tag",
"folder": "Folder",
"selectFolder": "Select Folder",
"createFolder": "Create Folder",
"saveCredential": "Save credential",
"deleteCredentialTitle": "Delete Credential",
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
"randomAlias": "Random Alias",
"manual": "Manual",
"notesPlaceholder": "Notes supplémentaires...",
"totp": "Authentification à deux facteurs",
"totpCode": "Mot de passe à usage unique",
"copyTotp": "Copier le mot de passe à usage unique",
"totpSecret": "Mot de passe à usage unique secret",
"totpSecretPlaceholder": "Entrez le mot de passe à usage unique",
"noCredentials": "Aucun identifiant trouvé",
"noCredentialsDescription": "Ajoutez vos premiers identifiants pour commencer",
"searchPlaceholder": "Rechercher des identifiants...",
"welcomeTitle": "Bienvenue dans AliasVault !",
"welcomeDescription": "Pour utiliser l'extension de navigateur AliasVault : accédez à un site web et utilisez la fenêtre de saisie automatique AliasVault pour créer un nouvel identifiant.",
"createdAt": "Créé",
"updatedAt": "Dernière mise à jour",
"autofill": "Remplissage automatique",
"fillForm": "Remplir le formulaire",
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cet identifiant ?",
"saveSuccess": "Identifiants enregistrés avec succès",
"tags": "Mots-clés",
"addTag": "Ajouter un mot-clé",
"removeTag": "Supprimer un mot-clé",
"folder": "Dossier",
"selectFolder": "Sélectionner un dossier",
"createFolder": "Nouveau dossier",
"saveCredential": "Enregistrer les identifiants",
"deleteCredentialTitle": "Supprimer les identifiants",
"deleteCredentialConfirm": "Êtes-vous sûr de vouloir supprimer ces identifiants ? Cette action est irréversible.",
"randomAlias": "Alias aléatoire",
"manual": "Manuel",
"service": "Service",
"serviceUrl": "Service URL",
"loginCredentials": "Login Credentials",
"generateRandomUsername": "Generate random username",
"generateRandomPassword": "Generate random password",
"changePasswordComplexity": "Change password complexity",
"passwordLength": "Password length",
"includeLowercase": "Include lowercase letters",
"includeUppercase": "Include uppercase letters",
"includeNumbers": "Include numbers",
"includeSpecialChars": "Include special characters",
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"serviceUrl": "URL de service",
"loginCredentials": "Identifiants de connexion",
"generateRandomUsername": "Générer un nom d'utilisateur aléatoire",
"generateRandomPassword": "Générer un mot de passe aléatoire",
"changePasswordComplexity": "Changer la complexité du mot de passe",
"passwordLength": "Longueur du mot de passe",
"includeLowercase": "Inclure les lettres minuscules",
"includeUppercase": "Inclure les lettres majuscules",
"includeNumbers": "Inclure des chiffres",
"includeSpecialChars": "Inclure des caractères spéciaux",
"avoidAmbiguousChars": "Éviter les caractères ambigus (o, 0, etc.)",
"generateNewPreview": "Générer un nouvel aperçu",
"generateRandomAlias": "Créer un alias aléatoire",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",
"nickName": "Nick Name",
"gender": "Gender",
"birthDate": "Birth Date",
"birthDatePlaceholder": "YYYY-MM-DD",
"metadata": "Metadata",
"firstName": "Prénom",
"lastName": "Nom",
"nickName": "Surnom",
"gender": "Genre",
"birthDate": "Date de naissance",
"birthDatePlaceholder": "AAAA-MM-JJ",
"metadata": "Métadonnées",
"validation": {
"required": "This field is required",
"serviceNameRequired": "Service name is required",
"invalidEmail": "Invalid email format",
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
"required": "Ce champ est obligatoire",
"serviceNameRequired": "Le nom du service est requis",
"invalidEmail": "Format de courriel non valide",
"invalidDateFormat": "La date doit être au format AAAA-MM-JJ"
},
"privateEmailTitle": "Private Email",
"privateEmailAliasVaultServer": "AliasVault server",
"privateEmailDescription": "E2E encrypted, fully private.",
"publicEmailTitle": "Public Temp Email Providers",
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
"useDomainChooser": "Use domain chooser",
"enterCustomDomain": "Enter custom domain",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix"
"privateEmailTitle": "Email privé",
"privateEmailAliasVaultServer": "Serveur AliasVault",
"privateEmailDescription": "Chiffrement bout en bout, entièrement privé.",
"publicEmailTitle": "Fournisseurs d'email public temporaires",
"publicEmailDescription": "Anonyme mais confidentialité limitée. Le contenu de l'email est lisible par toute personne qui connaît l'adresse.",
"useDomainChooser": "Utiliser le sélecteur de domaine",
"enterCustomDomain": "Entrez le domaine personnalisé",
"enterFullEmail": "Entrez l'adresse email complète",
"enterEmailPrefix": "Entrez le préfixe de l'email"
},
"emails": {
"title": "Emails",
"deleteEmailTitle": "Delete Email",
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
"from": "From",
"to": "To",
"deleteEmailTitle": "Supprimer l'email",
"deleteEmailConfirm": "Êtes-vous sûr de vouloir supprimer définitivement cet email ?",
"from": "De",
"to": "À",
"date": "Date",
"emailContent": "Email content",
"attachments": "Attachments",
"emailNotFound": "Email not found",
"noEmails": "No emails found",
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
"emailContent": "Contenu de l'email",
"attachments": "Pièces jointes",
"emailNotFound": "Email introuvable",
"noEmails": "Aucun email trouvé",
"noEmailsDescription": "Vous n'avez pas encore reçu d'emails dans vos adresses email privées. Quand vous recevez un nouvel email, il apparaîtra ici.",
"dateFormat": {
"justNow": "just now",
"minutesAgo_single": "{{count}} min ago",
"minutesAgo_plural": "{{count}} mins ago",
"hoursAgo_single": "{{count}} hr ago",
"hoursAgo_plural": "{{count}} hrs ago",
"yesterday": "yesterday"
"justNow": "maintenant",
"minutesAgo_single": "Il y a {{count}} minute",
"minutesAgo_plural": "Il y a {{count}} minutes",
"hoursAgo_single": "Il y a {{count}} heure",
"hoursAgo_plural": "Il y a {{count}} heures",
"yesterday": "hier"
},
"errors": {
"emailLoadError": "An error occurred while loading emails. Please try again later.",
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
"emailLoadError": "Une erreur s'est produite lors du chargement des emails. Veuillez réessayer plus tard.",
"emailUnexpectedError": "Une erreur inattendue s'est produite lors du chargement des emails. Veuillez réessayer plus tard."
},
"apiErrors": {
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
"CLAIM_DOES_NOT_EXIST": "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."
"CLAIM_DOES_NOT_MATCH_USER": "L'adresse email actuelle est déjà utilisée. Veuillez modifier l'adresse email en modifiant cet identifiant.",
"CLAIM_DOES_NOT_EXIST": "Une erreur s'est produite en essayant de charger les emails. Veuillez essayer de modifier et enregistrer les informations d'identification pour synchroniser la base de données, puis réessayez."
}
},
"settings": {
"title": "Settings",
"serverUrl": "Server URL",
"language": "Language",
"autofillEnabled": "Enable Autofill",
"title": "Réglages",
"serverUrl": "URL du serveur",
"language": "Langue",
"autofillEnabled": "Activer le remplissage automatique",
"version": "Version",
"openInNewWindow": "Open in new window",
"openWebApp": "Open web app",
"loggedIn": "Logged in",
"logout": "Logout",
"globalSettings": "Global Settings",
"autofillPopup": "Autofill popup",
"activeOnAllSites": "Active on all sites (unless disabled below)",
"disabledOnAllSites": "Disabled on all sites",
"enabled": "Enabled",
"disabled": "Disabled",
"rightClickContextMenu": "Right-click context menu",
"autofillMatching": "Autofill Matching",
"autofillMatchingMode": "Autofill matching mode",
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
"autofillMatchingDefault": "URL + subdomain + name wildcard",
"autofillMatchingUrlSubdomain": "URL + subdomain",
"autofillMatchingUrlExact": "Exact URL domain only",
"siteSpecificSettings": "Site-Specific Settings",
"autofillPopupOn": "Autofill popup on: ",
"enabledForThisSite": "Enabled for this site",
"disabledForThisSite": "Disabled for this site",
"temporarilyDisabledUntil": "Temporarily disabled until ",
"resetAllSiteSettings": "Reset all site-specific settings",
"appearance": "Appearance",
"theme": "Theme",
"useDefault": "Use default",
"light": "Light",
"dark": "Dark",
"keyboardShortcuts": "Keyboard Shortcuts",
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
"configure": "Configure",
"security": "Security",
"clipboardClearTimeout": "Clear clipboard after copying",
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
"clipboardClearDisabled": "Never clear",
"clipboardClear5Seconds": "Clear after 5 seconds",
"clipboardClear10Seconds": "Clear after 10 seconds",
"clipboardClear15Seconds": "Clear after 15 seconds",
"autoLockTimeout": "Auto-lock Timeout",
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
"autoLockNever": "Never",
"autoLock15Seconds": "15 seconds",
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
"openWebApp": "Ouvrir lapplication web",
"loggedIn": "Connecté(e)",
"logout": "Se déconnecter",
"globalSettings": "Paramètres généraux",
"autofillPopup": "Remplissage automatique de la popup",
"activeOnAllSites": "Activé sur tous les sites (sauf si désactivé ci-dessous)",
"disabledOnAllSites": "Désactivé sur tous les sites",
"enabled": "Activé",
"disabled": "Désactivé",
"rightClickContextMenu": "Clic-droit sur le menu contextuel",
"autofillMatching": "Correspondance de remplissage automatique",
"autofillMatchingMode": "Remplir automatiquement le mode correspondant",
"autofillMatchingModeDescription": "Détermine quels identifiants sont considérés comme une correspondance et sont affichés comme des suggestions dans la fenêtre de saisie automatique pour un site web donné.",
"autofillMatchingDefault": "URL + sous-domaine + nom générique",
"autofillMatchingUrlSubdomain": "URL + sous-domaine",
"autofillMatchingUrlExact": "Domaine d'URL exact uniquement",
"siteSpecificSettings": "Paramètres spécifiques au site",
"autofillPopupOn": "Popup de saisie automatique sur: ",
"enabledForThisSite": "Activé pour ce site",
"disabledForThisSite": "Désactivé pour ce site",
"temporarilyDisabledUntil": "Temporairement désactivé jusqu'au ",
"resetAllSiteSettings": "Réinitialiser tous les paramètres spécifiques au site",
"appearance": "Apparence",
"theme": "Thème",
"useDefault": "Utiliser par défaut",
"light": "Clair",
"dark": "Sombre",
"keyboardShortcuts": "Raccourcis clavier",
"configureKeyboardShortcuts": "Configurer les raccourcis clavier",
"configure": "Configurer",
"security": "Sécurité",
"clipboardClearTimeout": "Effacer le presse-papiers après copie",
"clipboardClearTimeoutDescription": "Effacer automatiquement le presse-papiers après copie des données sensibles",
"clipboardClearDisabled": "Ne jamais effacer",
"clipboardClear5Seconds": "Effacer après 5 secondes",
"clipboardClear10Seconds": "Effacer après 10 secondes",
"clipboardClear15Seconds": "Effacer après 15 secondes",
"autoLockTimeout": "Délai de verrouillage automatique",
"autoLockTimeoutDescription": "Verrouiller automatiquement le coffre après une période d'inactivité",
"autoLockTimeoutHelp": "Le coffre ne se verrouille qu'après la période d'inactivité spécifiée (aucune fenêtre pop-up de saisie automatique ou d'extension). Le coffre sera toujours verrouillé lorsque le navigateur sera fermé, quel que soit ce paramètre.",
"autoLockNever": "Jamais",
"autoLock15Seconds": "15 secondes",
"autoLock1Minute": "1 minute",
"autoLock5Minutes": "5 minutes",
"autoLock15Minutes": "15 minutes",
"autoLock30Minutes": "30 minutes",
"autoLock1Hour": "1 hour",
"autoLock4Hours": "4 hours",
"autoLock8Hours": "8 hours",
"autoLock24Hours": "24 hours",
"autoLock1Hour": "1 heure",
"autoLock4Hours": "4 heures",
"autoLock8Hours": "8 heures",
"autoLock24Hours": "24 heures",
"versionPrefix": "Version ",
"preferences": "Preferences",
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"preferences": "Préférences",
"autofillSettings": "Paramètres du remplissage automatique",
"clipboardSettings": "Paramètres du presse-papiers",
"contextMenuSettings": "Paramètres du menu contextuel",
"contextMenu": "Menu contextuel",
"contextMenuEnabled": "Le menu contextuel est activé",
"contextMenuDisabled": "Le menu contextuel est désactivé",
"contextMenuDescription": "Faites un clic droit sur les champs de saisie pour accéder aux options d'AliasVault",
"selectLanguage": "Sélectionner une langue",
"validation": {
"apiUrlRequired": "API URL is required",
"apiUrlInvalid": "Please enter a valid API URL",
"clientUrlRequired": "Client URL is required",
"clientUrlInvalid": "Please enter a valid client URL"
"apiUrlRequired": "L'URL de l'API est requise",
"apiUrlInvalid": "Veuillez entrer une URL d'API valide",
"clientUrlRequired": "L'URL du client est requise",
"clientUrlInvalid": "Veuillez entrer une URL de client valide"
}
},
"upgrade": {
"title": "Upgrade Vault",
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
"versionInformation": "Version Information",
"yourVault": "Your vault:",
"newVersion": "New version:",
"upgrade": "Upgrade Vault",
"upgrading": "Upgrading...",
"logout": "Logout",
"whatsNew": "What's New",
"whatsNewDescription": "An upgrade is required to support the following changes:",
"noDescriptionAvailable": "No description available for this version.",
"title": "Mettre à niveau le coffre",
"subtitle": "AliasVault a mis à jour et votre coffre doit être mis à niveau. Cela ne devrait prendre que quelques secondes.",
"versionInformation": "Informations de version",
"yourVault": "Votre coffre :",
"newVersion": "Nouvelle version :",
"upgrade": "Mettre le coffre à niveau",
"upgrading": "Mise à niveau...",
"logout": "Se déconnecter",
"whatsNew": "Nouveautés",
"whatsNewDescription": "Une mise à niveau est nécessaire pour prendre en charge les modifications suivantes :",
"noDescriptionAvailable": "Aucune description disponible pour cette version.",
"okay": "Ok",
"status": {
"preparingUpgrade": "Preparing upgrade...",
"vaultAlreadyUpToDate": "Vault is already up to date",
"startingDatabaseTransaction": "Starting database transaction...",
"applyingDatabaseMigrations": "Applying database migrations...",
"applyingMigration": "Applying migration {{current}} of {{total}}...",
"committingChanges": "Committing changes..."
"preparingUpgrade": "Préparation de la mise à niveau...",
"vaultAlreadyUpToDate": "Le coffre est déjà à jour",
"startingDatabaseTransaction": "Démarrage de la transaction de la base de données...",
"applyingDatabaseMigrations": "Application des migrations de base de données...",
"applyingMigration": "Application de la migration {{current}} sur {{total}}...",
"committingChanges": "Validation des modifications..."
},
"alerts": {
"error": "Error",
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
"selfHostedServer": "Self-Hosted Server",
"selfHostedWarning": "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.",
"cancel": "Cancel",
"continueUpgrade": "Continue Upgrade",
"upgradeFailed": "Upgrade Failed",
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
"error": "Erreur",
"unableToGetVersionInfo": "Impossible d'obtenir les informations de version. Veuillez réessayer.",
"selfHostedServer": "Serveur auto-hébergé",
"selfHostedWarning": "Si vous utilisez un serveur auto-hébergé, assurez-vous également de mettre à jour votre instance auto-hébergée, sinon la connexion au client web cessera de fonctionner.",
"cancel": "Annuler",
"continueUpgrade": "Continuer la mise à jour",
"upgradeFailed": "Échec de la mise à niveau",
"failedToApplyMigration": "Impossible d'appliquer la migration ({{current}} sur {{total}})",
"unknownErrorDuringUpgrade": "Une erreur inconnue s'est produite pendant la mise à niveau. Veuillez réessayer."
}
}
}

View File

@@ -0,0 +1,393 @@
{
"auth": {
"loginTitle": "כניסה ל־AliasVault",
"username": "שם משתמש או דוא״ל",
"usernamePlaceholder": "שם / name@company.com",
"password": "סיסמה",
"passwordPlaceholder": "נא למלא את הסיסמה שלך",
"rememberMe": "לזכור אותי",
"loginButton": "כניסה",
"noAccount": "אין לך חשבון עדיין?",
"createVault": "יצירת כספת חדשה",
"twoFactorTitle": "נא למלא את קוד האימות מיישומון המאמת שלך.",
"authCode": "קוד אימות",
"authCodePlaceholder": "נא למלא קוד באורך 6 ספרות",
"verify": "אימות",
"cancel": "ביטול",
"twoFactorNote": "לתשומת ליבך: אם אין לך גישה להתקן המאמת (authenticator) שלך, אפשר לאפס אימות דו־שלבי עם קוד שחזור על ידי כניסה דרך האתר.",
"masterPassword": "סיסמת על",
"unlockVault": "שחרור נעילת כספת",
"unlockTitle": "שחרור נעילת הכספת שלך",
"unlockDescription": "נא למלא את סיסמת העל שלך כדי לשחרר את הכספת שלך.",
"logout": "יציאה",
"logoutConfirm": "לצאת?",
"sessionExpired": "תוקף ההפעלה שלך פג. נא להיכנס מחדש.",
"unlockSuccess": "נעילת הכספת שוחררה בהצלחה!",
"unlockSuccessTitle": "נעילת הכספת שלך נפתחה בהצלחה",
"unlockSuccessDescription": "מעתה ניתן להשתמש בהשלמה אוטומטית בטופסי כניסה בדפדפן שלך.",
"closePopup": "סגירת החלונית הצצה הזאת",
"browseVault": "עיון בתוכן הכספת",
"connectingTo": "מתבצעת התחברות אל",
"switchAccounts": "להחליף חשבונות?",
"loggedIn": "נכנסת",
"errors": {
"invalidCode": "נא למלא קוד אימות באורך 6 ספרות.",
"serverError": "לא ניתן ליצור קשר עם השרת של AliasVault. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
"noToken": "הכניסה נכשלה - לא הוחזר אסימון",
"migrationError": "אירעה שגיאה בעת בדיקה לאיתור הסבות ממתינות.",
"wrongPassword": "סיסמה שגויה. נא לנסות שוב.",
"accountLocked": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים.",
"networkError": "שגיאת רשת. נא לבדוק את החיבור ולנסות שוב.",
"loginDataMissing": "תוקף ההפעלה שלך פג. נא לנסות שוב."
}
},
"menu": {
"credentials": "פרטי גישה",
"emails": "הודעות דוא״ל",
"settings": "הגדרות"
},
"common": {
"appName": "AliasVault",
"loading": "בטעינה…",
"error": "שגיאה",
"success": "הצליח",
"cancel": "ביטול",
"use": "להשתמש",
"delete": "מחיקה",
"close": "סגירה",
"copied": "הועתק!",
"openInNewWindow": "פתיחה בחלון חדש",
"language": "שפה",
"enabled": "פעיל",
"disabled": "כבוי",
"showPassword": "הצגת סיסמה",
"hidePassword": "הסתרת סיסמה",
"copyToClipboard": "העתקה ללוח הגזירים",
"loadingEmails": "הודעות הדוא״ל נטענות…",
"loadingTotpCodes": "הקודים החד־פעמיים הזמניים נטענים…",
"attachments": "צרופות",
"loadingAttachments": "הצרופות נטענות…",
"settings": "הגדרות",
"recentEmails": "הודעות דוא״ל אחרונות",
"loginCredentials": "פרטי הגישה",
"twoFactorAuthentication": "אימות דו־שלבי",
"alias": "כינוי",
"notes": "הערות",
"fullName": "שם מלא",
"firstName": "שם פרטי",
"lastName": "שם משפחה",
"birthDate": "תאריך לידה",
"nickname": "כינוי",
"email": "דוא״ל",
"username": "שם משתמש",
"password": "סיסמה",
"syncingVault": "הכספת מסתנכרת",
"savingChangesToVault": "השינוים לכספת נשמרים",
"uploadingVaultToServer": "הכספת נשלחת לשרת",
"checkingVaultUpdates": "מתבצעת בדיקה לשינויים בכספת",
"syncingUpdatedVault": "הכספת העדכנית מסתנכרת",
"executingOperation": "הפעולה רצה…",
"loadMore": "לטעון עוד",
"errors": {
"VaultOutdated": "הכספת שלך לא עדכנית. נא להיכנס לאתר AliasVault ולעקוב אחר ההנחיות.",
"serverNotAvailable": "שרת ה־AliasVault לא זמין. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
"clientVersionNotSupported": "הגרסה הזאת של הרחבת הדפדפן של AliasVault לא נתמכת עוד על ידי השרת. נא לעדכן את הרחבת הדפדפן שלך לגרסה העדכנית ביותר.",
"serverVersionNotSupported": "יש לעדכן את שרת AliasVault לגרסה חדשה יותר כדי להשתמש בהרחבת הדפדפן הזאת. נא ליצור קשר עם התמיכה לקבלת עזרה.",
"unknownError": "אירעה שגיאה לא ידועה",
"failedToStoreVault": "אחסון הכספת נכשל",
"vaultNotAvailable": "הכספת לא זמינה",
"failedToRetrieveData": "משיכת הנתונים נכשלה",
"vaultIsLocked": "הכספת נעולה",
"failedToUploadVault": "העלאת הכספת נכשלה",
"passwordChanged": "הסיסמה שלך השתנתה מאז הפעם האחרונה שנכנסת למערכת. נא להיכנס שוב מטעמי אבטחת מידע."
},
"apiErrors": {
"UNKNOWN_ERROR": "אירעה שגיאה לא ידועה, נא לנסות שוב.",
"ACCOUNT_LOCKED": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים. נא לנסות שוב מאוחר יותר.",
"ACCOUNT_BLOCKED": "החשבון שלך הושבת. אם לדעתך מדובר בטעות, נא ליצור קשר עם התמיכה.",
"USER_NOT_FOUND": "שם המשתמש או הסיסמה שגויים. נא לנסות שוב.",
"INVALID_AUTHENTICATOR_CODE": "קוד מאמת שגוי. נא לנסות שוב.",
"INVALID_RECOVERY_CODE": "קוד שחזור שגוי. נא לנסות שוב.",
"REFRESH_TOKEN_REQUIRED": "אסימון ריענון חובה.",
"INVALID_REFRESH_TOKEN": "אסימון ריענון שגוי.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "אסימון הריענון נשלל בהצלחה.",
"PUBLIC_REGISTRATION_DISABLED": "רישום חשבון חדש מושבת כרגע בשרת הזה. נא ליצור קשר עם ההנהלה.",
"USERNAME_REQUIRED": "שם משתמש חובה.",
"USERNAME_ALREADY_IN_USE": "שם המשתמש כבר תפוס.",
"USERNAME_AVAILABLE": "שם המשתמש פנוי.",
"USERNAME_MISMATCH": "שם המשתמש לא מתאים למשתמש הנוכחי.",
"PASSWORD_MISMATCH": "הסיסמה שסופקה לא תואמת לסיסמה הנוכחית שלך.",
"ACCOUNT_SUCCESSFULLY_DELETED": "החשבון נמחק בהצלחה.",
"USERNAME_EMPTY_OR_WHITESPACE": "שם המשתמש לא יכול להיות ריק או מורכב מרווחים בלבד.",
"USERNAME_TOO_SHORT": "שם המשתמש קצר מדי: חייב להיות באורך 3 תווים לפחות.",
"USERNAME_TOO_LONG": "שם המשתמש ארוך מדי: לא יכול להיות ארוך מ־40 תווים.",
"USERNAME_INVALID_EMAIL": "כתובת דוא״ל שגויה.",
"USERNAME_INVALID_CHARACTERS": "שם המשתמש שגוי, יכול להכיל רק תווים או ספרות.",
"VAULT_NOT_UP_TO_DATE": "הכספת שלך אינה עדכנית. נא לסנכרן את הכספת שלך ולנסות שוב.",
"INTERNAL_SERVER_ERROR": "שגיאת שרת פנימית.",
"VAULT_ERROR": "הכספת המקומית אינה עדכנית. נא לסנכרן את הכספת שלך על ידי ריענון העמוד ולנסות שוב."
}
},
"content": {
"or": "או",
"new": "חדש",
"cancel": "ביטול",
"search": "חיפוש",
"vaultLocked": "AliasVault נעול.",
"creatingNewAlias": "נוצר כינוי חדש...",
"noMatchesFound": "לא נמצאו תוצאות",
"searchVault": "חיפוש בכספת…",
"serviceName": "שם השירות",
"email": "דוא״ל",
"username": "שם משתמש",
"password": "סיסמה",
"enterServiceName": "נא למלא את שם השירות",
"enterEmailAddress": "נא למלא כתובת דוא״ל",
"enterUsername": "נא למלא שם משתמש",
"hideFor1Hour": "הסתרה למשך שעה (האתר הנוכחי)",
"hidePermanently": "הסתרה לצמיתות (האתר הנוכחי)",
"createRandomAlias": "יצירת כינוי אקראי",
"createUsernamePassword": "יצירת שם משתמש/סיסמה",
"randomAlias": "כינוי אקראי",
"usernamePassword": "שם משתמש/סיסמה",
"createAndSaveAlias": "יצירה ושמירה של כינוי",
"createAndSaveCredential": "יצירה ושמירה של פרטי גישה",
"randomIdentityDescription": "יצירת זהות אקראית עם כתובת דוא״ל אקראית שנגישה דרך AliasVault.",
"randomIdentityDescriptionDropdown": "זהות אקראיות עם דוא״ל אקראי",
"manualCredentialDescription": "נא לציין כתובת דוא״ל ושם משתמש משלך.",
"manualCredentialDescriptionDropdown": "שם משתמש וסיסמה ידניים",
"failedToCreateIdentity": "יצירת הזהות נכשלה. נא לנסות שוב.",
"enterEmailAndOrUsername": "נא למלא דוא״ל ו/או שם משתמש",
"autofillWithAliasVault": "השלמה אוטומטית עם AliasVault",
"generateRandomPassword": "יצירת סיסמה אקראית (העתקה ללוח הגזירים)",
"generateNewPassword": "יצירת סיסמה חדשה",
"togglePasswordVisibility": "הצגת/הסתרת סיסמה",
"passwordCopiedToClipboard": "הסיסמה הועתקה ללוח הגזירים",
"enterEmailAndOrUsernameError": "נא למלא דוא״ל ו/או שם משתמש",
"openAliasVaultToUpgrade": "יש לפתוח את AliasVault כדי לשדרג",
"vaultUpgradeRequired": "יש לשדרג את הכספת.",
"dismissPopup": "התעלמות מחלונית"
},
"credentials": {
"title": "פרטי גישה",
"addCredential": "הוספת פרטי גישה",
"editCredential": "עריכת פרטי גישה",
"deleteCredential": "מחיקת פרטי גישה",
"credentialDetails": "פירוט פרטי גישה",
"serviceName": "שם השירות",
"serviceNamePlaceholder": "למשל: ג׳ימייל, פייסבוק, בנק",
"website": "אתר",
"websitePlaceholder": "https://example.com",
"username": "שם משתמש",
"usernamePlaceholder": "נא למלא שם משתמש",
"password": "סיסמה",
"passwordPlaceholder": "נא למלא סיסמה",
"generatePassword": "יצירת סיסמה",
"copyPassword": "העתקת סיסמה",
"showPassword": "הצגת סיסמה",
"hidePassword": "הסתרת סיסמה",
"notes": "הערות",
"notesPlaceholder": "הערות נוספות…",
"totp": "אימות דו־שלבי",
"totpCode": "קוד חד־פעמי זמני",
"copyTotp": "העתקת קוד חד־פעמי זמני",
"totpSecret": "סוג סיסמה חד־פעמית זמנית",
"totpSecretPlaceholder": "נא למלא מפתח סודי לסיסמה חד־פעמית זמנית",
"noCredentials": "לא נמצאו פרטי גישה",
"noCredentialsDescription": "יש להוסיף את פרטי הגישה הראשונים שלך כדי להתחיל",
"searchPlaceholder": "חיפוש פרטי גישה…",
"welcomeTitle": "ברוך בואך ל־AliasVault!",
"welcomeDescription": "כדי להשתמש בהרחבת הדפדפן של AliasVault: יש לנווט לאתר ולהשתמש בחלונית ההשלמה האוטומטית של AliasVault כדי ליצור פרטי גישה חדשים.",
"createdAt": "יצירה",
"updatedAt": "עדכון אחרון",
"autofill": "השלמה אוטומטית",
"fillForm": "מילוי טופס",
"deleteConfirm": "למחוק את פרטי הגישה האלה?",
"saveSuccess": "פרטי הגישה נשמרו בהצלחה",
"tags": "תגיות",
"addTag": "הוספת תגית",
"removeTag": "הסרת תגית",
"folder": "תיקייה",
"selectFolder": "בחירת תיקייה",
"createFolder": "יצירת תיקייה",
"saveCredential": "שמירת פרטי גישה",
"deleteCredentialTitle": "מחיקת פרטי גישה",
"deleteCredentialConfirm": "למחוק את פרטי הגישה? זאת פעולה בלתי הפיכה.",
"randomAlias": "כינוי אקראי",
"manual": "ידני",
"service": "שירות",
"serviceUrl": "כתובת השירות",
"loginCredentials": "פרטי הגישה",
"generateRandomUsername": "יצירת שם משתמש אקראי",
"generateRandomPassword": "יצירת סיסמה אקראית",
"changePasswordComplexity": "החלפת מורכבת הסיסמה",
"passwordLength": "אורך הסיסמה",
"includeLowercase": "לכלול אותיות קטנות",
"includeUppercase": "לכלול אותיות גדולות",
"includeNumbers": "לכלול מספרים",
"includeSpecialChars": "לכלול תווים מיוחדים",
"avoidAmbiguousChars": "עדיף להימנע מאותיות וספרות שדומים זה לזה (o, 0 וכו׳)",
"generateNewPreview": "יצירת תצוגה מקדימה חדשה",
"generateRandomAlias": "יצירת כינוי אקראי",
"clearAliasFields": "Clear Alias Fields",
"alias": "כינוי",
"firstName": "שם פרטי",
"lastName": "שם משפחה",
"nickName": "כינוי",
"gender": "מגדר",
"birthDate": "תאריך לידה",
"birthDatePlaceholder": "YYYY-MM-DD",
"metadata": "נתוני על",
"validation": {
"required": "שדה חובה",
"serviceNameRequired": "שם השירות חובה",
"invalidEmail": "תבנית דוא״ל שגויה",
"invalidDateFormat": "התאריך חייב להיות בתבנית YYYY-MM-DD"
},
"privateEmailTitle": "כתובת דוא״ל פרטית",
"privateEmailAliasVaultServer": "שרת AliasVault",
"privateEmailDescription": "הצפנה מקצה לקצה, פרטיות מלאה.",
"publicEmailTitle": "ספקי תיבת דוא״ל זמנית ציבוריים",
"publicEmailDescription": "פרטיות אלמונית אך מוגבלת. תוכן הדוא״ל נגיש לכל מי שיודע את הכתובת.",
"useDomainChooser": "להשתמש בבורר שמות התחום",
"enterCustomDomain": "נא למלא שם תחום מותאם אישית",
"enterFullEmail": "נא למלא כתובת דוא״ל מלאה",
"enterEmailPrefix": "נא למלא קידומת דוא״ל"
},
"emails": {
"title": "הודעות דוא״ל",
"deleteEmailTitle": "מחיקת הודעת דוא״ל",
"deleteEmailConfirm": "למחוק את הודעת הדוא״ל הזאת לצמיתות?",
"from": "מאת",
"to": "אל",
"date": "תאריך",
"emailContent": "תוכן הודעת דוא״ל",
"attachments": "צרופות",
"emailNotFound": "הודעת הדוא״ל לא נמצאה",
"noEmails": "לא נמצאו הודעות דוא״ל",
"noEmailsDescription": "לא קיבלת הודעות דוא״ל כלשהן לכתובות הדוא״ל הפרטיות שלך עדיין. כשמגיעה הודעה חדשה היא תופיע כאן.",
"dateFormat": {
"justNow": "ממש הרגע",
"minutesAgo_single": "לפני דקה",
"minutesAgo_plural": "לפני {{count}} דקות",
"hoursAgo_single": "לפני שעה",
"hoursAgo_plural": "לפני {{count}} שעות",
"yesterday": "אתמול"
},
"errors": {
"emailLoadError": "אירעה שגיאה בטעינת הודעות הדוא״ל. נא לנסות שוב מאוחר יותר.",
"emailUnexpectedError": "אירעה שגיאה לא צפויה בטעינת הודעות הדוא״ל. נא לנסות שוב מאוחר יותר."
},
"apiErrors": {
"CLAIM_DOES_NOT_MATCH_USER": "כתובת הדוא״ל שנבחרה תפוסה. נא לשנות את כתובת הדוא״ל על ידי עריכת פרטי הגישה.",
"CLAIM_DOES_NOT_EXIST": "אירעה שגיאה בניסיון לטעון את הודעות הדוא״ל. נא לנסות לערוך ולשמור את רשומת פרטי הקשר כדי לסנכרן את מסד הנתונים ואז לנסות שוב."
}
},
"settings": {
"title": "הגדרות",
"serverUrl": "כתובת שרת",
"language": "שפה",
"autofillEnabled": "הפעלת השלמה אוטומטית",
"version": "גרסה",
"openInNewWindow": "פתיחה בחלון חדש",
"openWebApp": "פתיחת אתר",
"loggedIn": "נכנסת",
"logout": "יציאה",
"globalSettings": "הגדרות מקיפות",
"autofillPopup": "חלונית השלמה אוטומטית",
"activeOnAllSites": "פעיל בכל האתרים (למעט אם נכבה להלן)",
"disabledOnAllSites": "כבוי בכל האתרים",
"enabled": "פעיל",
"disabled": "כבוי",
"rightClickContextMenu": "תפריט הקשר בלחיצה ימנית",
"autofillMatching": "התאמת השלמה אוטומטית",
"autofillMatchingMode": "מצב התאמת השלמה אוטומטית",
"autofillMatchingModeDescription": "הגדרה אילו פרטי גישה נחשבים תואמים ויופיעו כהצעות בחלונית ההשלמה האוטומטית לאתר מסוים.",
"autofillMatchingDefault": "כתובת + שם תת־תחום + תו כל על שם",
"autofillMatchingUrlSubdomain": "כתובת + שם תת־תחום",
"autofillMatchingUrlExact": "תחום כתובת מדויקת בלבד",
"siteSpecificSettings": "הגדרות תואמות אתר",
"autofillPopupOn": "חלונית השלמה אוטומטית ב־: ",
"enabledForThisSite": "פעיל לאתר הזה",
"disabledForThisSite": "כבוי לאתר הזה",
"temporarilyDisabledUntil": "כבוי זמנית עד ",
"resetAllSiteSettings": "איפוס כל ההגדרות הנקודתיות לאתרים",
"appearance": "מראה",
"theme": "ערכת עיצוב",
"useDefault": "להשתמש בברירת המחדל",
"light": "בהירה",
"dark": "כהה",
"keyboardShortcuts": "קיצורי מקלדת",
"configureKeyboardShortcuts": "הגדרת קיצורי מקלדת",
"configure": "הגדרה",
"security": "אבטחה",
"clipboardClearTimeout": "לפנות את לוח הגזירים לאחר העתקה",
"clipboardClearTimeoutDescription": "לפנות את לוח הגזירים אוטומטית לאחר העתקת נתונים רגישים",
"clipboardClearDisabled": "אף פעם לא לפנות",
"clipboardClear5Seconds": "לפנות אחרי 5 שניות",
"clipboardClear10Seconds": "לפנות אחרי 10 שניות",
"clipboardClear15Seconds": "לפנות אחרי 15 שניות",
"autoLockTimeout": "תום המתנה לנעילה אוטומטית",
"autoLockTimeoutDescription": "לנעול את הכספת אוטומטית לאחר פרק זמן של חוסר פעילות",
"autoLockTimeoutHelp": "הכספת תינעל רק לאחר משך זמן של חוסר פעילות (אין שימוש בהשלמה אוטומטית או פתיחת חלונית הרחבה). הכספת תמיד תינעל עם סגירת הדפדפן, ללא תלות בהגדרה הזאת.",
"autoLockNever": "אף פעם",
"autoLock15Seconds": "15 שניות",
"autoLock1Minute": "דקה",
"autoLock5Minutes": "5 דקות",
"autoLock15Minutes": "15 דקות",
"autoLock30Minutes": "30 דקות",
"autoLock1Hour": "שעה",
"autoLock4Hours": "4 שעות",
"autoLock8Hours": "8 שעות",
"autoLock24Hours": "24 שעות",
"versionPrefix": "גרסה ",
"preferences": "העדפות",
"autofillSettings": "הגדרות השלמה אוטומטית",
"clipboardSettings": "הגדרות לוח הגזירים",
"contextMenuSettings": "הגדרות תפריט הקשר",
"contextMenu": "תפריט הקשר",
"contextMenuEnabled": "תפריט הקשר פעיל",
"contextMenuDisabled": "תפריט הקשר כבוי",
"contextMenuDescription": "ניתן ללחוץ על שדה עם הלחצן הימני כדי לגשת לאפשרויות AliasVault",
"selectLanguage": "בחירת שפה",
"validation": {
"apiUrlRequired": "כתובת API חובה",
"apiUrlInvalid": "נא למלא כתובת API תקפה",
"clientUrlRequired": "כתובת לקוח חובה",
"clientUrlInvalid": "נא למלא כתובת לקוח תקפה"
}
},
"upgrade": {
"title": "שדרוג כספת",
"subtitle": "AliasVault התעדכן וצריך לשדרג את הכספת שלך. הפעולה הזאת אמורה לארוך מספר שניות.",
"versionInformation": "פרטי גרסה",
"yourVault": "הכספת שלך:",
"newVersion": "גרסה חדשה:",
"upgrade": "שדרוג כספת",
"upgrading": "משתדרגת…",
"logout": "יציאה",
"whatsNew": "מה חדש",
"whatsNewDescription": "יש לשדרג כדי שתהיה תמיכה בשינויים הבאים:",
"noDescriptionAvailable": "אין תיאור זמין לגרסה הזאת.",
"okay": "אישור",
"status": {
"preparingUpgrade": "השדרוג בהכנה…",
"vaultAlreadyUpToDate": "הכספת כבר עדכנית",
"startingDatabaseTransaction": "הסבת מסד הנתונים מתחילה…",
"applyingDatabaseMigrations": "השינויים חלים על מסד הנתונים…",
"applyingMigration": "חלה ההסבה {{current}} מתוך {{total}}…",
"committingChanges": "השינויים מקובעים…"
},
"alerts": {
"error": "שגיאה",
"unableToGetVersionInfo": "לא ניתן לקבל את פרטי הגרסה. נא לנסות שוב מאוחר יותר.",
"selfHostedServer": "שרת באירוח עצמי",
"selfHostedWarning": "אם מדובר בשרת שמתארח עצמאית, נא לוודא שהעותק שמתארח אצלך גם כן מתעדכן כי אחרת הכניסה לאתר תפסיק לעבוד.",
"cancel": "ביטול",
"continueUpgrade": "להמשיך בשדרוג",
"upgradeFailed": "השדרוג נכשל",
"failedToApplyMigration": "החלת ההסבה נכשלה ({{current}} מתוך {{total}})",
"unknownErrorDuringUpgrade": "אירעה שגיאה בלתי ידועה במהלך השדרוג. נא לנסות שוב."
}
}
}

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Evita caratteri ambigui (o, 0, ecc.)",
"generateNewPreview": "Genera nuova anteprima",
"generateRandomAlias": "Genera alias casuale",
"clearAliasFields": "Cancella Campi Alias",
"alias": "Alias",
"firstName": "Nome",
"lastName": "Cognome",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Onduidelijke tekens vermijden (o, 0, etc.)",
"generateNewPreview": "Genereer nieuw voorbeeld",
"generateRandomAlias": "Alias genereren",
"clearAliasFields": "Leeg alias velden",
"alias": "Alias",
"firstName": "Voornaam",
"lastName": "Achternaam",

View File

@@ -0,0 +1,393 @@
{
"auth": {
"loginTitle": "Log in to AliasVault",
"username": "Username or email",
"usernamePlaceholder": "name / name@company.com",
"password": "Password",
"passwordPlaceholder": "Enter your password",
"rememberMe": "Remember me",
"loginButton": "Login",
"noAccount": "No account yet?",
"createVault": "Create new vault",
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
"authCode": "Authentication Code",
"authCodePlaceholder": "Enter 6-digit code",
"verify": "Verify",
"cancel": "Cancel",
"twoFactorNote": "Note: if you don't have access to your authenticator device, you can reset your 2FA with a recovery code by logging in via the website.",
"masterPassword": "Master Password",
"unlockVault": "Unlock Vault",
"unlockTitle": "Unlock Your Vault",
"unlockDescription": "Enter your master password to unlock your vault.",
"logout": "Logout",
"logoutConfirm": "Are you sure you want to logout?",
"sessionExpired": "Your session has expired. Please log in again.",
"unlockSuccess": "Vault unlocked successfully!",
"unlockSuccessTitle": "Your vault is successfully unlocked",
"unlockSuccessDescription": "You can now use autofill in login forms in your browser.",
"closePopup": "Close this popup",
"browseVault": "Browse vault contents",
"connectingTo": "Connecting to",
"switchAccounts": "Switch accounts?",
"loggedIn": "Logged in",
"errors": {
"invalidCode": "Please enter a valid 6-digit authentication code.",
"serverError": "Could not reach AliasVault server. Please try again later or contact support if the problem persists.",
"noToken": "Login failed -- no token returned",
"migrationError": "An error occurred while checking for pending migrations.",
"wrongPassword": "Incorrect password. Please try again.",
"accountLocked": "Account temporarily locked due to too many failed attempts.",
"networkError": "Network error. Please check your connection and try again.",
"loginDataMissing": "Login session expired. Please try again."
}
},
"menu": {
"credentials": "Credentials",
"emails": "Emails",
"settings": "Settings"
},
"common": {
"appName": "AliasVault",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"cancel": "Cancel",
"use": "Use",
"delete": "Delete",
"close": "Close",
"copied": "Copied!",
"openInNewWindow": "Open in new window",
"language": "Language",
"enabled": "Enabled",
"disabled": "Disabled",
"showPassword": "Show password",
"hidePassword": "Hide password",
"copyToClipboard": "Copy to clipboard",
"loadingEmails": "Loading emails...",
"loadingTotpCodes": "Loading TOTP codes...",
"attachments": "Attachments",
"loadingAttachments": "Loading attachments...",
"settings": "Settings",
"recentEmails": "Recent emails",
"loginCredentials": "Login credentials",
"twoFactorAuthentication": "Two-factor authentication",
"alias": "Alias",
"notes": "Notes",
"fullName": "Full Name",
"firstName": "First Name",
"lastName": "Last Name",
"birthDate": "Birth Date",
"nickname": "Nickname",
"email": "Email",
"username": "Username",
"password": "Password",
"syncingVault": "Syncing vault",
"savingChangesToVault": "Saving changes to vault",
"uploadingVaultToServer": "Uploading vault to server",
"checkingVaultUpdates": "Checking for vault updates",
"syncingUpdatedVault": "Syncing updated vault",
"executingOperation": "Executing operation...",
"loadMore": "Load more",
"errors": {
"VaultOutdated": "Your vault is outdated. Please login on the AliasVault website and follow the steps.",
"serverNotAvailable": "The AliasVault server is not available. Please try again later or contact support if the problem persists.",
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
"serverVersionNotSupported": "The AliasVault server needs to be updated to a newer version in order to use this browser extension. Please contact support if you need help.",
"unknownError": "An unknown error occurred",
"failedToStoreVault": "Failed to store vault",
"vaultNotAvailable": "Vault not available",
"failedToRetrieveData": "Failed to retrieve data",
"vaultIsLocked": "Vault is locked",
"failedToUploadVault": "Failed to upload vault",
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
},
"apiErrors": {
"UNKNOWN_ERROR": "An unknown error occurred. Please try again.",
"ACCOUNT_LOCKED": "Account temporarily locked due to too many failed attempts. Please try again later.",
"ACCOUNT_BLOCKED": "Your account has been disabled. If you believe this is a mistake, please contact support.",
"USER_NOT_FOUND": "Invalid username or password. Please try again.",
"INVALID_AUTHENTICATOR_CODE": "Invalid authenticator code. Please try again.",
"INVALID_RECOVERY_CODE": "Invalid recovery code. Please try again.",
"REFRESH_TOKEN_REQUIRED": "Refresh token is required.",
"INVALID_REFRESH_TOKEN": "Invalid refresh token.",
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token revoked successfully.",
"PUBLIC_REGISTRATION_DISABLED": "New account registration is currently disabled on this server. Please contact the administrator.",
"USERNAME_REQUIRED": "Username is required.",
"USERNAME_ALREADY_IN_USE": "Username is already in use.",
"USERNAME_AVAILABLE": "Username is available.",
"USERNAME_MISMATCH": "Username does not match the current user.",
"PASSWORD_MISMATCH": "The provided password does not match your current password.",
"ACCOUNT_SUCCESSFULLY_DELETED": "Account successfully deleted.",
"USERNAME_EMPTY_OR_WHITESPACE": "Username cannot be empty or whitespace.",
"USERNAME_TOO_SHORT": "Username too short: must be at least 3 characters long.",
"USERNAME_TOO_LONG": "Username too long: cannot be longer than 40 characters.",
"USERNAME_INVALID_EMAIL": "Invalid email address.",
"USERNAME_INVALID_CHARACTERS": "Username is invalid, can only contain letters or digits.",
"VAULT_NOT_UP_TO_DATE": "Your vault is not up-to-date. Please synchronize your vault and try again.",
"INTERNAL_SERVER_ERROR": "Internal server error.",
"VAULT_ERROR": "The local vault is not up-to-date. Please synchronize your vault by refreshing the page and try again."
}
},
"content": {
"or": "or",
"new": "New",
"cancel": "Cancel",
"search": "Search",
"vaultLocked": "AliasVault is locked.",
"creatingNewAlias": "Creating new alias...",
"noMatchesFound": "No matches found",
"searchVault": "Search vault...",
"serviceName": "Service name",
"email": "Email",
"username": "Username",
"password": "Password",
"enterServiceName": "Enter service name",
"enterEmailAddress": "Enter email address",
"enterUsername": "Enter username",
"hideFor1Hour": "Hide for 1 hour (current site)",
"hidePermanently": "Hide permanently (current site)",
"createRandomAlias": "Create random alias",
"createUsernamePassword": "Create username/password",
"randomAlias": "Random alias",
"usernamePassword": "Username/password",
"createAndSaveAlias": "Create and save alias",
"createAndSaveCredential": "Create and save credential",
"randomIdentityDescription": "Generate a random identity with a random email address accessible in AliasVault.",
"randomIdentityDescriptionDropdown": "Random identity with random email",
"manualCredentialDescription": "Specify your own email address and username.",
"manualCredentialDescriptionDropdown": "Manual username and password",
"failedToCreateIdentity": "Failed to create identity. Please try again.",
"enterEmailAndOrUsername": "Enter email and/or username",
"autofillWithAliasVault": "Autofill with AliasVault",
"generateRandomPassword": "Generate random password (copy to clipboard)",
"generateNewPassword": "Generate new password",
"togglePasswordVisibility": "Toggle password visibility",
"passwordCopiedToClipboard": "Password copied to clipboard",
"enterEmailAndOrUsernameError": "Enter email and/or username",
"openAliasVaultToUpgrade": "Open AliasVault to upgrade",
"vaultUpgradeRequired": "Vault upgrade required.",
"dismissPopup": "Dismiss popup"
},
"credentials": {
"title": "Credentials",
"addCredential": "Add Credential",
"editCredential": "Edit Credential",
"deleteCredential": "Delete Credential",
"credentialDetails": "Credential Details",
"serviceName": "Service Name",
"serviceNamePlaceholder": "e.g., Gmail, Facebook, Bank",
"website": "Website",
"websitePlaceholder": "https://example.com",
"username": "Username",
"usernamePlaceholder": "Enter username",
"password": "Password",
"passwordPlaceholder": "Enter password",
"generatePassword": "Generate Password",
"copyPassword": "Copy Password",
"showPassword": "Show Password",
"hidePassword": "Hide Password",
"notes": "Notes",
"notesPlaceholder": "Additional notes...",
"totp": "Two-Factor Authentication",
"totpCode": "TOTP Code",
"copyTotp": "Copy TOTP",
"totpSecret": "TOTP Secret",
"totpSecretPlaceholder": "Enter TOTP secret key",
"noCredentials": "No credentials found",
"noCredentialsDescription": "Add your first credential to get started",
"searchPlaceholder": "Search credentials...",
"welcomeTitle": "Welcome to AliasVault!",
"welcomeDescription": "To use the AliasVault browser extension: navigate to a website and use the AliasVault autofill popup to create a new credential.",
"createdAt": "Created",
"updatedAt": "Last updated",
"autofill": "Autofill",
"fillForm": "Fill Form",
"deleteConfirm": "Are you sure you want to delete this credential?",
"saveSuccess": "Credential saved successfully",
"tags": "Tags",
"addTag": "Add Tag",
"removeTag": "Remove Tag",
"folder": "Folder",
"selectFolder": "Select Folder",
"createFolder": "Create Folder",
"saveCredential": "Save credential",
"deleteCredentialTitle": "Delete Credential",
"deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.",
"randomAlias": "Random Alias",
"manual": "Manual",
"service": "Service",
"serviceUrl": "Service URL",
"loginCredentials": "Login Credentials",
"generateRandomUsername": "Generate random username",
"generateRandomPassword": "Generate random password",
"changePasswordComplexity": "Change password complexity",
"passwordLength": "Password length",
"includeLowercase": "Include lowercase letters",
"includeUppercase": "Include uppercase letters",
"includeNumbers": "Include numbers",
"includeSpecialChars": "Include special characters",
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",
"nickName": "Nick Name",
"gender": "Gender",
"birthDate": "Birth Date",
"birthDatePlaceholder": "YYYY-MM-DD",
"metadata": "Metadata",
"validation": {
"required": "This field is required",
"serviceNameRequired": "Service name is required",
"invalidEmail": "Invalid email format",
"invalidDateFormat": "Date must be in YYYY-MM-DD format"
},
"privateEmailTitle": "Private Email",
"privateEmailAliasVaultServer": "AliasVault server",
"privateEmailDescription": "E2E encrypted, fully private.",
"publicEmailTitle": "Public Temp Email Providers",
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
"useDomainChooser": "Use domain chooser",
"enterCustomDomain": "Enter custom domain",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix"
},
"emails": {
"title": "Emails",
"deleteEmailTitle": "Delete Email",
"deleteEmailConfirm": "Are you sure you want to permanently delete this email?",
"from": "From",
"to": "To",
"date": "Date",
"emailContent": "Email content",
"attachments": "Attachments",
"emailNotFound": "Email not found",
"noEmails": "No emails found",
"noEmailsDescription": "You have not received any emails at your private email addresses yet. When you receive a new email, it will appear here.",
"dateFormat": {
"justNow": "just now",
"minutesAgo_single": "{{count}} min ago",
"minutesAgo_plural": "{{count}} mins ago",
"hoursAgo_single": "{{count}} hr ago",
"hoursAgo_plural": "{{count}} hrs ago",
"yesterday": "yesterday"
},
"errors": {
"emailLoadError": "An error occurred while loading emails. Please try again later.",
"emailUnexpectedError": "An unexpected error occurred while loading emails. Please try again later."
},
"apiErrors": {
"CLAIM_DOES_NOT_MATCH_USER": "The current chosen email address is already in use. Please change the email address by editing this credential.",
"CLAIM_DOES_NOT_EXIST": "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."
}
},
"settings": {
"title": "Settings",
"serverUrl": "Server URL",
"language": "Language",
"autofillEnabled": "Enable Autofill",
"version": "Version",
"openInNewWindow": "Open in new window",
"openWebApp": "Open web app",
"loggedIn": "Logged in",
"logout": "Logout",
"globalSettings": "Global Settings",
"autofillPopup": "Autofill popup",
"activeOnAllSites": "Active on all sites (unless disabled below)",
"disabledOnAllSites": "Disabled on all sites",
"enabled": "Enabled",
"disabled": "Disabled",
"rightClickContextMenu": "Right-click context menu",
"autofillMatching": "Autofill Matching",
"autofillMatchingMode": "Autofill matching mode",
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
"autofillMatchingDefault": "URL + subdomain + name wildcard",
"autofillMatchingUrlSubdomain": "URL + subdomain",
"autofillMatchingUrlExact": "Exact URL domain only",
"siteSpecificSettings": "Site-Specific Settings",
"autofillPopupOn": "Autofill popup on: ",
"enabledForThisSite": "Enabled for this site",
"disabledForThisSite": "Disabled for this site",
"temporarilyDisabledUntil": "Temporarily disabled until ",
"resetAllSiteSettings": "Reset all site-specific settings",
"appearance": "Appearance",
"theme": "Theme",
"useDefault": "Use default",
"light": "Light",
"dark": "Dark",
"keyboardShortcuts": "Keyboard Shortcuts",
"configureKeyboardShortcuts": "Configure keyboard shortcuts",
"configure": "Configure",
"security": "Security",
"clipboardClearTimeout": "Clear clipboard after copying",
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
"clipboardClearDisabled": "Never clear",
"clipboardClear5Seconds": "Clear after 5 seconds",
"clipboardClear10Seconds": "Clear after 10 seconds",
"clipboardClear15Seconds": "Clear after 15 seconds",
"autoLockTimeout": "Auto-lock Timeout",
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
"autoLockNever": "Never",
"autoLock15Seconds": "15 seconds",
"autoLock1Minute": "1 minute",
"autoLock5Minutes": "5 minutes",
"autoLock15Minutes": "15 minutes",
"autoLock30Minutes": "30 minutes",
"autoLock1Hour": "1 hour",
"autoLock4Hours": "4 hours",
"autoLock8Hours": "8 hours",
"autoLock24Hours": "24 hours",
"versionPrefix": "Version ",
"preferences": "Preferences",
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"validation": {
"apiUrlRequired": "API URL is required",
"apiUrlInvalid": "Please enter a valid API URL",
"clientUrlRequired": "Client URL is required",
"clientUrlInvalid": "Please enter a valid client URL"
}
},
"upgrade": {
"title": "Upgrade Vault",
"subtitle": "AliasVault has updated and your vault needs to be upgraded. This should only take a few seconds.",
"versionInformation": "Version Information",
"yourVault": "Your vault:",
"newVersion": "New version:",
"upgrade": "Upgrade Vault",
"upgrading": "Upgrading...",
"logout": "Logout",
"whatsNew": "What's New",
"whatsNewDescription": "An upgrade is required to support the following changes:",
"noDescriptionAvailable": "No description available for this version.",
"okay": "Ok",
"status": {
"preparingUpgrade": "Preparing upgrade...",
"vaultAlreadyUpToDate": "Vault is already up to date",
"startingDatabaseTransaction": "Starting database transaction...",
"applyingDatabaseMigrations": "Applying database migrations...",
"applyingMigration": "Applying migration {{current}} of {{total}}...",
"committingChanges": "Committing changes..."
},
"alerts": {
"error": "Error",
"unableToGetVersionInfo": "Unable to get version information. Please try again.",
"selfHostedServer": "Self-Hosted Server",
"selfHostedWarning": "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.",
"cancel": "Cancel",
"continueUpgrade": "Continue Upgrade",
"upgradeFailed": "Upgrade Failed",
"failedToApplyMigration": "Failed to apply migration ({{current}} of {{total}})",
"unknownErrorDuringUpgrade": "An unknown error occurred during the upgrade. Please try again."
}
}
}

View File

@@ -96,10 +96,10 @@
"unknownError": "Произошла неизвестная ошибка",
"failedToStoreVault": "Не удалось сохранить хранилище",
"vaultNotAvailable": "Хранилище недоступно",
"failedToRetrieveData": "Failed to retrieve data",
"failedToRetrieveData": "Не удалось получить данные",
"vaultIsLocked": "Хранилище заблокировано",
"failedToUploadVault": "Не удалось загрузить хранилище",
"passwordChanged": "Your password has changed since the last time you logged in. Please login again for security reasons."
"passwordChanged": "С момента вашего последнего входа ваш пароль изменился. Пожалуйста, войдите еще раз в целях безопасности."
},
"apiErrors": {
"UNKNOWN_ERROR": "Произошла неизвестная ошибка. Пожалуйста, попробуйте снова.",
@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Избегать двусмысленных символов (o, 0 и т.д.).",
"generateNewPreview": "Создать новый предварительный просмотр",
"generateRandomAlias": "Сгенерировать случайный псевдоним",
"clearAliasFields": "Очистить поля псевдонимов",
"alias": "Псевдоним",
"firstName": "Имя",
"lastName": "Фамилия",
@@ -243,15 +244,15 @@
"invalidEmail": "Неверный формат электронной почты",
"invalidDateFormat": "Дата должна быть указана в формате ГГГГ-ММ-ДД"
},
"privateEmailTitle": "Private Email",
"privateEmailAliasVaultServer": "AliasVault server",
"privateEmailDescription": "E2E encrypted, fully private.",
"publicEmailTitle": "Public Temp Email Providers",
"publicEmailDescription": "Anonymous but limited privacy. Email content is readable by anyone that knows the address.",
"useDomainChooser": "Use domain chooser",
"enterCustomDomain": "Enter custom domain",
"enterFullEmail": "Enter full email address",
"enterEmailPrefix": "Enter email prefix"
"privateEmailTitle": "Личная электронная почта",
"privateEmailAliasVaultServer": "Сервер AliasVault",
"privateEmailDescription": "Шифрование E2E, полностью приватный.",
"publicEmailTitle": "Общедоступные временные поставщики электронной почты",
"publicEmailDescription": "Анонимность, но ограниченная конфиденциальность. Содержимое письма может прочитать любой, кому известен адрес.",
"useDomainChooser": "Использовать выбор домена",
"enterCustomDomain": "Ввести пользовательский домен",
"enterFullEmail": "Введите полный адрес электронной почты",
"enterEmailPrefix": "Введите префикс электронной почты"
},
"emails": {
"title": "Письма",
@@ -299,12 +300,12 @@
"enabled": "Включен",
"disabled": "Выключен",
"rightClickContextMenu": "Контекстное меню правым щелчком мыши",
"autofillMatching": "Autofill Matching",
"autofillMatchingMode": "Autofill matching mode",
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
"autofillMatchingDefault": "URL + subdomain + name wildcard",
"autofillMatchingUrlSubdomain": "URL + subdomain",
"autofillMatchingUrlExact": "Exact URL domain only",
"autofillMatching": "Соответствие автозаполнения",
"autofillMatchingMode": "Режим сопоставления автозаполнения",
"autofillMatchingModeDescription": "Определяет, какие учетные данные считаются соответствующими и отображаются в качестве предложений во всплывающем окне автозаполнения для данного веб-сайта.",
"autofillMatchingDefault": "URL + поддомен + подстановочный знак в названии",
"autofillMatchingUrlSubdomain": "URL + поддомен",
"autofillMatchingUrlExact": "Только точный URL-адрес домена",
"siteSpecificSettings": "Настройки для конкретного сайта",
"autofillPopupOn": "Всплывающее окно автозаполнения: ",
"enabledForThisSite": "Включено для этого сайта",
@@ -319,36 +320,36 @@
"keyboardShortcuts": "Горячие клавиши",
"configureKeyboardShortcuts": "Настройка горячих клавиш",
"configure": "Настройка",
"security": "Security",
"clipboardClearTimeout": "Clear clipboard after copying",
"clipboardClearTimeoutDescription": "Automatically clear the clipboard after copying sensitive data",
"clipboardClearDisabled": "Never clear",
"clipboardClear5Seconds": "Clear after 5 seconds",
"clipboardClear10Seconds": "Clear after 10 seconds",
"clipboardClear15Seconds": "Clear after 15 seconds",
"autoLockTimeout": "Auto-lock Timeout",
"autoLockTimeoutDescription": "Automatically lock the vault after a period of inactivity",
"autoLockTimeoutHelp": "The vault will only lock after the specified period of inactivity (no autofill usage or extension popup opened). The vault will always lock when the browser is closed, regardless of this setting.",
"autoLockNever": "Never",
"autoLock15Seconds": "15 seconds",
"autoLock1Minute": "1 minute",
"autoLock5Minutes": "5 minutes",
"autoLock15Minutes": "15 minutes",
"autoLock30Minutes": "30 minutes",
"autoLock1Hour": "1 hour",
"autoLock4Hours": "4 hours",
"autoLock8Hours": "8 hours",
"autoLock24Hours": "24 hours",
"security": "Безопасность",
"clipboardClearTimeout": "Очистить буфер обмена после копирования",
"clipboardClearTimeoutDescription": "Автоматическая очистка буфера обмена после копирования конфиденциальных данных",
"clipboardClearDisabled": "Никогда не очищать",
"clipboardClear5Seconds": "Очистка через 5 секунд",
"clipboardClear10Seconds": "Очистка через 10 секунд",
"clipboardClear15Seconds": "Очистка через 15 секунд",
"autoLockTimeout": "Тайм-аут автоматической блокировки",
"autoLockTimeoutDescription": "Автоматическая блокировка хранилища после некоторого периода бездействия",
"autoLockTimeoutHelp": "Хранилище будет заблокировано только по истечении указанного периода бездействия (не будет использоваться функция автозаполнения или не откроется всплывающее окно с расширением). Хранилище всегда будет заблокировано при закрытии браузера, независимо от этого параметра.",
"autoLockNever": "Никогда",
"autoLock15Seconds": "15 секунд",
"autoLock1Minute": "1 минута",
"autoLock5Minutes": "5 минут",
"autoLock15Minutes": "15 минут",
"autoLock30Minutes": "30 минут",
"autoLock1Hour": "1 час",
"autoLock4Hours": "4 часа",
"autoLock8Hours": "8 часов",
"autoLock24Hours": "24 часов",
"versionPrefix": "Версия ",
"preferences": "Preferences",
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"preferences": "Предпочтения",
"autofillSettings": "Настройки автозаполнения",
"clipboardSettings": "Настройки буфера обмена",
"contextMenuSettings": "Настройки контекстного меню",
"contextMenu": "Контекстное меню",
"contextMenuEnabled": "Контекстное меню включено",
"contextMenuDisabled": "Контекстное меню отключено",
"contextMenuDescription": "Щелкните правой кнопкой мыши на полях ввода, чтобы получить доступ к параметрам AliasVault",
"selectLanguage": "Выбрать язык",
"validation": {
"apiUrlRequired": "Требуется URL-адрес API",
"apiUrlInvalid": "Пожалуйста, введите корректный URL-адрес API",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Avoid ambiguous characters (o, 0, etc.)",
"generateNewPreview": "Generate new preview",
"generateRandomAlias": "Generate Random Alias",
"clearAliasFields": "Clear Alias Fields",
"alias": "Alias",
"firstName": "First Name",
"lastName": "Last Name",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "Уникайте неоднозначних символів (o, 0 тощо)",
"generateNewPreview": "Згенерувати новий попередній перегляд",
"generateRandomAlias": "Генерувати випадковий псевдонім",
"clearAliasFields": "Clear Alias Fields",
"alias": "Псевдонім",
"firstName": "Ім’я",
"lastName": "Прізвище",
@@ -299,12 +300,12 @@
"enabled": "Увімкнено",
"disabled": "Вимкнено",
"rightClickContextMenu": "Контекстне меню правою кнопкою миші",
"autofillMatching": "Autofill Matching",
"autofillMatchingMode": "Autofill matching mode",
"autofillMatchingModeDescription": "Determines which credentials are considered a match and shown as suggestions in the autofill popup for a given website.",
"autofillMatchingDefault": "URL + subdomain + name wildcard",
"autofillMatchingUrlSubdomain": "URL + subdomain",
"autofillMatchingUrlExact": "Exact URL domain only",
"autofillMatching": "Автозаповнення відповідності",
"autofillMatchingMode": "Режим автозаповнення відповідностей",
"autofillMatchingModeDescription": "Визначає, які облікові дані вважаються відповідними та будуть показуватися як пропозиції у спливному вікні автозаповнення для певного вебсайту.",
"autofillMatchingDefault": "URL-адреса + піддомен + універсальне ім'я",
"autofillMatchingUrlSubdomain": "URL-адреса + піддомен",
"autofillMatchingUrlExact": "Лише точний домен URL-адреси",
"siteSpecificSettings": "Налаштування, специфічні для сайту",
"autofillPopupOn": "Спливаюче вікно автозаповнення на: ",
"enabledForThisSite": "Увімкнено для цього сайту",
@@ -326,7 +327,7 @@
"clipboardClear5Seconds": "Очистити після 5 секунд",
"clipboardClear10Seconds": "Очистити після 10 секунд",
"clipboardClear15Seconds": "Очистити після 15 секунд",
"autoLockTimeout": "Auto-lock Timeout",
"autoLockTimeout": "Тайм-аут автоматичного блокування",
"autoLockTimeoutDescription": "Автоматично блокувати сховище після періоду бездіяльності",
"autoLockTimeoutHelp": "Сховище буде заблоковано лише після зазначеного періоду бездіяльності (не використовується автозаповнення або не відкривається спливне вікно розширення). Сховище завжди блокуватиметься, коли браузер закривається, незалежно від цього налаштування.",
"autoLockNever": "Ніколи",
@@ -340,15 +341,15 @@
"autoLock8Hours": "8 годин",
"autoLock24Hours": "24 години",
"versionPrefix": "Версія ",
"preferences": "Preferences",
"autofillSettings": "Autofill Settings",
"clipboardSettings": "Clipboard Settings",
"contextMenuSettings": "Context Menu Settings",
"contextMenu": "Context Menu",
"contextMenuEnabled": "Context menu is enabled",
"contextMenuDisabled": "Context menu is disabled",
"contextMenuDescription": "Right-click on input fields to access AliasVault options",
"selectLanguage": "Select Language",
"preferences": "Налаштування",
"autofillSettings": "Налаштування автозаповнення",
"clipboardSettings": "Параметри буфера обміну",
"contextMenuSettings": "Налаштування контекстного меню",
"contextMenu": "Контекстне меню",
"contextMenuEnabled": "Контекстне меню увімкнено",
"contextMenuDisabled": "Контекстне меню вимкнено",
"contextMenuDescription": "Натисніть правою кнопкою миші на поля введення, щоб отримати доступ до параметрів AliasVault",
"selectLanguage": "Виберіть мову",
"validation": {
"apiUrlRequired": "URL-адреса API обов'язкова",
"apiUrlInvalid": "Будь ласка, введіть дійсну URL-адресу API",

View File

@@ -229,6 +229,7 @@
"avoidAmbiguousChars": "避免易混淆字符o、0 等)",
"generateNewPreview": "生成新预览",
"generateRandomAlias": "生成随机别名",
"clearAliasFields": "清除别名字段",
"alias": "别名",
"firstName": "名",
"lastName": "姓",

View File

@@ -6,7 +6,7 @@ export class AppInfo {
/**
* The current extension version. This should be updated with each release of the extension.
*/
public static readonly VERSION = '0.22.0';
public static readonly VERSION = '0.23.2';
/**
* The minimum supported AliasVault server (API) version. If the server version is below this, the

View File

@@ -1,4 +1,3 @@
// TODO: store generic setting constants somewhere else.
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';
export const GLOBAL_AUTOFILL_POPUP_ENABLED_KEY = 'local:aliasvault_global_autofill_popup_enabled';
export const GLOBAL_CONTEXT_MENU_ENABLED_KEY = 'local:aliasvault_global_context_menu_enabled';
@@ -9,5 +8,6 @@ export const AUTO_LOCK_TIMEOUT_KEY = 'local:aliasvault_auto_lock_timeout';
export const AUTOFILL_MATCHING_MODE_KEY = 'local:aliasvault_autofill_matching_mode';
// TODO: store these settings in the actual vault when updating the datamodel for roadmap v1.0.
export const LAST_CUSTOM_EMAIL_KEY = 'local:aliasvault_last_custom_email';
export const LAST_CUSTOM_USERNAME_KEY = 'local:aliasvault_last_custom_username';
export const CUSTOM_EMAIL_HISTORY_KEY = 'local:aliasvault_custom_email_history';
export const CUSTOM_USERNAME_HISTORY_KEY = 'local:aliasvault_custom_username_history';
export const SKIP_FORM_RESTORE_KEY = 'local:aliasvault_skip_form_restore';

View File

@@ -479,14 +479,18 @@ export class FormDetector {
excludeElements: HTMLInputElement[] = []
): HTMLInputElement | null {
const all = this.findAllInputFields(form, patterns, types, excludeElements);
// Filter out parent-child duplicates
const filtered = this.filterOutNestedDuplicates(all);
// if email type explicitly requested, prefer actual <input type="email">
if (types.includes('email')) {
const emailMatch = all.find(i => (i.type || '').toLowerCase() === 'email');
const emailMatch = filtered.find(i => (i.type || '').toLowerCase() === 'email');
if (emailMatch) {
return emailMatch;
}
}
return all.length > 0 ? all[0] : null;
return filtered.length > 0 ? filtered[0] : null;
}
/**
@@ -496,25 +500,32 @@ export class FormDetector {
primary: HTMLInputElement | null,
confirm: HTMLInputElement | null
} {
// Find primary email field
const primaryEmail = this.findInputField(
// Find all email fields first
const emailFields = this.findAllInputFields(
form,
CombinedFieldPatterns.email,
['text', 'email']
);
// Filter out parent-child relationships
const filteredEmailFields = this.filterOutNestedDuplicates(emailFields);
const primaryEmail = filteredEmailFields[0] ?? null;
/*
* Find confirmation email field if primary exists
* and ensure it's not the same as the primary email field.
*/
const confirmEmail = primaryEmail
? this.findInputField(
const confirmEmailFields = primaryEmail
? this.findAllInputFields(
form,
CombinedFieldPatterns.emailConfirm,
['text', 'email'],
[primaryEmail]
)
: null;
: [];
const filteredConfirmFields = this.filterOutNestedDuplicates(confirmEmailFields);
const confirmEmail = filteredConfirmFields[0] ?? null;
return {
primary: primaryEmail,
@@ -667,6 +678,56 @@ export class FormDetector {
};
}
/**
* Filter out nested duplicates where a parent element and its child are both detected.
* This happens with custom elements that contain actual input elements.
* We prefer the innermost actual input element over the parent custom element.
*/
private filterOutNestedDuplicates(fields: HTMLInputElement[]): HTMLInputElement[] {
if (fields.length <= 1) {
return fields;
}
const filtered: HTMLInputElement[] = [];
for (const field of fields) {
let shouldInclude = true;
// Check if this field is a parent of any other field in the list
for (const otherField of fields) {
if (field !== otherField) {
// Check if field contains otherField (field is parent)
if (field.contains(otherField)) {
shouldInclude = false;
break;
}
// Check if field's shadow DOM contains otherField
const fieldWithShadow = field as HTMLElement & { shadowRoot?: ShadowRoot };
if (fieldWithShadow.shadowRoot && fieldWithShadow.shadowRoot.contains(otherField)) {
shouldInclude = false;
break;
}
}
}
if (shouldInclude) {
// Also check if this field is not already represented by its actual input
const actualInput = this.getActualInputElement(field);
if (actualInput !== field) {
// If the actual input is also in the list, skip the parent
if (fields.includes(actualInput as HTMLInputElement)) {
continue;
}
}
filtered.push(field);
}
}
return filtered;
}
/**
* Find the password field in a form.
*/
@@ -676,9 +737,12 @@ export class FormDetector {
} {
const passwordFields = this.findAllInputFields(form, CombinedFieldPatterns.password, ['password']);
// Filter out parent-child relationships to avoid detecting the same field twice
const filteredFields = this.filterOutNestedDuplicates(passwordFields);
return {
primary: passwordFields[0] ?? null,
confirm: passwordFields[1] ?? null
primary: filteredFields[0] ?? null,
confirm: filteredFields[1] ?? null
};
}

View File

@@ -33,7 +33,12 @@ export class FormFiller {
return;
}
this.fillBasicFields(credential);
// Fill basic fields and password fields in parallel
await Promise.all([
this.fillBasicFields(credential),
this.fillPasswordFields(credential)
]);
this.fillBirthdateFields(credential);
this.fillGenderFields(credential);
}
@@ -61,7 +66,7 @@ export class FormFiller {
clientY: window.innerHeight / 2
});
// Note: isTrusted is read-only and set by the browser
if (!await this.clickValidator.validateClick(dummyEvent)) {
console.warn('[AliasVault Security] Form autofill blocked: Page-wide attack detected');
return false;
@@ -94,7 +99,7 @@ export class FormFiller {
*/
private getAllFormFields(): HTMLElement[] {
const fields: HTMLElement[] = [];
if (this.form.usernameField) {
fields.push(this.form.usernameField);
}
@@ -110,7 +115,7 @@ export class FormFiller {
if (this.form.emailConfirmField) {
fields.push(this.form.emailConfirmField);
}
return fields;
}
@@ -132,8 +137,8 @@ export class FormFiller {
const centerY = rect.top + rect.height / 2;
// Check if field is within viewport
if (rect.width === 0 || rect.height === 0 ||
centerX < 0 || centerY < 0 ||
if (rect.width === 0 || rect.height === 0 ||
centerX < 0 || centerY < 0 ||
centerX > window.innerWidth || centerY > window.innerHeight) {
console.warn('[AliasVault Security] Field outside viewport or zero-sized:', rect);
return false;
@@ -142,16 +147,16 @@ export class FormFiller {
// Use elementsFromPoint to check what's actually at the field center
try {
const elementsAtPoint = document.elementsFromPoint(centerX, centerY);
if (elementsAtPoint.length === 0) {
console.warn('[AliasVault Security] No elements found at field center');
return false;
}
// Check if our field is in the element stack (or its parents/children)
const fieldFound = elementsAtPoint.some(element =>
element === field ||
field.contains(element) ||
const fieldFound = elementsAtPoint.some(element =>
element === field ||
field.contains(element) ||
element.contains(field)
);
@@ -167,7 +172,7 @@ export class FormFiller {
}
const style = getComputedStyle(element);
// Check for nearly transparent overlays
const opacity = parseFloat(style.opacity);
if (opacity > 0 && opacity < 0.1) {
@@ -184,7 +189,7 @@ export class FormFiller {
// Check for elements covering large areas (potential clickjacking overlays)
const elementRect = element.getBoundingClientRect();
if (elementRect.width >= window.innerWidth * 0.8 &&
if (elementRect.width >= window.innerWidth * 0.8 &&
elementRect.height >= window.innerHeight * 0.8) {
console.warn('[AliasVault Security] Large covering element detected:', element);
return true;
@@ -207,35 +212,35 @@ export class FormFiller {
try {
// Find all forms on the page
const allForms = Array.from(document.querySelectorAll('form'));
if (allForms.length <= 1) {
return false; // Only one form, no decoy risk
}
let suspiciousFormCount = 0;
for (const form of allForms) {
const hasPasswordField = form.querySelector('input[type="password"]');
const hasEmailField = form.querySelector('input[type="email"], input[name*="email" i], input[placeholder*="email" i]');
const hasUsernameField = form.querySelector('input[type="text"], input[name*="user" i], input[placeholder*="user" i]');
// Count forms with login-like patterns
if (hasPasswordField && (hasEmailField || hasUsernameField)) {
const formRect = form.getBoundingClientRect();
const isVisible = formRect.width > 0 && formRect.height > 0;
if (isVisible) {
suspiciousFormCount++;
}
}
}
// If more than 2 visible login forms, it's suspicious
if (suspiciousFormCount > 2) {
console.warn('[AliasVault Security] Multiple login forms detected:', suspiciousFormCount);
return true;
}
return false;
} catch (error) {
console.warn('[AliasVault Security] Decoy form detection error:', error);
@@ -251,7 +256,7 @@ export class FormFiller {
private setElementValue(element: HTMLInputElement | HTMLSelectElement, value: string): void {
// Try to set value directly on the element
element.value = value;
// If it's a custom element with shadow DOM, try to find and fill the actual input
if (element.shadowRoot) {
const shadowInput = element.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
@@ -261,7 +266,7 @@ export class FormFiller {
this.triggerInputEvents(shadowInput, false);
}
}
// Also check if the element contains a regular child input (non-shadow DOM)
const childInput = element.querySelector('input, textarea') as HTMLInputElement;
if (childInput && childInput !== element) {
@@ -274,18 +279,9 @@ export class FormFiller {
* Fill the basic fields of the form.
* @param credential The credential to fill the form with.
*/
private fillBasicFields(credential: Credential): void {
private async fillBasicFields(credential: Credential): Promise<void> {
if (this.form.usernameField && credential.Username) {
this.setElementValue(this.form.usernameField, credential.Username);
this.triggerInputEvents(this.form.usernameField);
}
if (this.form.passwordField && credential.Password) {
this.fillPasswordField(this.form.passwordField, credential.Password);
}
if (this.form.passwordConfirmField && credential.Password) {
this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
await this.fillTextFieldWithTyping(this.form.usernameField, credential.Username);
}
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
@@ -329,6 +325,70 @@ export class FormFiller {
}
}
/**
* Fill a text field with character-by-character typing to better simulate human input.
* This method is similar to fillPasswordField but optimized for regular text fields.
*
* @param field The text field to fill.
* @param text The text to fill the field with.
*/
private async fillTextFieldWithTyping(field: HTMLInputElement, text: string): Promise<void> {
// Find the actual input element (could be in shadow DOM)
let actualInput = field;
// Check for shadow DOM input
if (field.shadowRoot) {
const shadowInput = field.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
if (shadowInput) {
actualInput = shadowInput;
}
} else if (field.tagName.toLowerCase() !== 'input' && field.tagName.toLowerCase() !== 'textarea') {
// Check for child input (non-shadow DOM) only if field is not already an input
const childInput = field.querySelector('input, textarea') as HTMLInputElement;
if (childInput) {
actualInput = childInput;
}
}
// Clear the field first without triggering events
actualInput.value = '';
// Type each character with a small delay
for (let i = 0; i < text.length; i++) {
actualInput.value += text[i];
/*
* Small delay between characters to simulate human typing
* This helps with sites that have input event handlers
*/
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
}
// Trigger events once after all typing is complete
this.triggerInputEvents(actualInput, true);
}
/**
* Fill password fields sequentially to avoid visual conflicts.
* First fills the main password field, then the confirm field if present.
* @param credential The credential containing the password.
*/
private async fillPasswordFields(credential: Credential): Promise<void> {
if (!credential.Password) {
return;
}
// Fill main password field first
if (this.form.passwordField) {
await this.fillPasswordField(this.form.passwordField, credential.Password);
}
// Then fill password confirm field after main field is complete
if (this.form.passwordConfirmField) {
await this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
}
}
/**
* Fill the password field with the given password. This uses a small delay between each character to simulate human typing.
* Simulates actual keystroke behavior by appending characters one by one.
@@ -340,48 +400,37 @@ export class FormFiller {
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
// Find the actual input element (could be in shadow DOM)
let actualInput = field;
let isCustomElement = false;
// Check for shadow DOM input
if (field.shadowRoot) {
const shadowInput = field.shadowRoot.querySelector('input[type="password"], input') as HTMLInputElement;
if (shadowInput) {
actualInput = shadowInput;
isCustomElement = true;
}
} else if (field.tagName.toLowerCase() !== 'input') {
// Check for child input (non-shadow DOM) only if field is not already an input
const childInput = field.querySelector('input[type="password"], input') as HTMLInputElement;
if (childInput) {
actualInput = childInput;
isCustomElement = true;
}
}
// Clear the field first
// Clear the field first without triggering events
actualInput.value = '';
if (isCustomElement) {
field.value = '';
}
this.triggerInputEvents(actualInput, true);
// Type each character with a small delay
for (const char of password) {
// Append the character to the actual input
actualInput.value += char;
if (isCustomElement) {
// Also update the custom element's value property for compatibility
field.value += char;
}
// Small random delay between 5-15ms to simulate human typing
this.triggerInputEvents(actualInput, false);
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 5));
for (let i = 0; i < password.length; i++) {
actualInput.value += password[i];
/*
* Small delay between characters to simulate human typing
* This helps with sites that have input event handlers
*/
await new Promise(resolve => setTimeout(resolve, Math.random() * 10 + 10));
}
this.triggerInputEvents(actualInput, false);
if (isCustomElement) {
this.triggerInputEvents(field, false);
}
// Trigger events once after all typing is complete
this.triggerInputEvents(actualInput, true);
}
/**

View File

@@ -79,4 +79,75 @@ describe('FormDetector generic tests', () => {
expect(form).toBe(false);
});
});
describe('Nested custom elements (parent-child duplicate prevention)', () => {
describe('TrueNAS-style nested custom elements', () => {
const htmlFile = 'nested-custom-elements.html';
it('should not detect both parent custom element and child input as separate password fields', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
// Click on the actual password input element
const passwordInput = document.getElementById('password-field');
const formDetector = new FormDetector(document, passwordInput as HTMLElement);
// Get the detected form
const form = formDetector.getForm();
expect(form).toBeTruthy();
// Should detect only ONE password field
expect(form?.passwordField).toBeTruthy();
expect(form?.passwordConfirmField).toBeFalsy();
// The detected password field should be the actual input element
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
expect(form?.passwordField?.type).toBe('password');
expect(form?.passwordField?.id).toBe('password-field');
});
it('should detect username field correctly without duplication', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
const usernameInput = document.getElementById('username-field');
const formDetector = new FormDetector(document, usernameInput as HTMLElement);
const form = formDetector.getForm();
expect(form).toBeTruthy();
// Should detect the username field
expect(form?.usernameField).toBeTruthy();
expect(form?.usernameField?.tagName.toLowerCase()).toBe('input');
expect(form?.usernameField?.id).toBe('username-field');
});
});
describe('Nested custom elements with actual password confirm field', () => {
const htmlFile = 'nested-custom-elements-confirm.html';
it('should correctly identify actual password confirm fields vs parent-child duplicates', () => {
const dom = createTestDom(htmlFile);
const document = dom.window.document;
const passwordElement = document.getElementById('password-field');
const formDetector = new FormDetector(document, passwordElement as HTMLElement);
const form = formDetector.getForm();
expect(form).toBeTruthy();
// Should correctly detect both password and confirm as separate fields
expect(form?.passwordField).toBeTruthy();
expect(form?.passwordConfirmField).toBeTruthy();
// Both should be actual input elements
expect(form?.passwordField?.tagName.toLowerCase()).toBe('input');
expect(form?.passwordConfirmField?.tagName.toLowerCase()).toBe('input');
// They should be different elements
expect(form?.passwordField?.id).toBe('password-field');
expect(form?.passwordConfirmField?.id).toBe('password-confirm-field');
});
});
});
});

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Registration Form - Nested Custom Elements with Confirm</title>
</head>
<body>
<form id="registration-form">
<div class="field-group">
<ix-input formcontrolname="username" type="text" ix-label="Username" name="username">
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
</ix-input>
</div>
<div class="field-group">
<ix-input formcontrolname="password" type="password" ix-label="Password" name="password">
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
</ix-input>
</div>
<div class="field-group">
<ix-input formcontrolname="passwordConfirm" type="password" ix-label="Confirm Password" name="passwordConfirm">
<input id="password-confirm-field" type="password" aria-label="Confirm Password" name="passwordConfirm" class="mat-input-element">
</ix-input>
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>TrueNAS Login - Nested Custom Elements</title>
</head>
<body>
<form id="login-form">
<div class="field-group">
<ix-input id="username-wrapper" formcontrolname="username" type="text" ix-label="Username" name="username">
<ix-label><label><span>Username</span></label></ix-label>
<div class="input-container">
<input id="username-field" type="text" aria-label="Username" name="username" class="mat-input-element">
</div>
</ix-input>
</div>
<div class="field-group">
<ix-input id="password-wrapper" formcontrolname="password" type="password" ix-label="Password" name="password">
<ix-label><label><span>Password</span></label></ix-label>
<div class="input-container">
<input id="password-field" type="password" aria-label="Password" name="password" class="mat-input-element">
</div>
</ix-input>
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,148 @@
import { FormDetector } from '../formDetector/FormDetector';
/**
* Utility for detecting service name and URL information
* Shared between content script and popup dashboard
*/
export class ServiceDetectionUtility {
/**
* Get service information from the current page
*/
public static getServiceInfo(document: Document, location: Location): ServiceInfo {
// Get suggested service names using FormDetector
const suggestedNames = FormDetector.getSuggestedServiceName(document, location);
// Get the current URL
const currentUrl = location.href;
// Process the URL to extract service URL (origin + pathname)
let serviceUrl = '';
try {
const url = new URL(currentUrl);
// Only include http/https URLs
if (url.protocol === 'http:' || url.protocol === 'https:') {
serviceUrl = url.origin + url.pathname;
// Remove trailing slash
if (serviceUrl.endsWith('/')) {
serviceUrl = serviceUrl.slice(0, -1);
}
}
} catch (error) {
console.error('Error parsing current URL:', error);
}
return {
suggestedNames,
currentUrl,
serviceUrl,
domain: location.hostname.replace(/^www\./, '')
};
}
/**
* Get service information from tab data (for use in popup dashboard)
*/
public static getServiceInfoFromTab(tabUrl: string, tabTitle?: string): ServiceInfo {
try {
const url = new URL(tabUrl);
const location = {
href: tabUrl,
hostname: url.hostname,
protocol: url.protocol,
pathname: url.pathname,
origin: url.origin
} as Location;
// Create a minimal document object for service name detection
const mockDocument = {
title: tabTitle || url.hostname
} as Document;
// Use FormDetector logic for service name detection
const suggestedNames = FormDetector.getSuggestedServiceName(mockDocument, location);
// Get service URL (origin + pathname)
let serviceUrl = '';
if (url.protocol === 'http:' || url.protocol === 'https:') {
serviceUrl = url.origin + url.pathname;
// Remove trailing slash
if (serviceUrl.endsWith('/')) {
serviceUrl = serviceUrl.slice(0, -1);
}
}
return {
suggestedNames,
currentUrl: tabUrl,
serviceUrl,
domain: url.hostname.replace(/^www\./, '')
};
} catch (error) {
console.error('Error parsing tab URL:', error);
// Fallback to basic hostname detection
const domain = tabUrl.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
return {
suggestedNames: [domain],
currentUrl: tabUrl,
serviceUrl: tabUrl,
domain
};
}
}
/**
* Get encoded service information suitable for URL parameters
*/
public static getEncodedServiceInfo(document: Document, location: Location): EncodedServiceInfo {
const serviceInfo = this.getServiceInfo(document, location);
return {
serviceName: serviceInfo.suggestedNames.length > 0 ? encodeURIComponent(serviceInfo.suggestedNames[0]) : '',
serviceUrl: serviceInfo.serviceUrl ? encodeURIComponent(serviceInfo.serviceUrl) : '',
currentUrl: encodeURIComponent(serviceInfo.currentUrl),
domain: encodeURIComponent(serviceInfo.domain)
};
}
/**
* Get encoded service information from tab data
*/
public static getEncodedServiceInfoFromTab(tabUrl: string, tabTitle?: string): EncodedServiceInfo {
const serviceInfo = this.getServiceInfoFromTab(tabUrl, tabTitle);
return {
serviceName: serviceInfo.suggestedNames.length > 0 ? encodeURIComponent(serviceInfo.suggestedNames[0]) : '',
serviceUrl: serviceInfo.serviceUrl ? encodeURIComponent(serviceInfo.serviceUrl) : '',
currentUrl: encodeURIComponent(serviceInfo.currentUrl),
domain: encodeURIComponent(serviceInfo.domain)
};
}
}
/**
* Service information interface
*/
export type ServiceInfo = {
/** Array of suggested service names */
suggestedNames: string[];
/** Current page URL */
currentUrl: string;
/** Service URL (origin + pathname) */
serviceUrl: string;
/** Domain name without www prefix */
domain: string;
}
/**
* Encoded service information interface
*/
export type EncodedServiceInfo = {
/** URL-encoded primary service name */
serviceName: string;
/** URL-encoded service URL */
serviceUrl: string;
/** URL-encoded current page URL */
currentUrl: string;
/** URL-encoded domain */
domain: string;
}

View File

@@ -20,7 +20,7 @@ export default defineConfig({
return {
name: "AliasVault",
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
version: "0.22.0",
version: "0.23.2",
content_security_policy: {
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},

View File

@@ -1,194 +0,0 @@
module.exports = {
root: true,
ignorePatterns: ["dist/**", "node_modules/**", "utils/dist/shared/**", "expo-env.d.ts", "*.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: "latest",
sourceType: "module",
project: "./tsconfig.json",
tsconfigRootDir: ".",
},
plugins: [
"@typescript-eslint",
"react",
"react-hooks",
"react-native",
"import",
"jsdoc",
],
extends: [
"expo",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:react-native/all",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript"
],
env: {
browser: true,
node: true,
"react-native/react-native": true,
},
globals: {
__DEV__: "readonly",
chrome: "readonly",
},
settings: {
react: {
version: "detect",
},
"import/ignore": ["node_modules/react-native/index\\.js"],
'react-native/components': {
Text: ['ThemedText'],
},
},
rules: {
// TypeScript
"@typescript-eslint/await-thenable": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/explicit-member-accessibility": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/typedef": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
vars: "all",
args: "after-used",
ignoreRestSiblings: true,
varsIgnorePattern: "^_",
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/naming-convention": [
"error",
{
selector: "interface",
format: ["PascalCase"],
prefix: ["I"],
},
{
selector: "class",
format: ["PascalCase"],
},
],
// React
"react/react-in-jsx-scope": "off",
"react/no-unused-prop-types": "error",
"react/jsx-no-constructed-context-values": "error",
// React Hooks
"react-hooks/exhaustive-deps": "warn",
// React Native
"react-native/no-unused-styles": "warn",
"react-native/split-platform-components": "warn",
"react-native/no-inline-styles": "warn",
"react-native/no-color-literals": "warn",
"react-native/no-single-element-style-arrays": "warn",
// Import
"import/no-unresolved": "error",
"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
}
}
],
// JSDoc
"jsdoc/require-jsdoc": [
"error",
{
require: {
FunctionDeclaration: true,
MethodDefinition: true,
ClassDeclaration: true,
ArrowFunctionExpression: true,
FunctionExpression: true,
},
},
],
"jsdoc/require-description": [
"error",
{
contexts: [
"FunctionDeclaration",
"MethodDefinition",
"ClassDeclaration",
"ArrowFunctionExpression",
"FunctionExpression",
],
},
],
// Style
curly: ["error", "all"],
"brace-style": ["error", "1tbs", { allowSingleLine: false }],
indent: [
"error",
2,
{
SwitchCase: 1,
VariableDeclarator: 1,
outerIIFEBody: 1,
MemberExpression: 1,
FunctionDeclaration: { parameters: 1, body: 1 },
FunctionExpression: { parameters: 1, body: 1 },
CallExpression: { arguments: 1 },
ArrayExpression: 1,
ObjectExpression: 1,
ImportDeclaration: 1,
flatTernaryExpressions: false,
ignoreComments: false,
},
],
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1, maxBOF: 0 }],
"no-console": ["error", { allow: ["warn", "error", "info", "debug"] }],
"spaced-comment": ["error", "always"],
"multiline-comment-style": ["error", "starred-block"],
// TODO: this line is added to prevent "Raw text (×) cannot be used outside of a <Text> tag" errors.
// When adding proper i18n multilingual enforcement checks, the following line should be removed
'react-native/no-raw-text': 'off',
// Disable prop-types rule because we're using TypeScript for type-checking
'react/prop-types': 'off',
},
};

View File

@@ -73,14 +73,14 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
ndkVersion rootProject.ext.ndkVersion
@@ -93,8 +93,8 @@ android {
applicationId 'net.aliasvault.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 220000
versionName "0.22.0"
versionCode 230200
versionName "0.23.2"
}
signingConfigs {
debug {
@@ -184,7 +184,10 @@ dependencies {
implementation("androidx.biometric:biometric:1.1.0")
// Add vector drawable support for SVG
implementation 'com.caverock:androidsvg:1.4'
implementation("com.caverock:androidsvg-aar:1.4")
// Add Argon2 library for password key derivation
implementation("com.lambdapioneer.argon2kt:argon2kt:1.4.0")
// Test dependencies
testImplementation 'junit:junit:4.13.2'

View File

@@ -12,7 +12,7 @@
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:usesCleartextTraffic="true" android:localeConfig="@xml/locales_config">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:usesCleartextTraffic="true" android:localeConfig="@xml/locales_config" android:networkSecurityConfig="@xml/network_security_config">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>

View File

@@ -778,6 +778,27 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
}
}
/**
* Derive a key from a password using Argon2Id.
* @param password The password to derive from
* @param salt The salt to use
* @param encryptionType The type of encryption (should be "Argon2Id")
* @param encryptionSettings JSON string with encryption parameters
* @param promise The promise to resolve
*/
@ReactMethod
override fun deriveKeyFromPassword(password: String, salt: String, encryptionType: String, encryptionSettings: String, promise: Promise) {
try {
val derivedKey = vaultStore.deriveKeyFromPassword(password, salt, encryptionType, encryptionSettings)
// Return as base64 string
val base64Key = android.util.Base64.encodeToString(derivedKey, android.util.Base64.NO_WRAP)
promise.resolve(base64Key)
} catch (e: Exception) {
Log.e(TAG, "Error deriving key from password", e)
promise.reject("ERR_DERIVE_KEY", "Failed to derive key from password: ${e.message}", e)
}
}
/**
* Open the autofill settings page.
* @param promise The promise to resolve

View File

@@ -6,6 +6,9 @@ import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.lambdapioneer.argon2kt.Argon2Kt
import com.lambdapioneer.argon2kt.Argon2Mode
import com.lambdapioneer.argon2kt.Argon2Version
import net.aliasvault.app.vaultstore.interfaces.CredentialOperationCallback
import net.aliasvault.app.vaultstore.interfaces.CryptoOperationCallback
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreOperationCallback
@@ -240,6 +243,43 @@ class VaultStore(
return this.storageProvider.getKeyDerivationParams()
}
/**
* Derive a key from a password using Argon2Id.
* @param password The password to derive from
* @param salt The salt to use
* @param encryptionType The type of encryption (should be "Argon2Id")
* @param encryptionSettings JSON string with encryption parameters
* @return The derived key as a ByteArray
*/
fun deriveKeyFromPassword(password: String, salt: String, encryptionType: String, encryptionSettings: String): ByteArray {
if (encryptionType != "Argon2Id") {
throw IllegalArgumentException("Unsupported encryption type: $encryptionType")
}
// Parse encryption settings JSON
val settings = JSONObject(encryptionSettings)
val iterations = settings.getInt("Iterations")
val memorySize = settings.getInt("MemorySize")
val parallelism = settings.getInt("DegreeOfParallelism")
// Create Argon2 instance
val argon2 = Argon2Kt()
// Hash the password using Argon2Id
val hashResult = argon2.hash(
mode = Argon2Mode.ARGON2_ID,
password = password.toByteArray(Charsets.UTF_8),
salt = salt.toByteArray(Charsets.UTF_8),
tCostInIterations = iterations,
mCostInKibibyte = memorySize,
parallelism = parallelism,
hashLengthInBytes = 32,
version = Argon2Version.V13,
)
return hashResult.rawHashAsByteArray()
}
/**
* Store the encrypted database in the storage provider.
* @param encryptedData The encrypted database as a base64 encoded string

View File

@@ -4,10 +4,10 @@
android:viewportWidth="500"
android:viewportHeight="500">
<group
android:scaleX="0.6"
android:scaleY="0.6"
android:translateX="100"
android:translateY="100">
android:scaleX="0.56"
android:scaleY="0.56"
android:translateX="110"
android:translateY="110">
<path
android:fillColor="#EEC170"
android:pathData="m459.87,294.95c0.016,5.4 0.032,10.801 -0.35,16.873c-1.111,6.339 -1.194,12.173 -2.635,17.649c-10.922,41.508 -36.731,69.481 -77.351,83.408c-7.216,2.474 -14.972,3.37 -22.479,4.995c-23.629,0.042 -47.257,0.115 -70.886,0.12c-46.762,0.011 -93.523,-0.014 -140.95,-0.434c-8.59,-2.002 -16.766,-2.835 -24.398,-5.333c-21.595,-7.067 -39.523,-19.656 -53.708,-37.552c-10.227,-12.903 -17.579,-27.17 -21.28,-43.221c-1.475,-6.397 -2.471,-12.904 -3.685,-19.361c-0.052,-5.747 -0.104,-11.494 0.269,-17.886c4.159,-42.973 27.68,-71.638 63.562,-92.153c0,-0.708 -0.002,-1.699 0,-2.69c0.022,-9.829 -1.307,-19.894 0.357,-29.438c3.239,-18.579 11.08,-35.272 23.763,-49.773c12.098,-13.832 26.457,-23.989 43.609,-30.029c7.813,-2.751 16.14,-4.042 24.234,-5.995c7.392,-0.026 14.784,-0.051 22.835,0.323c4.196,0.954 7.795,1.254 11.258,2.105c17.16,4.219 32.287,12.176 45.469,24.104c2.256,2.041 4.372,6.624 9.621,3.868c16.839,-8.842 34.718,-11.597 53.603,-8.594c16.791,2.67 31.602,9.431 44.236,20.636c11.531,10.227 19.84,22.841 25.393,37.236c6.344,16.445 10.389,33.163 6.08,49.389c7.959,8.932 15.807,16.704 22.421,25.414c9.162,12.065 15.33,25.746 18.144,40.776c0.97,5.185 1.911,10.375 2.865,15.563m-71.597,71.012c5.562,-5.228 12.002,-9.799 16.508,-15.817c10.474,-13.992 14.333,-29.916 11.288,-47.446c-2.25,-12.95 -8.197,-24.076 -17.243,-33.063c-12.746,-12.663 -28.865,-18.614 -46.786,-18.569c-69.912,0.177 -139.82,0.568 -209.74,0.962c-15.922,0.09 -29.168,7.421 -39.685,18.296c-14.45,14.944 -20.408,33.343 -16.655,54.368c2.276,12.754 8.217,23.748 17.158,32.66c13.299,13.255 30.097,18.653 48.728,18.651c59.321,-0.005 118.64,0.042 177.96,-0.047c9.591,-0.014 19.181,-0.866 28.773,-0.889c10.649,-0.025 19.978,-3.825 29.687,-9.107z" />

View File

@@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- AliasVault logo scaled and centered with padding -->
<group
android:translateX="23.5"
android:translateY="23.5"
android:scaleX="0.122"
android:scaleY="0.122">
<!-- Main vault shape -->
<path
android:fillColor="#000000"
android:pathData="m459.87 294.95c0.016205 5.4005 0.03241 10.801-0.35022 16.873-1.111 6.3392-1.1941 12.173-2.6351 17.649-10.922 41.508-36.731 69.481-77.351 83.408-7.2157 2.4739-14.972 3.3702-22.479 4.995-23.629 0.042205-47.257 0.11453-70.886 0.12027-46.762 0.011322-93.523-0.01416-140.95-0.43411-8.59-2.0024-16.766-2.8352-24.398-5.3326-21.595-7.0666-39.523-19.656-53.708-37.552-10.227-12.903-17.579-27.17-21.28-43.221-1.475-6.3967-2.4711-12.904-3.6852-19.361-0.051849-5.747-0.1037-11.494 0.26915-17.886 4.159-42.973 27.68-71.638 63.562-92.153 0-0.70761-0.001961-1.6988 3.12e-4 -2.69 0.022484-9.8293-1.3071-19.894 0.35664-29.438 3.2391-18.579 11.08-35.272 23.763-49.773 12.098-13.832 26.457-23.989 43.609-30.029 7.813-2.7512 16.14-4.0417 24.234-5.9948 7.392-0.025734 14.784-0.05146 22.835 0.32253 4.1959 0.95392 7.7946 1.2538 11.258 2.1053 17.16 4.2192 32.287 12.176 45.469 24.104 2.2558 2.0411 4.372 6.6241 9.621 3.868 16.839-8.8419 34.718-11.597 53.603-8.594 16.791 2.6699 31.602 9.4308 44.236 20.636 11.531 10.227 19.84 22.841 25.393 37.236 6.3436 16.445 10.389 33.163 6.0798 49.389 7.9587 8.9321 15.807 16.704 22.421 25.414 9.162 12.065 15.33 25.746 18.144 40.776 0.97046 5.1848 1.9111 10.375 2.8654 15.563m-71.597 71.012c5.5615-5.2284 12.002-9.7986 16.508-15.817 10.474-13.992 14.333-29.916 11.288-47.446-2.2496-12.95-8.1973-24.076-17.243-33.063-12.746-12.663-28.865-18.614-46.786-18.569-69.912 0.17712-139.82 0.56831-209.74 0.96176-15.922 0.089599-29.168 7.4209-39.685 18.296-14.45 14.944-20.408 33.343-16.655 54.368 2.2763 12.754 8.2167 23.748 17.158 32.66 13.299 13.255 30.097 18.653 48.728 18.651 59.321-0.005188 118.64 0.042358 177.96-0.046601 9.5912-0.014374 19.181-0.86588 28.773-0.88855 10.649-0.025146 19.978-3.825 29.687-9.1074z"/>
<!-- First dot -->
<path
android:fillColor="#000000"
android:pathData="m162.77 293c15.654 4.3883 20.627 22.967 10.304 34.98-5.3104 6.1795-14.817 8.3208-24.278 5.0472-7.0723-2.4471-12.332-10.362-12.876-17.933-1.0451-14.542 11.089-23.176 21.705-23.046 1.5794 0.019287 3.1517 0.61566 5.1461 0.95184z"/>
<!-- Second dot -->
<path
android:fillColor="#000000"
android:pathData="m227.18 293.64c7.8499 2.3973 11.938 8.2143 13.524 15.077 1.8591 8.0439-0.44817 15.706-7.1588 21.121-6.7633 5.4572-14.417 6.8794-22.578 3.1483-8.2972-3.7933-12.836-10.849-12.736-19.438 0.1687-14.497 14.13-25.368 28.948-19.908z"/>
<!-- Third dot -->
<path
android:fillColor="#000000"
android:pathData="m261.57 319.07c-2.495-14.418 4.6853-22.603 14.596-26.108 9.8945-3.4995 23.181 3.4303 26.267 13.779 4.6504 15.591-7.1651 29.064-21.665 28.161-8.5254-0.53088-17.202-6.5094-19.198-15.831z"/>
<!-- Fourth dot -->
<path
android:fillColor="#000000"
android:pathData="m336.91 333.41c-9.0175-4.2491-15.337-14.349-13.829-21.682 3.0825-14.989 13.341-20.304 23.018-19.585 10.653 0.79141 17.93 7.407 19.765 17.547 1.9588 10.824-4.1171 19.939-13.494 23.703-5.272 2.1162-10.091 1.5086-15.46 0.017883z"/>
</group>
</vector>

View File

@@ -4,7 +4,8 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical">
android:gravity="center_vertical"
android:background="@color/splashscreen_background">
<TextView
android:id="@+id/text"

View File

@@ -4,7 +4,8 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical">
android:gravity="center_vertical"
android:background="@color/splashscreen_background">
<ImageView
android:id="@+id/icon"

View File

@@ -4,7 +4,8 @@
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
android:padding="8dp"
android:background="@color/splashscreen_background">
<ImageView
android:id="@+id/icon"

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<!-- mipmap-anydpi-v26/ic_launcher_round.xml -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

View File

@@ -9,8 +9,8 @@
<string name="autofill_open_app">Ouvrir lapplication</string>
<string name="autofill_vault_locked">Coffre-fort verrouillé</string>
<!-- Biometric prompts -->
<string name="biometric_store_key_title">Store Encryption Key</string>
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
<string name="biometric_unlock_vault_title">Unlock Vault</string>
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
<string name="biometric_store_key_title">Stocker la clé de chiffrement</string>
<string name="biometric_store_key_subtitle">Authentifiez-vous pour stocker votre clé de chiffrement en toute sécurité dans le Keystore Android. Cela permet un accès sécurisé à votre coffre.</string>
<string name="biometric_unlock_vault_title">Déverrouiller le coffre</string>
<string name="biometric_unlock_vault_subtitle">Authentifiez-vous pour accéder à votre coffre</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AliasVault</string>
<string name="autofill_service_description" translatable="true">השלמה אוטומטית עם AliasVault</string>
<string name="aliasvault_icon">סמל AliasVault</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">המשיכה נכשלה, נא לפתוח את היישום</string>
<string name="autofill_no_match_found">לא נמצאו התאמות, ליצור חדש?</string>
<string name="autofill_open_app">פתיחת היישום</string>
<string name="autofill_vault_locked">הכספת נעולה</string>
<!-- Biometric prompts -->
<string name="biometric_store_key_title">אחסון מפתח הצפנה</string>
<string name="biometric_store_key_subtitle">יש לעבור אימות כדי לאחסן בבטחה את מפתח ההצפנה שלך ב־Android Keystore (אחסון מפתחות). כך מופעלת גישה מאובטחת לכספת שלך.</string>
<string name="biometric_unlock_vault_title">שחרור נעילת כספת</string>
<string name="biometric_unlock_vault_subtitle">יש לעבור אימות כדי לגשת לכספת שלך</string>
</resources>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">AliasVault</string>
<string name="autofill_service_description" translatable="true">AliasVault AutoFill</string>
<string name="aliasvault_icon">AliasVault icon</string>
<!-- AutofillService strings -->
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
<string name="autofill_no_match_found">No match found, create new?</string>
<string name="autofill_open_app">Open app</string>
<string name="autofill_vault_locked">Vault locked</string>
<!-- Biometric prompts -->
<string name="biometric_store_key_title">Store Encryption Key</string>
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
<string name="biometric_unlock_vault_title">Unlock Vault</string>
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
</resources>

View File

@@ -9,8 +9,8 @@
<string name="autofill_open_app">Открыть приложение</string>
<string name="autofill_vault_locked">Хранилище заблокировано</string>
<!-- Biometric prompts -->
<string name="biometric_store_key_title">Store Encryption Key</string>
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
<string name="biometric_unlock_vault_title">Unlock Vault</string>
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
<string name="biometric_store_key_title">Храните ключ шифрования</string>
<string name="biometric_store_key_subtitle">Пройдите аутентификацию, чтобы надежно сохранить свой ключ шифрования в хранилище ключей Android. Это обеспечивает безопасный доступ к вашему хранилищу.</string>
<string name="biometric_unlock_vault_title">Разблокировать хранилище</string>
<string name="biometric_unlock_vault_subtitle">Пройдите проверку подлинности, чтобы получить доступ к вашему хранилищу</string>
</resources>

View File

@@ -1,4 +1,5 @@
<resources>
<color name="splashscreen_background">#ffffff</color>
<color name="colorPrimary">#023c69</color>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -3,7 +3,9 @@
<locale android:name="de" />
<locale android:name="en" />
<locale android:name="fi" />
<locale android:name="he" />
<locale android:name="it" />
<locale android:name="nl" />
<locale android:name="uk" />
<locale android:name="zh" />
</locale-config>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system"/>
<certificates src="user"/> <!-- opt-in to user-installed CAs -->
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -6,10 +6,10 @@ buildscript {
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '30')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
kotlinVersion = findProperty('android.kotlinVersion') ?: '2.0.21'
detektVersion = '1.23.5'
ndkVersion = "26.1.10909125"
ndkVersion = "27.1.12297006"
}
repositories {
gradlePluginPortal()

View File

@@ -56,5 +56,4 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
expo.useLegacyPackaging=false
# Workaround for Expo modules compatibility with Android Gradle Plugin 8.x
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false

View File

Binary file not shown.

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