mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 14:48:04 -05:00
Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44c16f4cd1 | ||
|
|
4bf103d261 | ||
|
|
68b19b9545 | ||
|
|
e6d51ca1b1 | ||
|
|
13e7f1ddd9 | ||
|
|
e5d342b961 | ||
|
|
a58426abcb | ||
|
|
819385bc0a | ||
|
|
c0cbc0be7b | ||
|
|
40686f97e0 | ||
|
|
f10fb989ce | ||
|
|
ca85c04c75 | ||
|
|
fd9eb9d653 | ||
|
|
0a70902d69 | ||
|
|
eee41df9a4 | ||
|
|
d563d6d448 | ||
|
|
db1474397c | ||
|
|
e881f9486a | ||
|
|
645fd605e6 | ||
|
|
254f0a1212 | ||
|
|
64d29ebcd4 | ||
|
|
df0d74595f | ||
|
|
2131e4922c | ||
|
|
d846825b84 | ||
|
|
2a902eeb97 | ||
|
|
d9a6dfab03 | ||
|
|
3da99ed4b1 | ||
|
|
5414f40c98 | ||
|
|
6c561e8ece | ||
|
|
3654b12cd7 | ||
|
|
266e7b36d4 | ||
|
|
cbe9978367 | ||
|
|
6b949bcb2f | ||
|
|
6a4fbb9193 | ||
|
|
c459a48927 | ||
|
|
d3f132df63 | ||
|
|
b5edc6ef76 | ||
|
|
4e0db87bc3 | ||
|
|
62cc0e7c2b | ||
|
|
dad3a6fa2c | ||
|
|
9560d550e4 | ||
|
|
0930ae03cd | ||
|
|
23c9bf2fc9 | ||
|
|
6ebaf8e1b8 | ||
|
|
aa630984e3 | ||
|
|
b894338869 | ||
|
|
d7ec6583f0 | ||
|
|
836fbc1941 | ||
|
|
c531096a98 | ||
|
|
b78a757728 | ||
|
|
f676fba980 | ||
|
|
003e3e4d1d | ||
|
|
637362856a | ||
|
|
b855896108 | ||
|
|
a92bbef41a | ||
|
|
dccbda7515 | ||
|
|
a45a468e35 | ||
|
|
97dc5f3570 | ||
|
|
425a977af9 | ||
|
|
30635d9714 | ||
|
|
cb2aa833bc | ||
|
|
f7b66ed307 | ||
|
|
85e33a9fcd | ||
|
|
51dc4d2844 | ||
|
|
b1d12af7dd | ||
|
|
ae4fc13330 | ||
|
|
e1c5b5f753 | ||
|
|
9ff7c6c23b | ||
|
|
40fdb4e21a | ||
|
|
72254f38ff | ||
|
|
274cb70d4b | ||
|
|
a30e68e0f8 | ||
|
|
fe0678f217 | ||
|
|
aab69ab1b4 | ||
|
|
02575d7366 | ||
|
|
b218ebf407 | ||
|
|
2043e94a91 | ||
|
|
e6bc3ea652 | ||
|
|
92b072868e | ||
|
|
aab7b475cc | ||
|
|
1e75d3806b | ||
|
|
e9bd073bac | ||
|
|
da496b31a1 | ||
|
|
2e34e64c6c | ||
|
|
0da8661d6c | ||
|
|
1797ed9ec6 | ||
|
|
4d613175ed | ||
|
|
a937098315 | ||
|
|
c3be660c1e | ||
|
|
9b622c8fb4 | ||
|
|
986c028d82 | ||
|
|
428c715ec2 | ||
|
|
4ae8839d9b | ||
|
|
a199b9e8da | ||
|
|
ae7eb2ca1a | ||
|
|
06b510c496 | ||
|
|
020e83d40f | ||
|
|
3b14bbcca4 | ||
|
|
e97bf6d168 | ||
|
|
76b829eb3d | ||
|
|
07b6097d31 | ||
|
|
81b6479682 | ||
|
|
9016a4b0b8 | ||
|
|
786bf655d0 | ||
|
|
bdfea51319 | ||
|
|
8ce636a5c1 | ||
|
|
9d4ceff4ba | ||
|
|
d562b183c5 | ||
|
|
3e7848bb3b | ||
|
|
e4614c8034 | ||
|
|
c404fa807f | ||
|
|
fa366cf2e6 | ||
|
|
3653ec3d55 | ||
|
|
4d74504882 | ||
|
|
29c7644b53 | ||
|
|
648fe0598d | ||
|
|
2a3a35f562 | ||
|
|
359f911057 | ||
|
|
267f2d3d17 | ||
|
|
80abfecd2e | ||
|
|
42524d1412 | ||
|
|
81750c4878 | ||
|
|
5c9d9c6933 | ||
|
|
ec8cb7836a | ||
|
|
a64f7d97e5 | ||
|
|
32fe2156f1 | ||
|
|
6aa43bb1a2 | ||
|
|
f9d7918e0a | ||
|
|
076060e7f3 | ||
|
|
4d7d061e07 | ||
|
|
582ab7d20a | ||
|
|
bcd1353cf7 | ||
|
|
eaa348bb23 | ||
|
|
0db3e2dbf4 | ||
|
|
728af0bff6 | ||
|
|
7923c16c51 | ||
|
|
18a5e062a5 | ||
|
|
1097218ee1 | ||
|
|
0a8722226b | ||
|
|
63cc511a9f | ||
|
|
5367c5eb34 | ||
|
|
f7b0084eba | ||
|
|
09d4ba46fa | ||
|
|
fb33e688df | ||
|
|
9017d0b642 | ||
|
|
f50fe913fb | ||
|
|
7b78552651 | ||
|
|
e7d7d9fe54 | ||
|
|
fdfe4b0aa8 | ||
|
|
6b2737eec5 | ||
|
|
79f1bca7a2 | ||
|
|
224e4ee741 | ||
|
|
9a453a1fab | ||
|
|
4cb7966492 | ||
|
|
dbfee0f5b6 | ||
|
|
94bad91411 | ||
|
|
9dc9ed9ba1 | ||
|
|
686ea56556 | ||
|
|
73f95b3a77 | ||
|
|
198fc57d93 | ||
|
|
fd64ea8647 | ||
|
|
4b9e2ba2e3 | ||
|
|
e849762985 | ||
|
|
868e708957 | ||
|
|
49fa36eedb | ||
|
|
f049399d9e | ||
|
|
b00e7c3ac5 | ||
|
|
31c7832745 | ||
|
|
3cc8c9f5de | ||
|
|
ccf923bc98 | ||
|
|
039e63f5c8 | ||
|
|
52b60e07d2 | ||
|
|
95a5391589 | ||
|
|
c8277be56f | ||
|
|
66115496fb | ||
|
|
6f89be6980 | ||
|
|
da36af15ae | ||
|
|
aa218f4f8f | ||
|
|
558d39ec96 | ||
|
|
4b59776b86 | ||
|
|
4a0c6d9499 | ||
|
|
f2bd892a5b | ||
|
|
dd1d6e64e1 | ||
|
|
73ae2a7b62 | ||
|
|
d9c914d09e | ||
|
|
74fd6c1656 | ||
|
|
f4cd3ae87f | ||
|
|
563941f913 | ||
|
|
1751a4c242 | ||
|
|
7b6170e927 | ||
|
|
e5ed8d380f | ||
|
|
30f03884c8 | ||
|
|
0ddd24c40e | ||
|
|
232245fd76 | ||
|
|
bb1549458f | ||
|
|
c63b7ceac4 | ||
|
|
987de6625f | ||
|
|
9efe878397 | ||
|
|
ec90890870 | ||
|
|
bdc405a836 | ||
|
|
27e411f485 | ||
|
|
108ec1869c | ||
|
|
e1b05b611e | ||
|
|
7d2630e197 | ||
|
|
9df5f6c81a | ||
|
|
93adb6d60f | ||
|
|
6abce9e9cf | ||
|
|
534d82990d | ||
|
|
fb28827f15 | ||
|
|
b14f22f9ad | ||
|
|
d5dee592ab | ||
|
|
b0df4c410a | ||
|
|
f09ce7ffcf | ||
|
|
b6609706e8 | ||
|
|
19620bff8e | ||
|
|
9da243fdac |
10
.env.example
10
.env.example
@@ -37,11 +37,19 @@ FORCE_HTTPS_REDIRECT=true
|
||||
# your DNS. Please refer to the full documentation for more instructions on DNS:
|
||||
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup
|
||||
#
|
||||
# Set the private email domains below that are allowed to be used (comma separated values).
|
||||
# Set the private email domains below that the server should accept incoming mail for (comma separated values).
|
||||
# Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
|
||||
# To disable the private email domains feature, keep this empty.
|
||||
PRIVATE_EMAIL_DOMAINS=
|
||||
|
||||
# Set private email domains that should be hidden from UI components (comma separated values).
|
||||
# These domains will still function as private email domains for receiving email and claims,
|
||||
# but will not appear in domain selection dropdowns or settings. This is useful for deprecating
|
||||
# legacy domains while maintaining backwards compatibility.
|
||||
# Example: HIDDEN_PRIVATE_EMAIL_DOMAINS=old-domain.com,deprecated.org
|
||||
# Note: Domains listed here should ALSO be included in PRIVATE_EMAIL_DOMAINS above.
|
||||
HIDDEN_PRIVATE_EMAIL_DOMAINS=
|
||||
|
||||
# Enable TLS for SMTP.
|
||||
# ⚠️ Requires valid TLS certificates on your mail server (not provided by the AliasVault installer).
|
||||
# If set to true without proper certificates, the SMTP service will fail to start.
|
||||
|
||||
3
.github/workflows/dotnet-e2e-tests.yml
vendored
3
.github/workflows/dotnet-e2e-tests.yml
vendored
@@ -24,6 +24,7 @@ jobs:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: apps/server
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build
|
||||
@@ -67,6 +68,7 @@ jobs:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: apps/server
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build
|
||||
@@ -104,6 +106,7 @@ jobs:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: apps/server
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build server
|
||||
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: apps/server
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Build
|
||||
|
||||
1
.github/workflows/dotnet-unit-tests.yml
vendored
1
.github/workflows/dotnet-unit-tests.yml
vendored
@@ -23,6 +23,7 @@ jobs:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: apps/server
|
||||
run: dotnet workload install wasm-tools
|
||||
|
||||
- name: Restore dependencies
|
||||
|
||||
3
.vscode/AliasVault.code-workspace
vendored
3
.vscode/AliasVault.code-workspace
vendored
@@ -24,6 +24,7 @@
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"java.configuration.updateBuildConfiguration": "disabled"
|
||||
"java.configuration.updateBuildConfiguration": "disabled",
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
This document provides a high-level overview of the AliasVault architecture, focusing on the encryption algorithms used to ensure the security of user data.
|
||||
|
||||
## Overview
|
||||
AliasVault features a [zero-knowledge architecture](https://en.wikipedia.org/wiki/Zero-knowledge_service) and uses a combination of encryption algorithms to protect the data of its users.
|
||||
AliasVault implements zero-knowledge encryption using a combination of encryption algorithms to protect the privacy of its users.
|
||||
|
||||
The basic premise is that the master password chosen by the user upon registration forms the basis for all encryption
|
||||
and decryption operations. This master password is never transmitted over the network and only resides on the client.
|
||||
All data is encrypted at rest and in transit. This ensures that even if the AliasVault servers are compromised,
|
||||
the user's data remains secure.
|
||||
The basic premise is that the master password chosen by the user upon registration forms the basis for all encryption and decryption operations. This master password is never transmitted over the network and only resides on the client.
|
||||
|
||||
### What is Zero-Knowledge Encrypted
|
||||
- **Vault Data**: Your entire vault (passwords, usernames, notes, passkeys, email addresses, etc.) is fully encrypted client-side before being sent to the server. The server cannot decrypt any vault contents.
|
||||
- **Email Contents**: When emails are received by the server, their contents are immediately encrypted with your public key (from your vault) before being saved. Only you can decrypt and read these emails with your private key.
|
||||
|
||||
This ensures that even if the AliasVault servers are compromised, vault contents and email messages remain secure and unreadable.
|
||||
|
||||
## Encryption algorithms
|
||||
The following encryption algorithms and standards are used by AliasVault:
|
||||
@@ -20,6 +23,7 @@ The following encryption algorithms and standards are used by AliasVault:
|
||||
### Additional Features
|
||||
- [RSA-OAEP](#rsa-oaep) - Email encryption
|
||||
- [Passkeys (WebAuthn)](#passkeys-webauthn) - Passwordless authentication
|
||||
- [Login with Mobile](#login-with-mobile) - Unlock vault in web app / browser extension via mobile app
|
||||
|
||||
Below is a detailed explanation of each encryption algorithm and standard.
|
||||
|
||||
@@ -133,3 +137,31 @@ All implementations follow the WebAuthn Level 2 specification and use:
|
||||
- Eliminates phishing risks through cryptographic domain binding
|
||||
|
||||
More information about WebAuthn can be found on the [WebAuthn specification](https://www.w3.org/TR/webauthn-2/) page.
|
||||
|
||||
### Login with Mobile
|
||||
AliasVault provides a secure "Login with Mobile" feature that allows users to unlock their vault on web browsers or browser extensions by scanning a QR code with their authenticated mobile app. This convenient authentication method maintains zero-knowledge security through hybrid encryption.
|
||||
|
||||
#### Implementation Details
|
||||
The mobile login system combines RSA-2048 asymmetric encryption with AES-256-GCM symmetric encryption:
|
||||
|
||||
1. **Initiation**: Browser/extension client generates an RSA-2048 key pair locally and sends the public key to the server, which returns a unique request ID displayed as a QR code.
|
||||
|
||||
2. **Authorization**: Mobile app scans the QR code, encrypts the user's vault decryption key with the RSA public key, and sends it to the server.
|
||||
|
||||
3. **Retrieval**: Browser client polls the server for completion. When ready, the server:
|
||||
- Generates fresh JWT tokens for the session
|
||||
- Creates a random AES-256 symmetric key
|
||||
- Encrypts tokens and username with the symmetric key
|
||||
- Encrypts the symmetric key with the client's RSA public key
|
||||
- Returns encrypted data and immediately purges it from the database
|
||||
|
||||
4. **Decryption**: Client uses its RSA private key to decrypt the symmetric key, then uses the symmetric key to decrypt tokens and username, and the RSA private key to decrypt the vault decryption key.
|
||||
|
||||
#### Security Properties
|
||||
- **Zero-Knowledge**: Server never accesses the vault decryption key in plaintext
|
||||
- **One-Time Use**: Requests cannot be retrieved twice; data is immediately cleared after retrieval
|
||||
- **Automatic Expiration**: Unfulfilled requests expire after 2 minutes client-side (3 minutes server-side); fulfilled but unretrieved requests auto-delete within 24 hours
|
||||
- **MITM Protection**: Only the client with the RSA private key can decrypt the response
|
||||
- **Limited Attack Surface**: Short timeout window minimizes QR code interception risks
|
||||
|
||||
More information about the mobile login flow can be found in the [Architecture Documentation](https://docs.aliasvault.net/architecture/#6-login-with-mobile).
|
||||
|
||||
@@ -28,11 +28,22 @@ Help grow the AliasVault community by:
|
||||
|
||||
Help make AliasVault accessible to users worldwide by contributing translations! AliasVault is currently available in English and Dutch, but we're looking for volunteers to help translate it into other languages such as German, French, Spanish, Ukrainian, Italian, and more.
|
||||
|
||||
AliasVault translations are managed through [Crowdin](https://crowdin.com/), an online translation platform. If you’d like to help translate AliasVault into your native language, please [request access to the Crowdin project](https://crowdin.com/project/aliasvault).
|
||||
### UI Translations
|
||||
|
||||
If you're willing to help, we also encourage you to get in contact via [Discord](https://discord.gg/DsaXMTEtpF) to chat (quickest), or contact us via email at [contact@support.aliasvault.net](mailto:contact@support.aliasvault.net) to discuss the language(s) you are willing to contribute to, and so we can answer any technical questions you might have.
|
||||
AliasVault UI translations are managed through [Crowdin](https://crowdin.com/), an online translation platform. If you'd like to help translate AliasVault into your native language, please [request access to the Crowdin project](https://crowdin.com/project/aliasvault).
|
||||
|
||||
Your translation contributions will help make AliasVault more accessible to privacy-conscious users around the world!
|
||||
You can also get in contact via [Discord](https://discord.gg/DsaXMTEtpF) to chat, or via email at [contact@support.aliasvault.net](mailto:contact@support.aliasvault.net) to discuss the language(s) you are willing to contribute to, and so we can answer any technical questions you might have.
|
||||
|
||||
### Identity Generator Translations
|
||||
|
||||
In AliasVault, when creating a new credential AliasVault automatically generates realistic alias identities including: first names, last names and birthdates. For this AliasVault uses dictionaries of possible names per language. You can help to enable AliasVault to generate proper identities in your language too.
|
||||
|
||||
**How to help:**
|
||||
- Create lists of common first names (male and female)
|
||||
- Create a list of common last names (surnames)
|
||||
- Optionally: Decade-specific names for more authentic generations
|
||||
|
||||
Read the specific instructions on how to contribute here: [Identity Generator Translations](https://docs.aliasvault.net/contributing/identity-generator.html).
|
||||
|
||||
## 3. Contributing to the Documentation
|
||||
|
||||
|
||||
@@ -126,8 +126,9 @@ Core features that are being worked on:
|
||||
- [x] Android native app
|
||||
- [x] Editing in browser extension
|
||||
- [x] Multi-language support across all client applications
|
||||
- [x] Passkeys
|
||||
- [ ] Data model and usability improvements (more flexible aliases and credential types, folder support, bulk selecting etc.)
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys and passkeys
|
||||
- [ ] Support for FIDO2/WebAuthn hardware keys
|
||||
- [ ] Adding support for family/team sharing (organization features)
|
||||
|
||||
👉 [View the full AliasVault roadmap here](https://github.com/aliasvault/aliasvault/issues/731)
|
||||
|
||||
@@ -1 +1 @@
|
||||
24
|
||||
26
|
||||
|
||||
@@ -1 +1 @@
|
||||
|
||||
-alpha
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.24.0
|
||||
0.26.0-alpha
|
||||
|
||||
2
apps/browser-extension/.gitignore
vendored
2
apps/browser-extension/.gitignore
vendored
@@ -17,8 +17,6 @@ stats-*.json
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
|
||||
7
apps/browser-extension/.vscode/settings.json
vendored
Normal file
7
apps/browser-extension/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/i18n",
|
||||
"src/i18n/locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
}
|
||||
301
apps/browser-extension/package-lock.json
generated
301
apps/browser-extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.24.0",
|
||||
"version": "0.26.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aliasvault-browser-extension",
|
||||
"version": "0.24.0",
|
||||
"version": "0.26.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
@@ -15,6 +15,7 @@
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
@@ -30,6 +31,7 @@
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
@@ -227,6 +229,7 @@
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -605,6 +608,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -628,6 +632,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2116,12 +2121,23 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qrcode": {
|
||||
"version": "1.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
|
||||
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz",
|
||||
"integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -2203,6 +2219,7 @@
|
||||
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.32.1",
|
||||
"@typescript-eslint/types": "8.32.1",
|
||||
@@ -2831,6 +2848,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2973,7 +2991,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -3539,6 +3556,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
@@ -4101,7 +4119,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -4114,7 +4131,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
@@ -4428,6 +4444,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
|
||||
@@ -4571,6 +4596,12 @@
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dlv": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||
@@ -5081,6 +5112,7 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5251,6 +5283,7 @@
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
@@ -6000,7 +6033,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
@@ -6115,9 +6147,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -6454,6 +6486,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
@@ -6810,7 +6843,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -7310,6 +7342,7 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -7322,9 +7355,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7356,6 +7389,7 @@
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@@ -8229,9 +8263,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz",
|
||||
"integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==",
|
||||
"dev": true,
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
@@ -8701,6 +8735,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz",
|
||||
@@ -8830,7 +8873,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -8990,6 +9032,15 @@
|
||||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
@@ -9019,6 +9070,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -9291,6 +9343,191 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
|
||||
@@ -9385,6 +9622,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -9394,6 +9632,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -9406,6 +9645,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
|
||||
"integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@@ -9647,12 +9887,17 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -9750,6 +9995,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
|
||||
"integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.7"
|
||||
},
|
||||
@@ -9976,6 +10222,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
@@ -10876,6 +11128,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11154,6 +11407,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -11301,6 +11555,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.2.2"
|
||||
},
|
||||
@@ -11428,6 +11683,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@@ -11558,6 +11814,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -11570,6 +11827,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz",
|
||||
"integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.1.3",
|
||||
"@vitest/mocker": "3.1.3",
|
||||
@@ -11900,6 +12158,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/which-typed-array": {
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||
@@ -12135,6 +12399,7 @@
|
||||
"integrity": "sha512-DqqHc/5COs8GR21ii99bANXf/mu6zuDpiXFV1YKNsqO5/PvkrCx5arY0aVPL5IATsuacAnNzdj4eMc1qbzS53Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@1natsu/wait-element": "^4.1.2",
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.24.0",
|
||||
"version": "0.26.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:chrome": "wxt -b chrome",
|
||||
@@ -32,6 +32,7 @@
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
@@ -47,6 +48,7 @@
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
|
||||
@@ -463,7 +463,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2400902;
|
||||
CURRENT_PROJECT_VERSION = 2600100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -476,7 +476,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.24.0;
|
||||
MARKETING_VERSION = 0.26.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -495,7 +495,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2400902;
|
||||
CURRENT_PROJECT_VERSION = 2600100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -508,7 +508,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.24.0;
|
||||
MARKETING_VERSION = 0.26.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -529,9 +529,10 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 2400902;
|
||||
CURRENT_PROJECT_VERSION = 2600100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -546,7 +547,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.24.0;
|
||||
MARKETING_VERSION = 0.26.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -570,7 +571,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 2400902;
|
||||
CURRENT_PROJECT_VERSION = 2600100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -585,7 +586,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.24.0;
|
||||
MARKETING_VERSION = 0.26.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -106,13 +106,23 @@ if [[ $CHOICE == "1" || $CHOICE == "2" ]]; then
|
||||
|
||||
# Export .pkg
|
||||
rm -rf "$EXPORT_DIR"
|
||||
xcodebuild -exportArchive \
|
||||
if ! xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportOptionsPlist "$EXPORT_PLIST" \
|
||||
-exportPath "$EXPORT_DIR" \
|
||||
-allowProvisioningUpdates
|
||||
-allowProvisioningUpdates; then
|
||||
echo "❌ Failed to export archive to PKG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKG_PATH=$(ls "$EXPORT_DIR"/*.pkg)
|
||||
PKG_PATH=$(ls "$EXPORT_DIR"/*.pkg 2>/dev/null)
|
||||
|
||||
if [ -z "$PKG_PATH" ]; then
|
||||
echo "❌ No PKG file found in $EXPORT_DIR after export"
|
||||
echo "Contents of export directory:"
|
||||
ls -la "$EXPORT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version info from newly built PKG
|
||||
extract_version_info "$PKG_PATH"
|
||||
@@ -157,10 +167,18 @@ fi
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "Submitting to App Store:"
|
||||
echo " PKG Path: $PKG_PATH"
|
||||
echo " Version: $VERSION"
|
||||
echo " Build: $BUILD"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Validate PKG_PATH is set and file exists
|
||||
if [ -z "$PKG_PATH" ] || [ ! -f "$PKG_PATH" ]; then
|
||||
echo "❌ Error: PKG file not found at: $PKG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
read -p "Are you sure you want to push this to App Store? (y/n): " -r
|
||||
echo ""
|
||||
|
||||
@@ -171,9 +189,4 @@ fi
|
||||
|
||||
echo "✅ Proceeding with upload..."
|
||||
|
||||
fastlane deliver \
|
||||
--pkg "$PKG_PATH" \
|
||||
--skip_screenshots \
|
||||
--skip_metadata \
|
||||
--api_key_path "$API_KEY_PATH" \
|
||||
--run_precheck_before_submit=false
|
||||
fastlane deliver --pkg "$PKG_PATH" --skip_screenshots --skip_metadata --api_key_path "$API_KEY_PATH" --run_precheck_before_submit false
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>app-store</string>
|
||||
<string>app-store-connect</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>destination</key>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardCl
|
||||
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
|
||||
import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handlePasskeyPopupResponse, handleGetRequestData } from '@/entrypoints/background/PasskeyHandler';
|
||||
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 { handleCheckAuthStatus, handleClearPersistedFormValues, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetFilteredCredentials, handleGetSearchCredentials, 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';
|
||||
import { EncryptionKeyDerivationParams } from "@/utils/dist/shared/models/metadata";
|
||||
@@ -28,6 +28,8 @@ export default defineBackground({
|
||||
onMessage('GET_ENCRYPTION_KEY_DERIVATION_PARAMS', () => handleGetEncryptionKeyDerivationParams());
|
||||
onMessage('GET_VAULT', () => handleGetVault());
|
||||
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
|
||||
onMessage('GET_FILTERED_CREDENTIALS', ({ data }) => handleGetFilteredCredentials(data as { currentUrl: string, pageTitle: string, matchingMode?: string }));
|
||||
onMessage('GET_SEARCH_CREDENTIALS', ({ data }) => handleGetSearchCredentials(data as { searchTerm: string }));
|
||||
|
||||
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
|
||||
onMessage('GET_DEFAULT_IDENTITY_SETTINGS', () => handleGetDefaultIdentitySettings());
|
||||
@@ -40,7 +42,6 @@ export default defineBackground({
|
||||
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
|
||||
onMessage('UPLOAD_VAULT', ({ data }) => handleUploadVault(data));
|
||||
onMessage('SYNC_VAULT', () => handleSyncVault());
|
||||
|
||||
onMessage('CLEAR_VAULT', () => handleClearVault());
|
||||
|
||||
onMessage('OPEN_POPUP', () => handleOpenPopup());
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
|
||||
import { handleGetEncryptionKey } from '@/entrypoints/background/VaultMessageHandler';
|
||||
|
||||
import {
|
||||
PASSKEY_PROVIDER_ENABLED_KEY,
|
||||
PASSKEY_DISABLED_SITES_KEY
|
||||
} from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import { EncryptionUtility } from '@/utils/EncryptionUtility';
|
||||
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
|
||||
import type {
|
||||
PasskeyPopupResponse,
|
||||
WebAuthnCreateRequest,
|
||||
@@ -20,6 +23,7 @@ import type {
|
||||
WebAuthnCreationPayload,
|
||||
WebAuthnPublicKeyGetPayload
|
||||
} from '@/utils/passkey/types';
|
||||
import { SqliteClient } from '@/utils/SqliteClient';
|
||||
|
||||
import { browser, storage } from '#imports';
|
||||
|
||||
@@ -124,9 +128,29 @@ export async function handleWebAuthnCreate(data: any): Promise<any> {
|
||||
* Note: Passkey retrieval is now handled in the popup via SqliteClient
|
||||
*/
|
||||
export async function handleWebAuthnGet(data: any): Promise<any> {
|
||||
const { publicKey, origin } = data as WebAuthnGetRequest;
|
||||
const { publicKey, origin, isAutomaticRequest } = data as WebAuthnGetRequest;
|
||||
const requestId = Math.random().toString(36).substr(2, 9);
|
||||
|
||||
/*
|
||||
* If this is an automatic request (within 2 seconds of page load), check if we have matching credentials
|
||||
* before opening the popup. This prevents AliasVault from blocking other password managers when we
|
||||
* don't have the passkey they need.
|
||||
*/
|
||||
if (isAutomaticRequest) {
|
||||
try {
|
||||
// Check if we have any matching passkeys in storage
|
||||
const hasMatchingPasskeys = await checkForMatchingPasskeys(publicKey, origin);
|
||||
|
||||
if (!hasMatchingPasskeys) {
|
||||
// No matching passkeys - don't intercept, let other password managers handle it
|
||||
return { fallback: true };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for matching passkeys:', error);
|
||||
// On error, fall back to showing the popup (better UX than silently failing)
|
||||
}
|
||||
}
|
||||
|
||||
// Store request data temporarily (to avoid URL length limits)
|
||||
const requestData: PendingPasskeyGetRequest = {
|
||||
type: 'get',
|
||||
@@ -178,6 +202,66 @@ export async function handleWebAuthnGet(data: any): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have any matching passkeys for the given request.
|
||||
* This is used to determine if we should intercept automatic passkey requests.
|
||||
*/
|
||||
async function checkForMatchingPasskeys(publicKey: any, origin: string): Promise<boolean> {
|
||||
try {
|
||||
// Check if vault is unlocked
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!encryptedVault || !encryptionKey) {
|
||||
/*
|
||||
* Vault is locked - we can't check for passkeys
|
||||
* In this case, we return false to avoid intercepting
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decrypt and load the vault
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
encryptionKey
|
||||
);
|
||||
const sqliteClient = new SqliteClient();
|
||||
await sqliteClient.initializeFromBase64(decryptedVault);
|
||||
|
||||
// Get the rpId from the request or derive from origin
|
||||
const rpId = publicKey.rpId || new URL(origin).hostname;
|
||||
|
||||
// Get passkeys for this rpId
|
||||
const passkeys = sqliteClient.getPasskeysByRpId(rpId);
|
||||
|
||||
// If allowCredentials is specified, filter by those specific credentials
|
||||
if (publicKey.allowCredentials && publicKey.allowCredentials.length > 0) {
|
||||
// Convert the RP's base64url credential IDs to GUIDs for comparison
|
||||
const allowedGuids = new Set(
|
||||
publicKey.allowCredentials.map((c: any) => {
|
||||
try {
|
||||
return PasskeyHelper.base64urlToGuid(c.id);
|
||||
} catch (e) {
|
||||
console.warn('Failed to convert credential ID to GUID:', c.id, e);
|
||||
return null;
|
||||
}
|
||||
}).filter((id: string | null): id is string => id !== null)
|
||||
);
|
||||
|
||||
// Check if we have any of the allowed credentials
|
||||
const matchingPasskeys = passkeys.filter(pk => allowedGuids.has(pk.Id));
|
||||
return matchingPasskeys.length > 0;
|
||||
}
|
||||
|
||||
// No allowCredentials specified - just check if we have any passkeys for this rpId
|
||||
return passkeys.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error in checkForMatchingPasskeys:', error);
|
||||
// On error, return false to avoid intercepting
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle response from passkey popup
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,21 @@ import { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
import { t } from '@/i18n/StandaloneI18n';
|
||||
|
||||
/**
|
||||
* Cache for the SqliteClient to avoid repeated decryption and initialization.
|
||||
* The cached instance is the single source of truth for the in-memory vault.
|
||||
*
|
||||
* Cache Strategy:
|
||||
* - Local mutations (createCredential, etc.): Work directly on cachedSqliteClient, no cache clearing
|
||||
* - New vault from remote (login, sync): Clear cache by setting both to null
|
||||
* - Logout/clear vault: Clear cache by setting both to null
|
||||
*
|
||||
* The cache is cleared by setting cachedSqliteClient and cachedVaultBlob to null directly
|
||||
* in the functions that receive new vault data from external sources.
|
||||
*/
|
||||
let cachedSqliteClient: SqliteClient | null = null;
|
||||
let cachedVaultBlob: string | null = null;
|
||||
|
||||
/**
|
||||
* Check if the user is logged in and if the vault is locked, and also check for pending migrations.
|
||||
*/
|
||||
@@ -25,9 +40,10 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
|
||||
const username = await storage.getItem('local:username');
|
||||
const accessToken = await storage.getItem('local:accessToken');
|
||||
const vaultData = await storage.getItem('session:encryptedVault');
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
const isLoggedIn = username !== null && accessToken !== null;
|
||||
const isVaultLocked = isLoggedIn && vaultData === null;
|
||||
const isVaultLocked = isLoggedIn && (vaultData === null || encryptionKey === null);
|
||||
|
||||
// If vault is locked, we can't check for pending migrations
|
||||
if (isVaultLocked) {
|
||||
@@ -57,8 +73,6 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i
|
||||
hasPendingMigrations
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking pending migrations:', error);
|
||||
|
||||
// If it's a version incompatibility error, we need to handle it specially
|
||||
if (error instanceof VaultVersionIncompatibleError) {
|
||||
// Return the error so the UI can handle it appropriately (logout user)
|
||||
@@ -91,6 +105,10 @@ export async function handleStoreVault(
|
||||
// Store new encrypted vault in session storage.
|
||||
await storage.setItem('session:encryptedVault', vaultRequest.vaultBlob);
|
||||
|
||||
// Clear cached client since we received a new vault blob from external source
|
||||
cachedSqliteClient = null;
|
||||
cachedVaultBlob = null;
|
||||
|
||||
/*
|
||||
* For all other values, check if they have a value and store them in session storage if they do.
|
||||
* Some updates, e.g. when mutating local database, these values will not be set.
|
||||
@@ -104,6 +122,10 @@ export async function handleStoreVault(
|
||||
await storage.setItem('session:privateEmailDomains', vaultRequest.privateEmailDomainList);
|
||||
}
|
||||
|
||||
if (vaultRequest.hiddenPrivateEmailDomainList) {
|
||||
await storage.setItem('session:hiddenPrivateEmailDomains', vaultRequest.hiddenPrivateEmailDomainList);
|
||||
}
|
||||
|
||||
if (vaultRequest.vaultRevisionNumber) {
|
||||
await storage.setItem('session:vaultRevisionNumber', vaultRequest.vaultRevisionNumber);
|
||||
}
|
||||
@@ -126,7 +148,7 @@ export async function handleStoreEncryptionKey(
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to store encryption key:', error);
|
||||
return { success: false, error: await t('common.errors.failedToStoreEncryptionKey') };
|
||||
return { success: false, error: await t('common.errors.unknownErrorTryAgain') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +163,7 @@ export async function handleStoreEncryptionKeyDerivationParams(
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to store encryption key derivation params:', error);
|
||||
return { success: false, error: await t('common.errors.failedToStoreEncryptionParams') };
|
||||
return { success: false, error: await t('common.errors.unknownErrorTryAgain') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +172,7 @@ export async function handleStoreEncryptionKeyDerivationParams(
|
||||
*/
|
||||
export async function handleSyncVault(
|
||||
) : Promise<messageBoolResponse> {
|
||||
const webApi = new WebApiService(() => {});
|
||||
const webApi = new WebApiService();
|
||||
const statusResponse = await webApi.getStatus();
|
||||
const statusError = webApi.validateStatusResponse(statusResponse);
|
||||
if (statusError !== null) {
|
||||
@@ -167,8 +189,13 @@ export async function handleSyncVault(
|
||||
{ key: 'session:encryptedVault', value: vaultResponse.vault.blob },
|
||||
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
|
||||
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
|
||||
{ key: 'session:hiddenPrivateEmailDomains', value: vaultResponse.vault.hiddenPrivateEmailDomainList },
|
||||
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber }
|
||||
]);
|
||||
|
||||
// Clear cached client since we received a new vault blob from server
|
||||
cachedSqliteClient = null;
|
||||
cachedVaultBlob = null;
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
@@ -185,6 +212,7 @@ export async function handleGetVault(
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const hiddenPrivateEmailDomains = await storage.getItem('session:hiddenPrivateEmailDomains') as string[] ?? [];
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
if (!encryptedVault) {
|
||||
@@ -207,11 +235,12 @@ export async function handleGetVault(
|
||||
vault: decryptedVault,
|
||||
publicEmailDomains: publicEmailDomains ?? [],
|
||||
privateEmailDomains: privateEmailDomains ?? [],
|
||||
hiddenPrivateEmailDomains: hiddenPrivateEmailDomains ?? [],
|
||||
vaultRevisionNumber: vaultRevisionNumber ?? 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get vault:', error);
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,9 +257,14 @@ export function handleClearVault(
|
||||
'session:encryptionKeyDerivationParams',
|
||||
'session:publicEmailDomains',
|
||||
'session:privateEmailDomains',
|
||||
'session:hiddenPrivateEmailDomains',
|
||||
'session:vaultRevisionNumber'
|
||||
]);
|
||||
|
||||
// Clear cached client since vault was cleared
|
||||
cachedSqliteClient = null;
|
||||
cachedVaultBlob = null;
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -251,7 +285,101 @@ export async function handleGetCredentials(
|
||||
return { success: true, credentials: credentials };
|
||||
} catch (error) {
|
||||
console.error('Error getting credentials:', error);
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials filtered by URL and page title for autofill performance optimization.
|
||||
* Filters credentials in the background script before sending to reduce message payload size.
|
||||
* Critical for large vaults (1000+ credentials) to avoid multi-second delays.
|
||||
*
|
||||
* @param message - Filtering parameters: currentUrl, pageTitle, matchingMode
|
||||
*/
|
||||
export async function handleGetFilteredCredentials(
|
||||
message: { currentUrl: string, pageTitle: string, matchingMode?: string }
|
||||
) : Promise<messageCredentialsResponse> {
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!encryptionKey) {
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const allCredentials = sqliteClient.getAllCredentials();
|
||||
|
||||
const { filterCredentials, AutofillMatchingMode } = await import('@/utils/credentialMatcher/CredentialMatcher');
|
||||
|
||||
// Parse matching mode from string
|
||||
let matchingMode = AutofillMatchingMode.DEFAULT;
|
||||
if (message.matchingMode) {
|
||||
matchingMode = message.matchingMode as typeof AutofillMatchingMode[keyof typeof AutofillMatchingMode];
|
||||
}
|
||||
|
||||
// Filter credentials in background to reduce payload size (~95% reduction)
|
||||
const filteredCredentials = filterCredentials(
|
||||
allCredentials,
|
||||
message.currentUrl,
|
||||
message.pageTitle,
|
||||
matchingMode
|
||||
);
|
||||
|
||||
return { success: true, credentials: filteredCredentials };
|
||||
} catch (error) {
|
||||
console.error('Error getting filtered credentials:', error);
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credentials filtered by text search query.
|
||||
* Searches across entire vault (service name, username, email, URL) and returns matches.
|
||||
*
|
||||
* @param message - Search parameters: searchTerm
|
||||
*/
|
||||
export async function handleGetSearchCredentials(
|
||||
message: { searchTerm: string }
|
||||
) : Promise<messageCredentialsResponse> {
|
||||
const encryptionKey = await handleGetEncryptionKey();
|
||||
|
||||
if (!encryptionKey) {
|
||||
return { success: false, error: await t('common.errors.vaultIsLocked') };
|
||||
}
|
||||
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const allCredentials = sqliteClient.getAllCredentials();
|
||||
|
||||
// If search term is empty, return empty array
|
||||
if (!message.searchTerm || message.searchTerm.trim() === '') {
|
||||
return { success: true, credentials: [] };
|
||||
}
|
||||
|
||||
const searchTerm = message.searchTerm.toLowerCase().trim();
|
||||
|
||||
// Filter credentials by search term across multiple fields
|
||||
const searchResults = allCredentials.filter(cred => {
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase(),
|
||||
cred.ServiceUrl?.toLowerCase()
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchTerm));
|
||||
}).sort((a, b) => {
|
||||
// Sort by service name, then username
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
|
||||
return { success: true, credentials: searchResults };
|
||||
} catch (error) {
|
||||
console.error('Error searching credentials:', error);
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,28 +440,26 @@ export async function getEmailAddressesForVault(
|
||||
export function handleGetDefaultEmailDomain(): Promise<stringResponse> {
|
||||
return (async (): Promise<stringResponse> => {
|
||||
try {
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[];
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
|
||||
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const defaultEmailDomain = sqliteClient.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
const defaultEmailDomain = await sqliteClient.getDefaultEmailDomain();
|
||||
|
||||
return { success: true, value: defaultEmailDomain ?? undefined };
|
||||
} catch (error) {
|
||||
console.error('Error getting default email domain:', error);
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity settings.
|
||||
* Returns the effective language (with smart UI language matching if no explicit override is set).
|
||||
*/
|
||||
export async function handleGetDefaultIdentitySettings(
|
||||
) : Promise<IdentitySettingsResponse> {
|
||||
try {
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const language = sqliteClient.getDefaultIdentityLanguage();
|
||||
const language = await sqliteClient.getEffectiveIdentityLanguage();
|
||||
const gender = sqliteClient.getDefaultIdentityGender();
|
||||
|
||||
return {
|
||||
@@ -345,7 +471,7 @@ export async function handleGetDefaultIdentitySettings(
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting default identity settings:', error);
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +487,7 @@ export async function handleGetPasswordSettings(
|
||||
return { success: true, settings: passwordSettings };
|
||||
} catch (error) {
|
||||
console.error('Error getting password settings:', error);
|
||||
return { success: false, error: await t('common.errors.failedToRetrieveData') };
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,18 +524,16 @@ export async function handleUploadVault(
|
||||
message: any
|
||||
) : Promise<messageVaultUploadResponse> {
|
||||
try {
|
||||
// Store the new vault blob in session storage.
|
||||
// Persist the current updated vault blob in session storage.
|
||||
await storage.setItem('session:encryptedVault', message.vaultBlob);
|
||||
|
||||
// Create new sqlite client which will use the new vault blob.
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
|
||||
// Upload the new vault to the server.
|
||||
const sqliteClient = await createVaultSqliteClient();
|
||||
const response = await uploadNewVaultToServer(sqliteClient);
|
||||
return { success: true, status: response.status, newRevisionNumber: response.newRevisionNumber };
|
||||
} catch (error) {
|
||||
console.error('Failed to upload vault:', error);
|
||||
return { success: false, error: await t('common.errors.failedToUploadVault') };
|
||||
return { success: false, error: await t('common.errors.unknownError') };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,10 +603,17 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
// Update storage with the newly encrypted vault (serialized from current in-memory state)
|
||||
await storage.setItems([
|
||||
{ key: 'session:encryptedVault', value: encryptedVault }
|
||||
]);
|
||||
|
||||
/*
|
||||
* Update cached vault blob to match the new encrypted version
|
||||
* This prevents unnecessary cache invalidation since the in-memory sqliteClient is already up to date
|
||||
*/
|
||||
cachedVaultBlob = encryptedVault;
|
||||
|
||||
// Get metadata from storage
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
|
||||
|
||||
@@ -496,23 +627,21 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
credentialsCount: sqliteClient.getAllCredentials().length,
|
||||
currentRevisionNumber: vaultRevisionNumber,
|
||||
emailAddressList: emailAddresses,
|
||||
privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
|
||||
publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates.
|
||||
encryptionPublicKey: '', // Empty on purpose, only required if new public/private key pair is generated.
|
||||
client: '', // Empty on purpose, API will not use this for vault updates.
|
||||
updatedAt: new Date().toISOString(),
|
||||
username: username,
|
||||
version: (await sqliteClient.getDatabaseVersion()).version
|
||||
version: (await sqliteClient.getDatabaseVersion()).version,
|
||||
// TODO: add public RSA encryption key to payload when implementing vault creation from browser extension. Currently only web app does this.
|
||||
encryptionPublicKey: '',
|
||||
};
|
||||
|
||||
const webApi = new WebApiService(() => {});
|
||||
const webApi = new WebApiService();
|
||||
const response = await webApi.post<Vault, VaultPostResponse>('Vault', newVault);
|
||||
|
||||
// Check if response is successful (.status === 0)
|
||||
if (response.status === 0) {
|
||||
await storage.setItem('session:vaultRevisionNumber', response.newRevisionNumber);
|
||||
} else {
|
||||
throw new Error(await t('common.errors.failedToUploadVault'));
|
||||
throw new Error(await t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -520,6 +649,7 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
|
||||
|
||||
/**
|
||||
* Create a new sqlite client for the stored vault.
|
||||
* Uses a cache to avoid repeated decryption and initialization for read operations.
|
||||
*/
|
||||
async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
const encryptedVault = await storage.getItem('session:encryptedVault') as string;
|
||||
@@ -528,15 +658,24 @@ async function createVaultSqliteClient() : Promise<SqliteClient> {
|
||||
throw new Error(await t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
// Decrypt the vault.
|
||||
// Check if we have a valid cached client
|
||||
if (cachedSqliteClient && cachedVaultBlob === encryptedVault) {
|
||||
return cachedSqliteClient;
|
||||
}
|
||||
|
||||
// Decrypt the vault
|
||||
const decryptedVault = await EncryptionUtility.symmetricDecrypt(
|
||||
encryptedVault,
|
||||
encryptionKey
|
||||
);
|
||||
|
||||
// Initialize the SQLite client with the decrypted vault.
|
||||
// Initialize the SQLite client with the decrypted vault
|
||||
const sqliteClient = new SqliteClient();
|
||||
await sqliteClient.initializeFromBase64(decryptedVault);
|
||||
|
||||
// Cache the client and vault blob
|
||||
cachedSqliteClient = sqliteClient;
|
||||
cachedVaultBlob = encryptedVault;
|
||||
|
||||
return sqliteClient;
|
||||
}
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CombinedStopWords } from '@/utils/formDetector/FieldPatterns';
|
||||
|
||||
export enum AutofillMatchingMode {
|
||||
DEFAULT = 'default',
|
||||
URL_EXACT = 'url_exact',
|
||||
URL_SUBDOMAIN = 'url_subdomain'
|
||||
}
|
||||
|
||||
type CredentialWithPriority = Credential & {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domain from URL, handling both full URLs and partial domains
|
||||
* @param url - URL or domain string
|
||||
* @returns Normalized domain without protocol or www
|
||||
*/
|
||||
export function extractDomain(url: string): string {
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove protocol if present
|
||||
let domain = url.toLowerCase().trim();
|
||||
domain = domain.replace(/^https?:\/\//, '');
|
||||
|
||||
// Remove www. prefix
|
||||
domain = domain.replace(/^www\./, '');
|
||||
|
||||
// Remove path, query, and fragment
|
||||
domain = domain.split('/')[0];
|
||||
domain = domain.split('?')[0];
|
||||
domain = domain.split('#')[0];
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract root domain from a domain string.
|
||||
* E.g., "sub.example.com" -> "example.com"
|
||||
* E.g., "sub.example.com.au" -> "example.com.au"
|
||||
* E.g., "sub.example.co.uk" -> "example.co.uk"
|
||||
*/
|
||||
export function extractRootDomain(domain: string): string {
|
||||
const parts = domain.split('.');
|
||||
if (parts.length < 2) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
// Common two-level public TLDs
|
||||
const twoLevelTlds = new Set([
|
||||
// Australia
|
||||
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
|
||||
// United Kingdom
|
||||
'co.uk', 'org.uk', 'net.uk', 'ac.uk', 'gov.uk', 'plc.uk', 'ltd.uk', 'me.uk',
|
||||
// Canada
|
||||
'co.ca', 'net.ca', 'org.ca', 'gc.ca', 'ab.ca', 'bc.ca', 'mb.ca', 'nb.ca', 'nf.ca', 'nl.ca', 'ns.ca', 'nt.ca', 'nu.ca',
|
||||
'on.ca', 'pe.ca', 'qc.ca', 'sk.ca', 'yk.ca',
|
||||
// India
|
||||
'co.in', 'net.in', 'org.in', 'edu.in', 'gov.in', 'ac.in', 'res.in', 'gen.in', 'firm.in', 'ind.in',
|
||||
// Japan
|
||||
'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'ad.jp', 'ed.jp', 'go.jp', 'gr.jp', 'lg.jp',
|
||||
// South Africa
|
||||
'co.za', 'net.za', 'org.za', 'edu.za', 'gov.za', 'ac.za', 'web.za',
|
||||
// New Zealand
|
||||
'co.nz', 'net.nz', 'org.nz', 'edu.nz', 'govt.nz', 'ac.nz', 'geek.nz', 'gen.nz', 'kiwi.nz', 'maori.nz', 'mil.nz', 'school.nz',
|
||||
// Brazil
|
||||
'com.br', 'net.br', 'org.br', 'edu.br', 'gov.br', 'mil.br', 'art.br', 'etc.br', 'adv.br', 'arq.br', 'bio.br', 'cim.br',
|
||||
'cng.br', 'cnt.br', 'ecn.br', 'eng.br', 'esp.br', 'eti.br', 'far.br', 'fnd.br', 'fot.br', 'fst.br', 'g12.br', 'geo.br',
|
||||
'ggf.br', 'jor.br', 'lel.br', 'mat.br', 'med.br', 'mus.br', 'not.br', 'ntr.br', 'odo.br', 'ppg.br', 'pro.br', 'psc.br',
|
||||
'psi.br', 'qsl.br', 'rec.br', 'slg.br', 'srv.br', 'tmp.br', 'trd.br', 'tur.br', 'tv.br', 'vet.br', 'zlg.br',
|
||||
// Russia
|
||||
'com.ru', 'net.ru', 'org.ru', 'edu.ru', 'gov.ru', 'int.ru', 'mil.ru', 'spb.ru', 'msk.ru',
|
||||
// China
|
||||
'com.cn', 'net.cn', 'org.cn', 'edu.cn', 'gov.cn', 'mil.cn', 'ac.cn', 'ah.cn', 'bj.cn', 'cq.cn', 'fj.cn', 'gd.cn', 'gs.cn',
|
||||
'gz.cn', 'gx.cn', 'ha.cn', 'hb.cn', 'he.cn', 'hi.cn', 'hk.cn', 'hl.cn', 'hn.cn', 'jl.cn', 'js.cn', 'jx.cn', 'ln.cn', 'mo.cn',
|
||||
'nm.cn', 'nx.cn', 'qh.cn', 'sc.cn', 'sd.cn', 'sh.cn', 'sn.cn', 'sx.cn', 'tj.cn', 'tw.cn', 'xj.cn', 'xz.cn', 'yn.cn', 'zj.cn',
|
||||
// Mexico
|
||||
'com.mx', 'net.mx', 'org.mx', 'edu.mx', 'gob.mx',
|
||||
// Argentina
|
||||
'com.ar', 'net.ar', 'org.ar', 'edu.ar', 'gov.ar', 'mil.ar', 'int.ar',
|
||||
// Chile
|
||||
'com.cl', 'net.cl', 'org.cl', 'edu.cl', 'gov.cl', 'mil.cl',
|
||||
// Colombia
|
||||
'com.co', 'net.co', 'org.co', 'edu.co', 'gov.co', 'mil.co', 'nom.co',
|
||||
// Venezuela
|
||||
'com.ve', 'net.ve', 'org.ve', 'edu.ve', 'gov.ve', 'mil.ve', 'web.ve',
|
||||
// Peru
|
||||
'com.pe', 'net.pe', 'org.pe', 'edu.pe', 'gob.pe', 'mil.pe', 'nom.pe',
|
||||
// Ecuador
|
||||
'com.ec', 'net.ec', 'org.ec', 'edu.ec', 'gov.ec', 'mil.ec', 'med.ec', 'fin.ec', 'pro.ec', 'info.ec',
|
||||
// Europe
|
||||
'co.at', 'or.at', 'ac.at', 'gv.at', 'priv.at',
|
||||
'co.be', 'ac.be',
|
||||
'co.dk', 'ac.dk',
|
||||
'co.il', 'net.il', 'org.il', 'ac.il', 'gov.il', 'idf.il', 'k12.il', 'muni.il',
|
||||
'co.no', 'ac.no', 'priv.no',
|
||||
'co.pl', 'net.pl', 'org.pl', 'edu.pl', 'gov.pl', 'mil.pl', 'nom.pl', 'com.pl',
|
||||
'co.th', 'net.th', 'org.th', 'edu.th', 'gov.th', 'mil.th', 'ac.th', 'in.th',
|
||||
'co.kr', 'net.kr', 'org.kr', 'edu.kr', 'gov.kr', 'mil.kr', 'ac.kr', 'go.kr', 'ne.kr', 'or.kr', 'pe.kr', 're.kr', 'seoul.kr',
|
||||
'kyonggi.kr',
|
||||
// Others
|
||||
'co.id', 'net.id', 'org.id', 'edu.id', 'gov.id', 'mil.id', 'web.id', 'ac.id', 'sch.id',
|
||||
'co.ma', 'net.ma', 'org.ma', 'edu.ma', 'gov.ma', 'ac.ma', 'press.ma',
|
||||
'co.ke', 'net.ke', 'org.ke', 'edu.ke', 'gov.ke', 'ac.ke', 'go.ke', 'info.ke', 'me.ke', 'mobi.ke', 'sc.ke',
|
||||
'co.ug', 'net.ug', 'org.ug', 'edu.ug', 'gov.ug', 'ac.ug', 'sc.ug', 'go.ug', 'ne.ug', 'or.ug',
|
||||
'co.tz', 'net.tz', 'org.tz', 'edu.tz', 'gov.tz', 'ac.tz', 'go.tz', 'hotel.tz', 'info.tz', 'me.tz', 'mil.tz', 'mobi.tz',
|
||||
'ne.tz', 'or.tz', 'sc.tz', 'tv.tz',
|
||||
]);
|
||||
|
||||
// Check if the last two parts form a known two-level TLD
|
||||
if (parts.length >= 3) {
|
||||
const lastTwoParts = parts.slice(-2).join('.');
|
||||
if (twoLevelTlds.has(lastTwoParts)) {
|
||||
// Take the last three parts for two-level TLDs
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
}
|
||||
|
||||
// Default to last two parts for regular TLDs
|
||||
return parts.length >= 2 ? parts.slice(-2).join('.') : domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two domains match, supporting partial matches
|
||||
* @param domain1 - First domain
|
||||
* @param domain2 - Second domain
|
||||
* @returns True if domains match (including partial matches)
|
||||
*/
|
||||
function domainsMatch(domain1: string, domain2: string): boolean {
|
||||
if (!domain1 || !domain2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const d1 = extractDomain(domain1);
|
||||
const d2 = extractDomain(domain2);
|
||||
|
||||
// Exact match
|
||||
if (d1 === d2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if one domain contains the other (for subdomain matching)
|
||||
if (d1.includes(d2) || d2.includes(d1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check root domain match
|
||||
const d1Root = extractRootDomain(d1);
|
||||
const d2Root = extractRootDomain(d2);
|
||||
|
||||
return d1Root === d2Root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract meaningful words from text, removing punctuation and filtering stop words
|
||||
* @param text - Text to extract words from
|
||||
* @returns Array of filtered words
|
||||
*/
|
||||
function extractWords(text: string): string[] {
|
||||
if (!text || text.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return text.toLowerCase()
|
||||
// Replace common separators and punctuation with spaces
|
||||
.replace(/[|,;:\-–—/\\()[\]{}'"`~!@#$%^&*+=<>?]/g, ' ')
|
||||
// Split on whitespace and filter
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 &&
|
||||
!CombinedStopWords.has(word)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter credentials based on current URL and page context with anti-phishing protection.
|
||||
*
|
||||
* **Security Note**: When searching with a URL, text search fallback only applies to
|
||||
* credentials with no service URL defined. This prevents phishing attacks where a
|
||||
* malicious site might match credentials intended for the legitimate site.
|
||||
*
|
||||
* Credentials are sorted by priority:
|
||||
* 1. Exact domain match (priority 1 - highest)
|
||||
* 2. Partial/subdomain match (priority 2)
|
||||
* 3. Service name fallback match (priority 5 - lowest, only for credentials without URLs)
|
||||
*/
|
||||
export function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string, matchingMode: AutofillMatchingMode = AutofillMatchingMode.DEFAULT): Credential[] {
|
||||
const filtered: CredentialWithPriority[] = [];
|
||||
const currentDomain = extractDomain(currentUrl);
|
||||
|
||||
// Determine feature flags based on matching mode
|
||||
let enableExactMatch = false;
|
||||
let enableSubdomainMatch = false;
|
||||
let enableServiceNameFallback = false;
|
||||
|
||||
switch (matchingMode) {
|
||||
case AutofillMatchingMode.URL_EXACT:
|
||||
enableExactMatch = true;
|
||||
enableSubdomainMatch = false;
|
||||
enableServiceNameFallback = false;
|
||||
break;
|
||||
|
||||
case AutofillMatchingMode.URL_SUBDOMAIN:
|
||||
enableExactMatch = true;
|
||||
enableSubdomainMatch = true;
|
||||
enableServiceNameFallback = false;
|
||||
break;
|
||||
|
||||
case AutofillMatchingMode.DEFAULT:
|
||||
enableExactMatch = true;
|
||||
enableSubdomainMatch = true;
|
||||
enableServiceNameFallback = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Process credentials with service URLs
|
||||
credentials.forEach(cred => {
|
||||
if (!cred.ServiceUrl || cred.ServiceUrl.length === 0) {
|
||||
return; // Handle these in service name fallback
|
||||
}
|
||||
|
||||
const credDomain = extractDomain(cred.ServiceUrl);
|
||||
|
||||
// Check for exact match (priority 1)
|
||||
if (enableExactMatch && currentDomain === credDomain) {
|
||||
filtered.push({ ...cred, priority: 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for subdomain/partial match (priority 2)
|
||||
if (enableSubdomainMatch && domainsMatch(currentDomain, credDomain)) {
|
||||
filtered.push({ ...cred, priority: 2 });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Service name fallback for credentials without URLs (priority 5)
|
||||
if (enableServiceNameFallback) {
|
||||
/*
|
||||
* SECURITY: Service name matching only applies to credentials with no service URL.
|
||||
* This prevents phishing attacks where a malicious site might match credentials
|
||||
* intended for a legitimate site.
|
||||
*/
|
||||
|
||||
// Extract words from page title
|
||||
const titleWords = extractWords(pageTitle);
|
||||
|
||||
if (titleWords.length > 0) {
|
||||
credentials.forEach(cred => {
|
||||
// CRITICAL: Only check credentials that have NO service URL defined
|
||||
if (cred.ServiceUrl && cred.ServiceUrl.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if already in filtered list
|
||||
if (filtered.some(f => f.Id === cred.Id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check page title match with service name
|
||||
if (cred.ServiceName) {
|
||||
const credNameWords = extractWords(cred.ServiceName);
|
||||
|
||||
/*
|
||||
* Match only complete words, not substrings
|
||||
* For example: "Express" should match "My Express Account" but not "AliExpress"
|
||||
*/
|
||||
const hasTitleMatch = titleWords.some(titleWord =>
|
||||
credNameWords.some(credWord =>
|
||||
titleWord === credWord // Exact word match only
|
||||
)
|
||||
);
|
||||
|
||||
if (hasTitleMatch) {
|
||||
filtered.push({ ...cred, priority: 5 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority and return unique credentials (max 3)
|
||||
const uniqueCredentials = Array.from(
|
||||
new Map(
|
||||
filtered
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(cred => [cred.Id, cred])
|
||||
).values()
|
||||
);
|
||||
|
||||
return uniqueCredentials.slice(0, 3);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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, AUTOFILL_MATCHING_MODE_KEY, CUSTOM_EMAIL_HISTORY_KEY, CUSTOM_USERNAME_HISTORY_KEY } from '@/utils/Constants';
|
||||
import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
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';
|
||||
@@ -49,7 +49,14 @@ export function openAutofillPopup(input: HTMLInputElement, container: HTMLElemen
|
||||
document.addEventListener('keydown', handleEnterKey);
|
||||
|
||||
(async () : Promise<void> => {
|
||||
const response = await sendMessage('GET_CREDENTIALS', { }, 'background') as CredentialsResponse;
|
||||
// Load autofill matching mode setting to send to background for filtering
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
const response = await sendMessage('GET_FILTERED_CREDENTIALS', {
|
||||
currentUrl: window.location.href,
|
||||
pageTitle: document.title,
|
||||
matchingMode: matchingMode
|
||||
}, 'background') as CredentialsResponse;
|
||||
|
||||
if (response.success) {
|
||||
await createAutofillPopup(input, response.credentials, container);
|
||||
@@ -182,22 +189,12 @@ export async function createAutofillPopup(input: HTMLInputElement, credentials:
|
||||
credentialList.className = 'av-credential-list';
|
||||
popup.appendChild(credentialList);
|
||||
|
||||
// Add initial credentials
|
||||
// Add initial credentials (already filtered by background script for performance)
|
||||
if (!credentials) {
|
||||
credentials = [];
|
||||
}
|
||||
|
||||
// Load autofill matching mode setting
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
const filteredCredentials = filterCredentials(
|
||||
credentials,
|
||||
window.location.href,
|
||||
document.title,
|
||||
matchingMode
|
||||
);
|
||||
|
||||
updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
updatePopupContent(credentials, credentialList, input, rootContainer, noMatchesText);
|
||||
|
||||
// Add divider
|
||||
const divider = document.createElement('div');
|
||||
@@ -549,62 +546,41 @@ export async function createVaultLockedPopup(input: HTMLInputElement, rootContai
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle popup search input by filtering credentials based on the search term.
|
||||
* Handle popup search input - searches entire vault when user types.
|
||||
* When empty, shows the initially URL-filtered credentials.
|
||||
* When user types, searches ALL credentials in vault (not just the pre-filtered set).
|
||||
*
|
||||
* @param searchInput - The search input element
|
||||
* @param initialCredentials - The initially URL-filtered credentials to show when search is empty
|
||||
* @param rootContainer - The root container element
|
||||
* @param searchTimeout - Timeout for debouncing search
|
||||
* @param credentialList - The credential list element to update
|
||||
* @param input - The input field that triggered the popup
|
||||
* @param noMatchesText - Text to show when no matches found
|
||||
*/
|
||||
async function handleSearchInput(searchInput: HTMLInputElement, credentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
|
||||
async function handleSearchInput(searchInput: HTMLInputElement, initialCredentials: Credential[], rootContainer: HTMLElement, searchTimeout: NodeJS.Timeout | null, credentialList: HTMLElement | null, input: HTMLInputElement, noMatchesText?: string) : Promise<void> {
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
|
||||
// Ensure we have unique credentials
|
||||
const uniqueCredentials = Array.from(new Map(credentials.map(cred => [cred.Id, cred])).values());
|
||||
let filteredCredentials;
|
||||
const searchTerm = searchInput.value.trim();
|
||||
|
||||
if (searchTerm === '') {
|
||||
// Load autofill matching mode setting
|
||||
const matchingMode = await storage.getItem(AUTOFILL_MATCHING_MODE_KEY) as AutofillMatchingMode ?? AutofillMatchingMode.DEFAULT;
|
||||
|
||||
// If search is empty, use original URL-based filtering
|
||||
filteredCredentials = filterCredentials(
|
||||
uniqueCredentials,
|
||||
window.location.href,
|
||||
document.title,
|
||||
matchingMode
|
||||
).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
// If search is empty, show the initially URL-filtered credentials
|
||||
updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
} else {
|
||||
// Otherwise filter based on search term
|
||||
filteredCredentials = uniqueCredentials.filter(cred => {
|
||||
const searchableFields = [
|
||||
cred.ServiceName?.toLowerCase(),
|
||||
cred.Username?.toLowerCase(),
|
||||
cred.Alias?.Email?.toLowerCase(),
|
||||
cred.ServiceUrl?.toLowerCase()
|
||||
];
|
||||
return searchableFields.some(field => field?.includes(searchTerm));
|
||||
}).sort((a, b) => {
|
||||
// First compare by service name
|
||||
const serviceNameComparison = (a.ServiceName ?? '').localeCompare(b.ServiceName ?? '');
|
||||
if (serviceNameComparison !== 0) {
|
||||
return serviceNameComparison;
|
||||
}
|
||||
// Search in full vault with search term
|
||||
const response = await sendMessage('GET_SEARCH_CREDENTIALS', {
|
||||
searchTerm: searchTerm
|
||||
}, 'background') as CredentialsResponse;
|
||||
|
||||
// If service names are equal, compare by username/nickname
|
||||
return (a.Username ?? '').localeCompare(b.Username ?? '');
|
||||
});
|
||||
if (response.success && response.credentials) {
|
||||
updatePopupContent(response.credentials, credentialList, input, rootContainer, noMatchesText);
|
||||
} else {
|
||||
// On error, fallback to showing initial filtered credentials
|
||||
updatePopupContent(initialCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
}
|
||||
}
|
||||
|
||||
// Update popup content with filtered results
|
||||
updatePopupContent(filteredCredentials, credentialList, input, rootContainer, noMatchesText);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,14 @@ let interceptorInitialized = false;
|
||||
let lastCancelledTimestamp = 0;
|
||||
const CANCEL_COOLDOWN_MS = 500; // 500ms cooldown after a recent cancellation
|
||||
|
||||
/**
|
||||
* Track when the page finished loading to detect automatic vs user-initiated requests.
|
||||
* Some websites (like Nintendo, Amazon) automatically trigger passkey requests on page load.
|
||||
* We should filter these if no matching credentials exist.
|
||||
*/
|
||||
let pageLoadTime = 0;
|
||||
const AUTO_REQUEST_THRESHOLD_MS = 1000; // Requests within 1 second of page load are considered "automatic"
|
||||
|
||||
/**
|
||||
* Check if page is ready for WebAuthn interactions.
|
||||
* Safari and other browsers can trigger WebAuthn requests during URL autocomplete
|
||||
@@ -54,6 +62,9 @@ export async function initializeWebAuthnInterceptor(_ctx: any): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track page load time for detecting automatic requests
|
||||
pageLoadTime = Date.now();
|
||||
|
||||
// Listen for WebAuthn create events from the page
|
||||
window.addEventListener('aliasvault:webauthn:create', async (event: any) => {
|
||||
const { requestId, publicKey, origin } = event.detail;
|
||||
@@ -196,10 +207,14 @@ export async function initializeWebAuthnInterceptor(_ctx: any): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect if this is an automatic request (within 2 seconds of page load)
|
||||
const isAutomaticRequest = (Date.now() - pageLoadTime) < AUTO_REQUEST_THRESHOLD_MS;
|
||||
|
||||
// Send to background script to handle
|
||||
const result = await sendMessage('WEBAUTHN_GET', {
|
||||
publicKey,
|
||||
origin
|
||||
origin,
|
||||
isAutomaticRequest
|
||||
}, 'background');
|
||||
|
||||
// Track if user cancelled to enable cooldown
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HashRouter as Router, Routes, Route, useLocation } from 'react-router-d
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import DefaultLayout from '@/entrypoints/popup/components/Layout/DefaultLayout';
|
||||
import Header from '@/entrypoints/popup/components/Layout/Header';
|
||||
import PasskeyLayout from '@/entrypoints/popup/components/Layout/PasskeyLayout';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
import { useApp } from '@/entrypoints/popup/context/AppContext';
|
||||
@@ -31,6 +32,7 @@ import ContextMenuSettings from '@/entrypoints/popup/pages/settings/ContextMenuS
|
||||
import LanguageSettings from '@/entrypoints/popup/pages/settings/LanguageSettings';
|
||||
import PasskeySettings from '@/entrypoints/popup/pages/settings/PasskeySettings';
|
||||
import Settings from '@/entrypoints/popup/pages/settings/Settings';
|
||||
import VaultUnlockSettings from '@/entrypoints/popup/pages/settings/VaultUnlockSettings';
|
||||
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
|
||||
@@ -45,6 +47,8 @@ enum LayoutType {
|
||||
DEFAULT = 'default',
|
||||
/** Minimal layout for passkey operations - logo only, no footer */
|
||||
PASSKEY = 'passkey',
|
||||
/** Auth layout for login/unlock pages - no footer menu */
|
||||
AUTH = 'auth',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,12 +112,38 @@ const AppContent: React.FC<{
|
||||
<PasskeyLayout>
|
||||
{loadingOverlay}
|
||||
{message && (
|
||||
<p className="text-red-500 mb-4">{message}</p>
|
||||
<p className="mb-4 text-red-500 dark:text-red-400 text-sm">{message}</p>
|
||||
)}
|
||||
{routesComponent}
|
||||
</PasskeyLayout>
|
||||
);
|
||||
|
||||
case LayoutType.AUTH:
|
||||
// Auth layout - header only, no footer menu for login/unlock pages
|
||||
return (
|
||||
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
|
||||
{loadingOverlay}
|
||||
<Header
|
||||
routes={routes}
|
||||
rightButtons={headerButtons}
|
||||
/>
|
||||
<main
|
||||
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
|
||||
style={{
|
||||
paddingTop: '64px',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{message && (
|
||||
<div className="px-4 pt-0">
|
||||
<p className="text-red-500 dark:text-red-400 text-sm">{message}</p>
|
||||
</div>
|
||||
)}
|
||||
{routesComponent}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
case LayoutType.DEFAULT:
|
||||
default:
|
||||
// Default layout with full header, footer, navigation
|
||||
@@ -147,8 +177,8 @@ const App: React.FC = () => {
|
||||
const routes: RouteConfig[] = React.useMemo(() => [
|
||||
{ path: '/', element: <Index />, showBackButton: false },
|
||||
{ path: '/reinitialize', element: <Reinitialize />, showBackButton: false },
|
||||
{ path: '/login', element: <Login />, showBackButton: false },
|
||||
{ path: '/unlock', element: <Unlock />, showBackButton: false },
|
||||
{ path: '/login', element: <Login />, showBackButton: false, layout: LayoutType.AUTH },
|
||||
{ path: '/unlock', element: <Unlock />, showBackButton: false, layout: LayoutType.AUTH },
|
||||
{ path: '/unlock-success', element: <UnlockSuccess />, showBackButton: false },
|
||||
{ path: '/upgrade', element: <Upgrade />, showBackButton: false },
|
||||
{ path: '/auth-settings', element: <AuthSettings />, showBackButton: true, title: t('settings.title') },
|
||||
@@ -161,6 +191,7 @@ const App: React.FC = () => {
|
||||
{ path: '/emails', element: <EmailsList />, showBackButton: false },
|
||||
{ path: '/emails/:id', element: <EmailDetails />, showBackButton: true, title: t('emails.title') },
|
||||
{ path: '/settings', element: <Settings />, showBackButton: false },
|
||||
{ path: '/settings/unlock-method', element: <VaultUnlockSettings />, showBackButton: true, title: t('settings.unlockMethod.title') },
|
||||
{ path: '/settings/autofill', element: <AutofillSettings />, showBackButton: true, title: t('settings.autofillSettings') },
|
||||
{ path: '/settings/context-menu', element: <ContextMenuSettings />, showBackButton: true, title: t('settings.contextMenuSettings') },
|
||||
{ path: '/settings/clipboard', element: <ClipboardSettings />, showBackButton: true, title: t('settings.clipboardSettings') },
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
|
||||
export type AlertType = 'error' | 'success' | 'warning' | 'info';
|
||||
|
||||
interface IAlertMessageProps {
|
||||
type: AlertType;
|
||||
message: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert message component for displaying error, success, warning, or info messages.
|
||||
* @param props - The component props.
|
||||
* @param props.type - The type of alert (error, success, warning, info).
|
||||
* @param props.message - The message to display.
|
||||
* @param props.className - Optional additional CSS classes.
|
||||
* @returns The rendered alert message component.
|
||||
*/
|
||||
const AlertMessage: React.FC<IAlertMessageProps> = ({ type, message, className = '' }) => {
|
||||
/**
|
||||
* Get the appropriate CSS classes based on alert type.
|
||||
* @returns CSS class string.
|
||||
*/
|
||||
const getAlertClasses = (): string => {
|
||||
const baseClasses = 'p-3 border rounded-md text-sm';
|
||||
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return `${baseClasses} bg-red-100 dark:bg-red-900/30 border-red-300 dark:border-red-700 text-red-800 dark:text-red-300`;
|
||||
case 'success':
|
||||
return `${baseClasses} bg-green-100 dark:bg-green-900/30 border-green-300 dark:border-green-700 text-green-800 dark:text-green-300`;
|
||||
case 'warning':
|
||||
return `${baseClasses} bg-yellow-100 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300`;
|
||||
case 'info':
|
||||
return `${baseClasses} bg-blue-100 dark:bg-blue-900/30 border-blue-300 dark:border-blue-700 text-blue-800 dark:text-blue-300`;
|
||||
default:
|
||||
return baseClasses;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${getAlertClasses()} ${className}`}>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertMessage;
|
||||
@@ -31,7 +31,7 @@ const HeaderBlock: React.FC<HeaderBlockProps> = ({ credential }) => (
|
||||
{credential.ServiceUrl}
|
||||
</a>
|
||||
) : (
|
||||
<span className="break-all">{credential.ServiceUrl}</span>
|
||||
<span className="text-gray-500 dark:text-gray-300 break-all">{credential.ServiceUrl}</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
type TotpFormData = {
|
||||
name: string;
|
||||
secretKey: string;
|
||||
}
|
||||
|
||||
type TotpEditorState = {
|
||||
isAddFormVisible: boolean;
|
||||
formData: TotpFormData;
|
||||
}
|
||||
|
||||
type TotpEditorProps = {
|
||||
totpCodes: TotpCode[];
|
||||
onTotpCodesChange: (totpCodes: TotpCode[]) => void;
|
||||
originalTotpCodeIds: string[];
|
||||
isAddFormVisible: boolean;
|
||||
formData: TotpFormData;
|
||||
onStateChange: (state: TotpEditorState) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for editing TOTP codes for a credential.
|
||||
*/
|
||||
const TotpEditor: React.FC<TotpEditorProps> = ({
|
||||
totpCodes,
|
||||
onTotpCodesChange,
|
||||
originalTotpCodeIds,
|
||||
isAddFormVisible,
|
||||
formData,
|
||||
onStateChange
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* Sanitizes the secret key by extracting it from a TOTP URI if needed
|
||||
*/
|
||||
const sanitizeSecretKey = (secretKeyInput: string, nameInput: string): { secretKey: string, name: string } => {
|
||||
let secretKey = secretKeyInput.trim();
|
||||
let name = nameInput.trim();
|
||||
|
||||
// Check if it's a TOTP URI
|
||||
if (secretKey.toLowerCase().startsWith('otpauth://totp/')) {
|
||||
try {
|
||||
const uri = OTPAuth.URI.parse(secretKey);
|
||||
if (uri instanceof OTPAuth.TOTP) {
|
||||
secretKey = uri.secret.base32;
|
||||
// If name is empty, use the label from the URI
|
||||
if (!name && uri.label) {
|
||||
name = uri.label;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
throw new Error(t('totp.errors.invalidSecretKey'));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove spaces from the secret key
|
||||
secretKey = secretKey.replace(/\s/g, '');
|
||||
|
||||
// Validate the secret key format (base32)
|
||||
if (!/^[A-Z2-7]+=*$/i.test(secretKey)) {
|
||||
throw new Error(t('totp.errors.invalidSecretKey'));
|
||||
}
|
||||
|
||||
return { secretKey, name: name || 'Authenticator' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the add form
|
||||
*/
|
||||
const showAddForm = (): void => {
|
||||
onStateChange({
|
||||
isAddFormVisible: true,
|
||||
formData: { name: '', secretKey: '' }
|
||||
});
|
||||
setFormError(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the add form
|
||||
*/
|
||||
const hideAddForm = (): void => {
|
||||
onStateChange({
|
||||
isAddFormVisible: false,
|
||||
formData: { name: '', secretKey: '' }
|
||||
});
|
||||
setFormError(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates form data
|
||||
*/
|
||||
const updateFormData = (updates: Partial<TotpFormData>): void => {
|
||||
onStateChange({
|
||||
isAddFormVisible,
|
||||
formData: { ...formData, ...updates }
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles adding a new TOTP code
|
||||
*/
|
||||
const handleAddTotpCode = (e?: React.MouseEvent | React.KeyboardEvent): void => {
|
||||
e?.preventDefault();
|
||||
setFormError(null);
|
||||
|
||||
// Validate required fields
|
||||
if (!formData.secretKey) {
|
||||
setFormError(t('credentials.validation.required'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Sanitize the secret key
|
||||
const { secretKey, name } = sanitizeSecretKey(formData.secretKey, formData.name);
|
||||
|
||||
// Create new TOTP code
|
||||
const newTotpCode: TotpCode = {
|
||||
Id: crypto.randomUUID().toUpperCase(),
|
||||
Name: name,
|
||||
SecretKey: secretKey,
|
||||
CredentialId: '' // Will be set when saving the credential
|
||||
};
|
||||
|
||||
// Add to the list
|
||||
const updatedTotpCodes = [...totpCodes, newTotpCode];
|
||||
onTotpCodesChange(updatedTotpCodes);
|
||||
|
||||
// Hide the form
|
||||
hideAddForm();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
setFormError(error.message);
|
||||
} else {
|
||||
setFormError(t('common.errors.unknownErrorTryAgain'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initiates the delete process for a TOTP code
|
||||
*/
|
||||
const deleteTotpCode = (totpToDelete: TotpCode): void => {
|
||||
// Check if this TOTP code was part of the original set
|
||||
const wasOriginal = originalTotpCodeIds.includes(totpToDelete.Id);
|
||||
|
||||
let updatedTotpCodes: TotpCode[];
|
||||
if (wasOriginal) {
|
||||
// Mark as deleted (soft delete for syncing)
|
||||
updatedTotpCodes = totpCodes.map(tc =>
|
||||
tc.Id === totpToDelete.Id
|
||||
? { ...tc, IsDeleted: true }
|
||||
: tc
|
||||
);
|
||||
} else {
|
||||
// Hard delete (remove from array)
|
||||
updatedTotpCodes = totpCodes.filter(tc => tc.Id !== totpToDelete.Id);
|
||||
}
|
||||
|
||||
onTotpCodesChange(updatedTotpCodes);
|
||||
};
|
||||
|
||||
// Filter out deleted TOTP codes for display
|
||||
const activeTotpCodes = totpCodes.filter(tc => !tc.IsDeleted);
|
||||
const hasActiveTotpCodes = activeTotpCodes.length > 0;
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t('common.twoFactorAuthentication')}
|
||||
</h2>
|
||||
{hasActiveTotpCodes && !isAddFormVisible && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={showAddForm}
|
||||
className="w-8 h-8 flex items-center justify-center text-primary-700 hover:text-white border border-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg dark:border-primary-500 dark:text-primary-500 dark:hover:text-white dark:hover:bg-primary-600 dark:focus:ring-primary-800"
|
||||
title={t('totp.addCode')}
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hasActiveTotpCodes && !isAddFormVisible && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={showAddForm}
|
||||
className="w-full py-1.5 px-4 flex items-center justify-center gap-2 text-primary-700 hover:text-white border border-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg dark:border-primary-500 dark:text-primary-500 dark:hover:text-white dark:hover:bg-primary-600 dark:focus:ring-primary-800"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
<span>{t('totp.addCode')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isAddFormVisible && (
|
||||
<div className="p-4 mb-4 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{t('totp.addCode')}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={hideAddForm}
|
||||
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('totp.instructions')}
|
||||
</p>
|
||||
|
||||
{formError && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg dark:bg-red-900/20 dark:border-red-800">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{formError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="totp-name" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{t('totp.nameOptional')}
|
||||
</label>
|
||||
<input
|
||||
id="totp-name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => updateFormData({ name: e.target.value })}
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="totp-secret" className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{t('totp.secretKey')}
|
||||
</label>
|
||||
<input
|
||||
id="totp-secret"
|
||||
type="text"
|
||||
value={formData.secretKey}
|
||||
onChange={(e) => updateFormData({ secretKey: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddTotpCode(e);
|
||||
}
|
||||
}}
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleAddTotpCode(e)}
|
||||
className="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
|
||||
>
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasActiveTotpCodes && (
|
||||
<div className="grid grid-cols-1 gap-4 mt-4">
|
||||
{activeTotpCodes.map(totpCode => (
|
||||
<div
|
||||
key={totpCode.Id}
|
||||
className="p-2 ps-3 pe-3 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex items-center flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{totpCode.Name}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('totp.saveToViewCode')}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteTotpCode(totpCode)}
|
||||
className="text-red-600 hover:text-red-800 dark:text-red-500 dark:hover:text-red-400"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TotpEditor;
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type HelpModalProps = {
|
||||
titleKey: string;
|
||||
contentKey: string;
|
||||
title: string;
|
||||
content: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ type HelpModalProps = {
|
||||
* Reusable help modal component with a question mark icon button.
|
||||
* Shows a modal popup with help information when clicked.
|
||||
*/
|
||||
const HelpModal: React.FC<HelpModalProps> = ({ titleKey, contentKey, className = '' }) => {
|
||||
const HelpModal: React.FC<HelpModalProps> = ({ title, content, className = '' }) => {
|
||||
const { t } = useTranslation();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
@@ -43,7 +43,7 @@ const HelpModal: React.FC<HelpModalProps> = ({ titleKey, contentKey, className =
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t(titleKey)}
|
||||
{title}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
@@ -66,7 +66,7 @@ const HelpModal: React.FC<HelpModalProps> = ({ titleKey, contentKey, className =
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{t(contentKey)}
|
||||
{content}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import QRCode from 'qrcode';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { MobileLoginErrorCode } from '@/entrypoints/popup/types/MobileLoginErrorCode';
|
||||
import { MobileLoginUtility } from '@/entrypoints/popup/utils/MobileLoginUtility';
|
||||
|
||||
import type { MobileLoginResult } from '@/utils/types/messaging/MobileLoginResult';
|
||||
import type { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
interface IMobileUnlockModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (result: MobileLoginResult) => Promise<void>;
|
||||
webApi: WebApiService;
|
||||
mode?: 'login' | 'unlock';
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for mobile login/unlock via QR code scanning.
|
||||
*/
|
||||
const MobileUnlockModal: React.FC<IMobileUnlockModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
webApi,
|
||||
mode = 'login'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState<MobileLoginErrorCode | null>(null);
|
||||
const [timeRemaining, setTimeRemaining] = useState<number>(120); // 2 minutes in seconds
|
||||
const mobileLoginRef = useRef<MobileLoginUtility | null>(null);
|
||||
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
/**
|
||||
* Get translated error message for error code.
|
||||
*/
|
||||
const getErrorMessage = (errorCode: MobileLoginErrorCode): string => {
|
||||
switch (errorCode) {
|
||||
case MobileLoginErrorCode.TIMEOUT:
|
||||
return t('auth.errors.mobileLoginRequestExpired');
|
||||
case MobileLoginErrorCode.GENERIC:
|
||||
default:
|
||||
return t('common.errors.unknownError');
|
||||
}
|
||||
};
|
||||
|
||||
// Countdown timer effect
|
||||
useEffect(() => {
|
||||
if (qrCodeUrl && timeRemaining > 0 && isOpen) {
|
||||
countdownIntervalRef.current = setInterval(() => {
|
||||
setTimeRemaining(prev => {
|
||||
if (prev <= 1) {
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return (): void => {
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [qrCodeUrl, timeRemaining, isOpen]);
|
||||
|
||||
// Initialize mobile login when modal opens
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize mobile login on modal open.
|
||||
*/
|
||||
const initiateMobileLogin = async (): Promise<void> => {
|
||||
try {
|
||||
setError(null);
|
||||
setQrCodeUrl(null);
|
||||
setTimeRemaining(120);
|
||||
|
||||
// Initialize mobile login utility
|
||||
if (!mobileLoginRef.current) {
|
||||
mobileLoginRef.current = new MobileLoginUtility(webApi);
|
||||
}
|
||||
|
||||
// Initiate mobile login and get QR code data
|
||||
const requestId = await mobileLoginRef.current.initiate();
|
||||
|
||||
// Generate QR code with AliasVault prefix for mobile login
|
||||
const qrData = `aliasvault://open/mobile-unlock/${requestId}`;
|
||||
const qrDataUrl = await QRCode.toDataURL(qrData, {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
});
|
||||
|
||||
setQrCodeUrl(qrDataUrl);
|
||||
|
||||
// Start polling for response
|
||||
await mobileLoginRef.current.startPolling(
|
||||
async (result: MobileLoginResult) => {
|
||||
try {
|
||||
// Call success callback (parent handles loading state)
|
||||
await onSuccess(result);
|
||||
// Close modal after successful processing
|
||||
handleClose();
|
||||
} catch {
|
||||
// Show error if success handler fails and hide QR code
|
||||
setQrCodeUrl(null);
|
||||
setError(MobileLoginErrorCode.GENERIC);
|
||||
}
|
||||
},
|
||||
(errorCode) => {
|
||||
// Hide QR code when error occurs
|
||||
setQrCodeUrl(null);
|
||||
setError(errorCode);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
// err is a MobileLoginErrorCode thrown by initiate()
|
||||
if (typeof err === 'string' && Object.values(MobileLoginErrorCode).includes(err as MobileLoginErrorCode)) {
|
||||
setError(err as MobileLoginErrorCode);
|
||||
} else {
|
||||
setError(MobileLoginErrorCode.GENERIC);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initiateMobileLogin();
|
||||
|
||||
// Cleanup on unmount or when modal closes
|
||||
return (): void => {
|
||||
if (mobileLoginRef.current) {
|
||||
mobileLoginRef.current.cleanup();
|
||||
}
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
/**
|
||||
* Handle modal close.
|
||||
*/
|
||||
const handleClose = (): void => {
|
||||
if (mobileLoginRef.current) {
|
||||
mobileLoginRef.current.cleanup();
|
||||
}
|
||||
if (countdownIntervalRef.current) {
|
||||
clearInterval(countdownIntervalRef.current);
|
||||
}
|
||||
setQrCodeUrl(null);
|
||||
setError(null);
|
||||
setTimeRemaining(120);
|
||||
onClose();
|
||||
};
|
||||
|
||||
/**
|
||||
* Format time remaining as MM:SS.
|
||||
*/
|
||||
const formatTime = (seconds: number): string => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = mode === 'unlock' ? t('auth.unlockWithMobile') : t('auth.loginWithMobile');
|
||||
const description = t('auth.scanQrCode');
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 bg-black bg-opacity-80 transition-opacity" onClick={handleClose} />
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<div className="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-6 pb-6 pt-5 text-left shadow-xl transition-all w-full max-w-md">
|
||||
{/* Close button */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<span className="sr-only">{t('common.close')}</span>
|
||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="mt-3">
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-400 dark:border-red-700 rounded text-red-700 dark:text-red-400 text-sm">
|
||||
{getErrorMessage(error)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{qrCodeUrl && (
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<img src={qrCodeUrl} alt="QR Code" className="border-4 border-gray-200 dark:border-gray-600 rounded mb-3" />
|
||||
<div className="text-gray-700 dark:text-gray-300 text-sm font-medium">
|
||||
{formatTime(timeRemaining)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!qrCodeUrl && !error && (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="mt-4 w-full inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileUnlockModal;
|
||||
@@ -45,18 +45,18 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
const [selectedDomain, setSelectedDomain] = useState('');
|
||||
const [isPopupVisible, setIsPopupVisible] = useState(false);
|
||||
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
|
||||
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get private email domains from vault metadata
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Load private email domains from vault metadata.
|
||||
* Load private email domains from vault metadata, excluding hidden ones.
|
||||
*/
|
||||
const loadDomains = async (): Promise<void> => {
|
||||
const metadata = await dbContext.getVaultMetadata();
|
||||
if (metadata?.privateEmailDomains) {
|
||||
setPrivateEmailDomains(metadata.privateEmailDomains);
|
||||
}
|
||||
setPrivateEmailDomains(metadata?.privateEmailDomains ?? []);
|
||||
setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []);
|
||||
};
|
||||
loadDomains();
|
||||
}, [dbContext]);
|
||||
@@ -84,9 +84,10 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
setLocalPart(local);
|
||||
setSelectedDomain(domain);
|
||||
|
||||
// Check if it's a custom domain
|
||||
// Check if it's a custom domain (including hidden private domains as known domains)
|
||||
const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
|
||||
privateEmailDomains.includes(domain);
|
||||
privateEmailDomains.includes(domain) ||
|
||||
hiddenPrivateEmailDomains.includes(domain);
|
||||
setIsCustomDomain(!isKnownDomain);
|
||||
} else {
|
||||
setLocalPart(value);
|
||||
@@ -102,7 +103,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value, privateEmailDomains, showPrivateDomains]);
|
||||
}, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]);
|
||||
|
||||
// Handle local part changes
|
||||
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -246,20 +247,22 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
|
||||
{t('credentials.privateEmailDescription')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{privateEmailDomains.map((domain) => (
|
||||
<button
|
||||
key={domain}
|
||||
type="button"
|
||||
onClick={() => selectDomain(domain)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
selectedDomain === domain
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
{domain}
|
||||
</button>
|
||||
))}
|
||||
{privateEmailDomains
|
||||
.filter((domain) => !hiddenPrivateEmailDomains.includes(domain))
|
||||
.map((domain) => (
|
||||
<button
|
||||
key={domain}
|
||||
type="button"
|
||||
onClick={() => selectDomain(domain)}
|
||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
||||
selectedDomain === domain
|
||||
? 'bg-primary-600 text-white hover:bg-primary-700'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
{domain}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -46,7 +46,7 @@ const DefaultLayout: React.FC<DefaultLayoutProps> = ({ routes, headerButtons, me
|
||||
height: 'calc(100% - 120px)',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 mb-16">
|
||||
<div className="px-4 pb-4 pt-2 mb-16">
|
||||
{message && (
|
||||
<p className="text-red-500 mb-4">{message}</p>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
|
||||
/**
|
||||
* Username avatar component that shows the avatar and username.
|
||||
* Displays centered above the unlock form.
|
||||
*/
|
||||
const UsernameAvatar: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { username } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<div className="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center mb-3">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-2xl font-medium">
|
||||
{username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="font-medium text-gray-900 dark:text-white text-base">
|
||||
{username}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('auth.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsernameAvatar;
|
||||
@@ -4,6 +4,7 @@ import { sendMessage } from 'webext-bridge/popup';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
import { removeAndDisablePin } from '@/utils/PinUnlockService';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
@@ -73,6 +74,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
await storage.removeItems(['local:username', 'local:accessToken', 'local:refreshToken']);
|
||||
dbContext?.clearDatabase();
|
||||
|
||||
// Clear PIN unlock data (if any)
|
||||
try {
|
||||
await removeAndDisablePin();
|
||||
} catch (error) {
|
||||
console.error('Failed to remove PIN data:', error);
|
||||
// Non-fatal error - continue with logout
|
||||
}
|
||||
|
||||
// Set local storage global message that will be shown on the login page.
|
||||
if (errorMessage) {
|
||||
setGlobalMessage(errorMessage);
|
||||
|
||||
@@ -8,6 +8,8 @@ import SqliteClient from '@/utils/SqliteClient';
|
||||
import { StoreVaultRequest } from '@/utils/types/messaging/StoreVaultRequest';
|
||||
import type { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
type DbContextType = {
|
||||
sqliteClient: SqliteClient | null;
|
||||
dbInitialized: boolean;
|
||||
@@ -42,11 +44,6 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
*/
|
||||
const [dbAvailable, setDbAvailable] = useState(false);
|
||||
|
||||
/**
|
||||
* Vault revision.
|
||||
*/
|
||||
const [vaultMetadata, setVaultMetadata] = useState<VaultMetadata | null>(null);
|
||||
|
||||
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string) => {
|
||||
// Attempt to decrypt the blob.
|
||||
const decryptedBlob = await EncryptionUtility.symmetricDecrypt(
|
||||
@@ -61,19 +58,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
setVaultMetadata({
|
||||
publicEmailDomains: vaultResponse.vault.publicEmailDomainList,
|
||||
privateEmailDomains: vaultResponse.vault.privateEmailDomainList,
|
||||
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
|
||||
});
|
||||
|
||||
/**
|
||||
* Store encrypted vault in background worker.
|
||||
* Store encrypted vault and metadata in background worker (session storage).
|
||||
*/
|
||||
const request: StoreVaultRequest = {
|
||||
vaultBlob: vaultResponse.vault.blob,
|
||||
publicEmailDomainList: vaultResponse.vault.publicEmailDomainList,
|
||||
privateEmailDomainList: vaultResponse.vault.privateEmailDomainList,
|
||||
hiddenPrivateEmailDomainList: vaultResponse.vault.hiddenPrivateEmailDomainList,
|
||||
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
|
||||
};
|
||||
|
||||
@@ -92,12 +85,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
setSqliteClient(client);
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(true);
|
||||
|
||||
setVaultMetadata({
|
||||
publicEmailDomains: response.publicEmailDomains ?? [],
|
||||
privateEmailDomains: response.privateEmailDomains ?? [],
|
||||
vaultRevisionNumber: response.vaultRevisionNumber ?? 0,
|
||||
});
|
||||
// Metadata is already stored in session storage by background worker
|
||||
} else {
|
||||
setDbInitialized(true);
|
||||
setDbAvailable(false);
|
||||
@@ -110,22 +98,37 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get the vault metadata.
|
||||
* Get the vault metadata from session storage.
|
||||
*/
|
||||
const getVaultMetadata = useCallback(async () : Promise<VaultMetadata | null> => {
|
||||
return vaultMetadata;
|
||||
}, [vaultMetadata]);
|
||||
try {
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[] | null;
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[] | null;
|
||||
const hiddenPrivateEmailDomains = await storage.getItem('session:hiddenPrivateEmailDomains') as string[] | null;
|
||||
const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number | null;
|
||||
|
||||
if (!publicEmailDomains && !privateEmailDomains) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
publicEmailDomains: publicEmailDomains ?? [],
|
||||
privateEmailDomains: privateEmailDomains ?? [],
|
||||
hiddenPrivateEmailDomains: hiddenPrivateEmailDomains ?? [],
|
||||
vaultRevisionNumber: vaultRevisionNumber ?? 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting vault metadata from session storage:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Set the current vault revision number.
|
||||
* Set the current vault revision number in session storage.
|
||||
*/
|
||||
const setCurrentVaultRevisionNumber = useCallback(async (revisionNumber: number) => {
|
||||
setVaultMetadata({
|
||||
publicEmailDomains: vaultMetadata?.publicEmailDomains ?? [],
|
||||
privateEmailDomains: vaultMetadata?.privateEmailDomains ?? [],
|
||||
vaultRevisionNumber: revisionNumber,
|
||||
});
|
||||
}, [vaultMetadata]);
|
||||
await storage.setItem('session:vaultRevisionNumber', revisionNumber);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check if there are pending migrations.
|
||||
|
||||
@@ -74,9 +74,9 @@ export function useVaultMutate() : {
|
||||
// Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later.
|
||||
throw new Error('Vault merge required. Please login via the web app to merge the multiple pending updates to your vault.');
|
||||
} else if (response.status === 2) {
|
||||
throw new Error(t('common.errors.failedToUploadVault'));
|
||||
throw new Error(t('common.errors.unknownError'));
|
||||
} else {
|
||||
throw new Error(t('common.errors.failedToUploadVault'));
|
||||
throw new Error(t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
// Check if it's a network error
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULT_OPTIONS: ApiOption[] = [
|
||||
*/
|
||||
const createUrlSchema = (t: (key: string) => string): Yup.ObjectSchema<{apiUrl: string; clientUrl: string}> => Yup.object().shape({
|
||||
apiUrl: Yup.string()
|
||||
.required(t('validation.apiUrlRequired'))
|
||||
.required(t('settings.validation.apiUrlRequired'))
|
||||
.test('is-valid-api-url', t('settings.validation.apiUrlInvalid'), (value: string | undefined) => {
|
||||
if (!value) {
|
||||
return true; // Allow empty for non-custom option
|
||||
@@ -39,7 +39,7 @@ const createUrlSchema = (t: (key: string) => string): Yup.ObjectSchema<{apiUrl:
|
||||
}
|
||||
}),
|
||||
clientUrl: Yup.string()
|
||||
.required(t('validation.clientUrlRequired'))
|
||||
.required(t('settings.validation.clientUrlRequired'))
|
||||
.test('is-valid-client-url', t('settings.validation.clientUrlInvalid'), (value: string | undefined) => {
|
||||
if (!value) {
|
||||
return true; // Allow empty for non-custom option
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import MobileUnlockModal from '@/entrypoints/popup/components/Dialogs/MobileUnlockModal';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import LoginServerInfo from '@/entrypoints/popup/components/LoginServerInfo';
|
||||
@@ -21,6 +22,7 @@ import { AppInfo } from '@/utils/AppInfo';
|
||||
import type { VaultResponse, LoginResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
import type { MobileLoginResult } from '@/utils/types/messaging/MobileLoginResult';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
@@ -47,6 +49,7 @@ const Login: React.FC = () => {
|
||||
const [twoFactorCode, setTwoFactorCode] = useState('');
|
||||
const [clientUrl, setClientUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showMobileLoginModal, setShowMobileLoginModal] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
@@ -88,7 +91,7 @@ const Login: React.FC = () => {
|
||||
}
|
||||
} catch (err) {
|
||||
await app.logout();
|
||||
setError(err instanceof Error ? err.message : t('auth.errors.migrationError'));
|
||||
setError(err instanceof Error ? err.message : t('common.errors.unknownError'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +193,7 @@ const Login: React.FC = () => {
|
||||
|
||||
// Check if token was returned.
|
||||
if (!validationResponse.token) {
|
||||
throw new Error(t('auth.errors.noToken'));
|
||||
throw new Error(t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
// Handle successful authentication
|
||||
@@ -242,7 +245,7 @@ const Login: React.FC = () => {
|
||||
|
||||
// Check if token was returned.
|
||||
if (!validationResponse.token) {
|
||||
throw new Error(t('auth.errors.noToken'));
|
||||
throw new Error(t('common.errors.unknownError'));
|
||||
}
|
||||
|
||||
// Handle successful authentication
|
||||
@@ -272,6 +275,63 @@ const Login: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle successful mobile login
|
||||
*/
|
||||
const handleMobileLoginSuccess = async (result: MobileLoginResult): Promise<void> => {
|
||||
showLoading();
|
||||
try {
|
||||
// Clear global message if set
|
||||
app.clearGlobalMessage();
|
||||
|
||||
// Fetch vault from server with the new auth token
|
||||
const vaultResponse = await webApi.authFetch<VaultResponse>('Vault', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${result.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Store auth tokens and username
|
||||
await app.setAuthTokens(result.username, result.token, result.refreshToken);
|
||||
|
||||
// Store the encryption key and derivation params
|
||||
await dbContext.storeEncryptionKey(result.decryptionKey);
|
||||
await dbContext.storeEncryptionKeyDerivationParams({
|
||||
salt: result.salt,
|
||||
encryptionType: result.encryptionType,
|
||||
encryptionSettings: result.encryptionSettings,
|
||||
});
|
||||
|
||||
// Initialize the database with the vault data
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponse, result.decryptionKey);
|
||||
|
||||
// Check for pending migrations
|
||||
try {
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
setIsInitialLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
await app.logout();
|
||||
setError(err instanceof Error ? err.message : t('common.errors.unknownError'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to reinitialize page
|
||||
hideLoading();
|
||||
setIsInitialLoading(false);
|
||||
navigate('/reinitialize', { replace: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : t('common.errors.unknownError'));
|
||||
hideLoading();
|
||||
throw err; // Re-throw to let modal show error
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle change
|
||||
*/
|
||||
@@ -330,7 +390,7 @@ const Login: React.FC = () => {
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
{t('auth.cancel')}
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-4 text-center">
|
||||
@@ -342,82 +402,113 @@ const Login: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
{/* Title */}
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">{t('auth.loginTitle')}</h2>
|
||||
<LoginServerInfo />
|
||||
</div>
|
||||
)}
|
||||
<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 font-bold mb-2" htmlFor="username">
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<input
|
||||
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"
|
||||
placeholder={t('auth.usernamePlaceholder')}
|
||||
value={credentials.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-medium mb-2" htmlFor="username">
|
||||
{t('auth.username')}
|
||||
</label>
|
||||
<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}
|
||||
className="shadow appearance-none border rounded-lg w-full py-2 px-3 text-gray-700 dark:text-gray-200 dark:bg-gray-700 dark:border-gray-600 leading-tight focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder={t('auth.usernamePlaceholder')}
|
||||
value={credentials.username}
|
||||
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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">{t('auth.rememberMe')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-medium mb-2" htmlFor="password">
|
||||
{t('auth.password')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="shadow appearance-none border rounded-lg w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-700 dark:border-gray-600 leading-tight focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-200">{t('auth.rememberMe')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button type="submit">
|
||||
{t('auth.loginButton')}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{t('auth.loginButton')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center text-gray-600 dark:text-gray-400">
|
||||
{t('auth.noAccount')}{' '}
|
||||
<a
|
||||
href={clientUrl ?? ''}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-600 dark:text-orange-400 dark:hover:text-orange-500"
|
||||
>
|
||||
{t('auth.createVault')}
|
||||
</a>
|
||||
|
||||
{/* Mobile Login Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMobileLoginModal(true)}
|
||||
className="w-full max-w-md mt-4 px-4 py-2 text-sm font-medium text-center text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:bg-gray-600 dark:text-white dark:border-gray-500 dark:hover:bg-gray-500 dark:focus:ring-gray-700 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{t('auth.loginWithMobile')}
|
||||
</button>
|
||||
|
||||
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
|
||||
{t('auth.noAccount')}{' '}
|
||||
<a
|
||||
href={clientUrl ?? ''}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-600 dark:text-orange-400 dark:hover:text-orange-500"
|
||||
>
|
||||
{t('auth.createVault')}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Mobile Login Modal */}
|
||||
<MobileUnlockModal
|
||||
isOpen={showMobileLoginModal}
|
||||
onClose={() => setShowMobileLoginModal(false)}
|
||||
onSuccess={handleMobileLoginSuccess}
|
||||
webApi={webApi}
|
||||
mode="login"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import AlertMessage from '@/entrypoints/popup/components/AlertMessage';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import MobileUnlockModal from '@/entrypoints/popup/components/Dialogs/MobileUnlockModal';
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIcon, HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
import UsernameAvatar from '@/entrypoints/popup/components/Unlock/UsernameAvatar';
|
||||
import { useApp } from '@/entrypoints/popup/context/AppContext';
|
||||
import { useAuth } from '@/entrypoints/popup/context/AuthContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
@@ -19,12 +22,27 @@ import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
import type { VaultResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import {
|
||||
getPinLength,
|
||||
isPinEnabled,
|
||||
PinLockedError,
|
||||
IncorrectPinError,
|
||||
InvalidPinFormatError,
|
||||
resetFailedAttempts,
|
||||
unlockWithPin
|
||||
} from '@/utils/PinUnlockService';
|
||||
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
|
||||
import type { MobileLoginResult } from '@/utils/types/messaging/MobileLoginResult';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Unlock page
|
||||
* Unlock mode type
|
||||
*/
|
||||
type UnlockMode = 'pin' | 'password';
|
||||
|
||||
/**
|
||||
* Unified unlock page that handles both PIN and password unlock
|
||||
*/
|
||||
const Unlock: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -37,11 +55,27 @@ const Unlock: React.FC = () => {
|
||||
const webApi = useWebApi();
|
||||
const srpUtil = new SrpUtility(webApi);
|
||||
|
||||
// Unlock mode state
|
||||
const [unlockMode, setUnlockMode] = useState<UnlockMode>('password');
|
||||
const [pinAvailable, setPinAvailable] = useState<boolean>(false);
|
||||
|
||||
// Password unlock state
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// PIN unlock state
|
||||
const [pin, setPin] = useState('');
|
||||
const [pinLength, setPinLength] = useState<number>(6);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Common state
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
|
||||
|
||||
// Mobile unlock state
|
||||
const [showMobileUnlockModal, setShowMobileUnlockModal] = useState(false);
|
||||
|
||||
/**
|
||||
* Make status call to API which acts as health check.
|
||||
* This runs only once during component mount.
|
||||
@@ -64,8 +98,35 @@ const Unlock: React.FC = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize unlock page - check status and PIN availability
|
||||
*/
|
||||
useEffect(() => {
|
||||
checkStatus();
|
||||
/**
|
||||
* Initialize unlock page - check status and PIN availability
|
||||
*/
|
||||
const initialize = async (): Promise<void> => {
|
||||
// First check PIN availability and set initial mode
|
||||
const [pinEnabled, pinLength] = await Promise.all([
|
||||
isPinEnabled(),
|
||||
getPinLength(),
|
||||
]);
|
||||
|
||||
setPinAvailable(pinEnabled);
|
||||
setPinLength(pinLength || 6);
|
||||
|
||||
// Default to PIN mode if available, otherwise password
|
||||
if (pinEnabled) {
|
||||
setUnlockMode('pin');
|
||||
} else {
|
||||
setUnlockMode('password');
|
||||
}
|
||||
|
||||
// Then check API status
|
||||
await checkStatus();
|
||||
};
|
||||
|
||||
initialize();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Only run once on mount
|
||||
|
||||
@@ -87,9 +148,56 @@ const Unlock: React.FC = () => {
|
||||
}, [setHeaderButtons, t]);
|
||||
|
||||
/**
|
||||
* Handle submit
|
||||
* Keep input focused for PIN mode
|
||||
*/
|
||||
const handleSubmit = async (e: React.FormEvent) : Promise<void> => {
|
||||
useEffect(() => {
|
||||
if (unlockMode !== 'pin') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the hidden input element
|
||||
*/
|
||||
const focusInput = (): void => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-focus input whenever user clicks anywhere on the page
|
||||
*/
|
||||
const handleClick = (): void => {
|
||||
focusInput();
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-focus input when window/extension regains focus
|
||||
*/
|
||||
const handleFocus = (): void => {
|
||||
focusInput();
|
||||
};
|
||||
|
||||
focusInput();
|
||||
|
||||
const container = containerRef.current;
|
||||
if (container) {
|
||||
container.addEventListener('click', handleClick);
|
||||
}
|
||||
window.addEventListener('focus', handleFocus);
|
||||
|
||||
return (): void => {
|
||||
if (container) {
|
||||
container.removeEventListener('click', handleClick);
|
||||
}
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
};
|
||||
}, [unlockMode]);
|
||||
|
||||
/**
|
||||
* Handle password unlock
|
||||
*/
|
||||
const handlePasswordSubmit = async (e: React.FormEvent) : Promise<void> => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
showLoading();
|
||||
@@ -131,9 +239,12 @@ const Unlock: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
|
||||
// Clear dismiss until
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
|
||||
// Reset PIN failed attempts on successful password unlock
|
||||
await resetFailedAttempts();
|
||||
|
||||
navigate('/reinitialize', { replace: true });
|
||||
} catch (err) {
|
||||
// Check if it's a version incompatibility error
|
||||
@@ -148,6 +259,101 @@ const Unlock: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle PIN input change
|
||||
*/
|
||||
const handlePinChange = useCallback(async (newPin: string): Promise<void> => {
|
||||
setPin(newPin);
|
||||
setError(null);
|
||||
|
||||
// Auto-submit when PIN length is reached
|
||||
if (newPin.length === pinLength) {
|
||||
// Small delay to allow UI to update with the last digit before showing loading spinner
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await handlePinUnlock(newPin);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pinLength]);
|
||||
|
||||
/**
|
||||
* Handle numpad button click
|
||||
*/
|
||||
const handleNumpadClick = (digit: string): void => {
|
||||
if (pin.length < 8) {
|
||||
handlePinChange(pin + digit);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle backspace
|
||||
*/
|
||||
const handleBackspace = (): void => {
|
||||
setPin(pin.slice(0, -1));
|
||||
setError(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle PIN unlock
|
||||
*/
|
||||
const handlePinUnlock = async (pinToUse: string = pin): Promise<void> => {
|
||||
if (pinToUse.length !== pinLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
// Unlock with PIN
|
||||
const passwordHashBase64 = await unlockWithPin(pinToUse);
|
||||
|
||||
// Get latest vault from API
|
||||
const vaultResponseJson = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
// Store the encryption key in session storage
|
||||
await dbContext.storeEncryptionKey(passwordHashBase64);
|
||||
|
||||
// Initialize the SQLite context with the vault data
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
|
||||
|
||||
// Check if there are pending migrations
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear dismiss until
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
|
||||
navigate('/reinitialize', { replace: true });
|
||||
hideLoading();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof PinLockedError) {
|
||||
setPinAvailable(false);
|
||||
setUnlockMode('password');
|
||||
setError(t('settings.unlockMethod.pinLocked'));
|
||||
} else if (err instanceof IncorrectPinError) {
|
||||
/* Show translatable error with attempts remaining */
|
||||
const attemptsRemaining = err.attemptsRemaining;
|
||||
if (attemptsRemaining === 1) {
|
||||
setError(t('settings.unlockMethod.incorrectPinSingular'));
|
||||
} else {
|
||||
setError(t('settings.unlockMethod.incorrectPin', { attemptsRemaining }));
|
||||
}
|
||||
setPin('');
|
||||
} else if (err instanceof InvalidPinFormatError) {
|
||||
setError(t('settings.unlockMethod.invalidPinFormat'));
|
||||
setPin('');
|
||||
} else {
|
||||
console.error('PIN unlock failed:', err);
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
setPin('');
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout
|
||||
*/
|
||||
@@ -155,73 +361,259 @@ const Unlock: React.FC = () => {
|
||||
app.logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="bg-white dark:bg-gray-700 w-full shadow-md rounded px-8 pt-6 pb-8 mb-4">
|
||||
{/* User Avatar and Username Section */}
|
||||
<div className="flex items-center space-x-3 mb-6">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{authContext.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
/**
|
||||
* Handle successful mobile unlock
|
||||
*/
|
||||
const handleMobileUnlockSuccess = async (result: MobileLoginResult): Promise<void> => {
|
||||
showLoading();
|
||||
try {
|
||||
// Revoke current tokens before setting new ones (since we're already logged in)
|
||||
await webApi.revokeTokens();
|
||||
|
||||
// Set new auth tokens
|
||||
await authContext.setAuthTokens(result.username, result.token, result.refreshToken);
|
||||
|
||||
// Fetch vault from server with the new auth token
|
||||
const vaultResponse = await webApi.get<VaultResponse>('Vault');
|
||||
|
||||
// Store the encryption key and derivation params
|
||||
await dbContext.storeEncryptionKey(result.decryptionKey);
|
||||
await dbContext.storeEncryptionKeyDerivationParams({
|
||||
salt: result.salt,
|
||||
encryptionType: result.encryptionType,
|
||||
encryptionSettings: result.encryptionSettings,
|
||||
});
|
||||
|
||||
// Initialize the database with the vault data
|
||||
const sqliteClient = await dbContext.initializeDatabase(vaultResponse, result.decryptionKey);
|
||||
|
||||
// Check if there are pending migrations
|
||||
if (await sqliteClient.hasPendingMigrations()) {
|
||||
navigate('/upgrade', { replace: true });
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear dismiss until
|
||||
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
|
||||
|
||||
// Reset PIN failed attempts on successful unlock
|
||||
await resetFailedAttempts();
|
||||
|
||||
navigate('/reinitialize', { replace: true });
|
||||
} catch (err) {
|
||||
// Check if it's a version incompatibility error
|
||||
if (err instanceof VaultVersionIncompatibleError) {
|
||||
await app.logout(err.message);
|
||||
} else {
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
}
|
||||
console.error('Mobile unlock error:', err);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch to password mode
|
||||
*/
|
||||
const switchToPassword = () : void => {
|
||||
setUnlockMode('password');
|
||||
setError(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch to PIN mode
|
||||
*/
|
||||
const switchToPin = () : void => {
|
||||
setUnlockMode('pin');
|
||||
setError(null);
|
||||
};
|
||||
|
||||
// Generate PIN dots display
|
||||
const pinDots = Array.from({ length: pinLength }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-4 h-4 rounded-full border-2 transition-all ${
|
||||
i < pin.length
|
||||
? 'bg-primary-500 border-primary-500'
|
||||
: 'bg-transparent border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
/>
|
||||
));
|
||||
|
||||
// Render PIN unlock UI
|
||||
if (unlockMode === 'pin') {
|
||||
return (
|
||||
<div ref={containerRef} className="flex flex-col items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* User Avatar and Username Section */}
|
||||
<UsernameAvatar />
|
||||
|
||||
{/* Main Content Card */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
{/* Title */}
|
||||
<div className="text-center mb-4">
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white mb-1">
|
||||
{t('auth.unlockTitle')}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('auth.enterPinToUnlock')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* PIN Dots Display */}
|
||||
<div className="flex justify-center gap-2 mb-4">
|
||||
{pinDots}
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && <AlertMessage type="error" message={error} className="mb-3 text-center" />}
|
||||
|
||||
{/* Hidden Input for Keyboard Entry */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="password"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={8}
|
||||
value={pin}
|
||||
onChange={(e) => handlePinChange(e.target.value.replace(/\D/g, ''))}
|
||||
className="w-0 h-0 opacity-0 absolute"
|
||||
autoFocus
|
||||
aria-label="PIN input"
|
||||
/>
|
||||
|
||||
{/* On-Screen Numpad */}
|
||||
<div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{/* Numbers 1-9 */}
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
|
||||
<button
|
||||
key={num}
|
||||
type="button"
|
||||
onClick={() => handleNumpadClick(num.toString())}
|
||||
className="h-12 flex items-center justify-center text-xl font-semibold bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg transition-colors active:scale-95"
|
||||
>
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Empty space, 0, Backspace */}
|
||||
<div />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleNumpadClick('0')}
|
||||
className="h-12 flex items-center justify-center text-xl font-semibold bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg transition-colors active:scale-95"
|
||||
>
|
||||
0
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBackspace}
|
||||
className="h-12 flex items-center justify-center bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-lg transition-colors active:scale-95"
|
||||
aria-label="Backspace"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M3 12l6.414 6.414a2 2 0 001.414.586H19a2 2 0 002-2V7a2 2 0 00-2-2h-8.172a2 2 0 00-1.414.586L3 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{authContext.username}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('auth.loggedIn')}
|
||||
</p>
|
||||
|
||||
{/* Use Password Button */}
|
||||
<div className="mt-4">
|
||||
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
|
||||
<button type="button" onClick={switchToPassword} className="text-primary-600 hover:text-primary-700 dark:text-primary-500 dark:hover:text-primary-400 hover:underline font-medium">{t('auth.useMasterPassword')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Instruction Title */}
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{t('auth.unlockTitle')}
|
||||
</h2>
|
||||
// Render password unlock UI
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* User Avatar and Username Section */}
|
||||
<UsernameAvatar />
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 text-red-500 dark:text-red-400">
|
||||
{error}
|
||||
{/* Main Content Card */}
|
||||
<form onSubmit={handlePasswordSubmit} className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
{/* Title */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
{t('auth.unlockTitle')}
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-bold mb-2" htmlFor="password">
|
||||
{t('auth.masterPassword')}
|
||||
</label>
|
||||
<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>
|
||||
{/* Error Message */}
|
||||
{error && <AlertMessage type="error" message={error} className="mb-4 text-center" />}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-medium mb-2" htmlFor="password">
|
||||
{t('auth.masterPassword')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
className="shadow appearance-none border rounded-lg w-full py-2 px-3 pr-10 text-gray-700 dark:text-gray-200 dark:bg-gray-700 dark:border-gray-600 leading-tight focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
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>
|
||||
</div>
|
||||
|
||||
<Button type="submit">
|
||||
{t('auth.unlockVault')}
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{t('auth.unlockVault')}
|
||||
</Button>
|
||||
|
||||
<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>
|
||||
{/* Mobile Unlock Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMobileUnlockModal(true)}
|
||||
className="w-full max-w-md mt-4 px-4 py-2 text-sm font-medium text-center text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:bg-gray-600 dark:text-white dark:border-gray-500 dark:hover:bg-gray-500 dark:focus:ring-gray-700 flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{t('auth.unlockWithMobile')}
|
||||
</button>
|
||||
|
||||
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
|
||||
{t('auth.switchAccounts')} <button type="button" onClick={handleLogout} className="text-primary-600 hover:text-primary-700 dark:text-primary-500 dark:hover:text-primary-400 hover:underline font-medium">{t('auth.logout')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{pinAvailable && (
|
||||
<div className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
|
||||
<button type="button" onClick={switchToPin} className="text-primary-600 hover:text-primary-700 dark:text-primary-500 dark:hover:text-primary-400 hover:underline font-medium">{t('auth.unlockWithPin')}</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Mobile Unlock Modal */}
|
||||
<MobileUnlockModal
|
||||
isOpen={showMobileUnlockModal}
|
||||
onClose={() => setShowMobileUnlockModal(false)}
|
||||
onSuccess={handleMobileUnlockSuccess}
|
||||
webApi={webApi}
|
||||
mode="unlock"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { sendMessage } from 'webext-bridge/popup';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import AttachmentUploader from '@/entrypoints/popup/components/Credentials/Details/AttachmentUploader';
|
||||
import TotpEditor from '@/entrypoints/popup/components/Credentials/Details/TotpEditor';
|
||||
import Modal from '@/entrypoints/popup/components/Dialogs/Modal';
|
||||
import EmailDomainField from '@/entrypoints/popup/components/Forms/EmailDomainField';
|
||||
import { FormInput } from '@/entrypoints/popup/components/Forms/FormInput';
|
||||
@@ -24,8 +25,8 @@ 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 { IdentityHelperUtils, CreateIdentityGenerator, CreateUsernameEmailGenerator, Identity, Gender, convertAgeRangeToBirthdateOptions } from '@/utils/dist/shared/identity-generator';
|
||||
import type { Attachment, Credential, TotpCode } from '@/utils/dist/shared/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/shared/password-generator';
|
||||
import { ServiceDetectionUtility } from '@/utils/serviceDetection/ServiceDetectionUtility';
|
||||
|
||||
@@ -38,6 +39,13 @@ type PersistedFormData = {
|
||||
credentialId: string | null;
|
||||
mode: CredentialMode;
|
||||
formValues: Omit<Credential, 'Logo'> & { Logo?: string | null };
|
||||
totpEditorState?: {
|
||||
isAddFormVisible: boolean;
|
||||
formData: {
|
||||
name: string;
|
||||
secretKey: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +100,15 @@ const CredentialAddEdit: React.FC = () => {
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [originalAttachmentIds, setOriginalAttachmentIds] = useState<string[]>([]);
|
||||
const [totpCodes, setTotpCodes] = useState<TotpCode[]>([]);
|
||||
const [originalTotpCodeIds, setOriginalTotpCodeIds] = useState<string[]>([]);
|
||||
const [totpEditorState, setTotpEditorState] = useState<{
|
||||
isAddFormVisible: boolean;
|
||||
formData: { name: string; secretKey: string };
|
||||
}>({
|
||||
isAddFormVisible: false,
|
||||
formData: { name: '', secretKey: '' }
|
||||
});
|
||||
const [passkeyMarkedForDeletion, setPasskeyMarkedForDeletion] = useState(false);
|
||||
const webApi = useWebApi();
|
||||
|
||||
@@ -141,19 +158,20 @@ const CredentialAddEdit: React.FC = () => {
|
||||
formValues: {
|
||||
...formValues,
|
||||
Logo: null // Don't persist the Logo field as it can't be user modified in the UI.
|
||||
}
|
||||
},
|
||||
totpEditorState
|
||||
};
|
||||
await sendMessage('PERSIST_FORM_VALUES', JSON.stringify(persistedData), 'background');
|
||||
}, [watch, id, mode, localLoading]);
|
||||
}, [watch, id, mode, localLoading, totpEditorState]);
|
||||
|
||||
/**
|
||||
* Watch for mode changes and persist form values
|
||||
* Watch for mode and totpEditorState changes and persist form values
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!localLoading) {
|
||||
void persistFormValues();
|
||||
}
|
||||
}, [mode, persistFormValues, localLoading]);
|
||||
}, [mode, totpEditorState, persistFormValues, localLoading]);
|
||||
|
||||
// Watch for form changes and persist them
|
||||
useEffect(() => {
|
||||
@@ -200,6 +218,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
Object.entries(persistedDataObject.formValues).forEach(([key, value]) => {
|
||||
setValue(key as keyof Credential, value as Credential[keyof Credential]);
|
||||
});
|
||||
|
||||
// Restore TOTP editor state if it exists
|
||||
if (persistedDataObject.totpEditorState) {
|
||||
setTotpEditorState(persistedDataObject.totpEditorState);
|
||||
}
|
||||
} else {
|
||||
console.error('Persisted values do not match current page');
|
||||
}
|
||||
@@ -330,6 +353,11 @@ const CredentialAddEdit: React.FC = () => {
|
||||
setAttachments(credentialAttachments);
|
||||
setOriginalAttachmentIds(credentialAttachments.map(a => a.Id));
|
||||
|
||||
// Load TOTP codes for this credential
|
||||
const credentialTotpCodes = dbContext.sqliteClient.getTotpCodesForCredential(id);
|
||||
setTotpCodes(credentialTotpCodes);
|
||||
setOriginalTotpCodeIds(credentialTotpCodes.map(tc => tc.Id));
|
||||
|
||||
setMode('manual');
|
||||
setIsInitialLoading(false);
|
||||
|
||||
@@ -370,8 +398,8 @@ const CredentialAddEdit: React.FC = () => {
|
||||
* Initialize the identity and password generators with settings from user's vault.
|
||||
*/
|
||||
const initializeGenerators = useCallback(async () => {
|
||||
// Get default identity language from database
|
||||
const identityLanguage = dbContext.sqliteClient!.getDefaultIdentityLanguage();
|
||||
// Get effective identity language (smart default based on UI language if no explicit override)
|
||||
const identityLanguage = await dbContext.sqliteClient!.getEffectiveIdentityLanguage();
|
||||
|
||||
// Initialize identity generator based on language
|
||||
const identityGenerator = CreateIdentityGenerator(identityLanguage);
|
||||
@@ -392,15 +420,15 @@ const CredentialAddEdit: React.FC = () => {
|
||||
// Get gender preference from database
|
||||
const genderPreference = dbContext.sqliteClient!.getDefaultIdentityGender();
|
||||
|
||||
// Generate identity with gender preference
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference);
|
||||
// Get age range preference and convert to birthdate options
|
||||
const ageRange = dbContext.sqliteClient!.getDefaultIdentityAgeRange();
|
||||
const birthdateOptions = convertAgeRangeToBirthdateOptions(ageRange);
|
||||
|
||||
// Generate identity with gender preference and birthdate options (null is handled by generator)
|
||||
const identity = identityGenerator.generateRandomIdentity(genderPreference, birthdateOptions);
|
||||
const password = passwordGenerator.generateRandomPassword();
|
||||
|
||||
const metadata = await dbContext.getVaultMetadata();
|
||||
|
||||
const privateEmailDomains = metadata?.privateEmailDomains ?? [];
|
||||
const publicEmailDomains = metadata?.publicEmailDomains ?? [];
|
||||
const defaultEmailDomain = dbContext.sqliteClient!.getDefaultEmailDomain(privateEmailDomains, publicEmailDomains);
|
||||
const defaultEmailDomain = await dbContext.sqliteClient!.getDefaultEmailDomain();
|
||||
const email = defaultEmailDomain ? `${identity.emailPrefix}@${defaultEmailDomain}` : identity.emailPrefix;
|
||||
|
||||
// Check current values
|
||||
@@ -463,36 +491,57 @@ const CredentialAddEdit: React.FC = () => {
|
||||
|
||||
const generateRandomUsername = useCallback(async () => {
|
||||
try {
|
||||
const usernameEmailGenerator = CreateUsernameEmailGenerator();
|
||||
const firstName = watch('Alias.FirstName') ?? '';
|
||||
const lastName = watch('Alias.LastName') ?? '';
|
||||
const nickName = watch('Alias.NickName') ?? '';
|
||||
const birthDate = watch('Alias.BirthDate') ?? '';
|
||||
|
||||
let gender = Gender.Other;
|
||||
try {
|
||||
gender = watch('Alias.Gender') as Gender;
|
||||
} catch {
|
||||
// Gender parsing failed, default to other.
|
||||
let username: string;
|
||||
|
||||
// If alias fields are empty, generate a completely random username
|
||||
if (!firstName && !lastName && !nickName && !birthDate) {
|
||||
const { identityGenerator } = await initializeGenerators();
|
||||
const genderPreference = dbContext.sqliteClient!.getDefaultIdentityGender();
|
||||
const ageRange = dbContext.sqliteClient!.getDefaultIdentityAgeRange();
|
||||
const birthdateOptions = convertAgeRangeToBirthdateOptions(ageRange);
|
||||
const randomIdentity = identityGenerator.generateRandomIdentity(genderPreference, birthdateOptions);
|
||||
username = randomIdentity.nickName;
|
||||
} else {
|
||||
// Generate username based on current identity fields
|
||||
const usernameEmailGenerator = CreateUsernameEmailGenerator();
|
||||
|
||||
let gender = Gender.Other;
|
||||
try {
|
||||
gender = watch('Alias.Gender') as Gender;
|
||||
} catch {
|
||||
// Gender parsing failed, default to other.
|
||||
}
|
||||
|
||||
// Parse birthDate, fallback to current date if invalid
|
||||
let parsedBirthDate = new Date(birthDate);
|
||||
if (!birthDate || isNaN(parsedBirthDate.getTime())) {
|
||||
parsedBirthDate = new Date();
|
||||
}
|
||||
|
||||
const identity: Identity = {
|
||||
firstName,
|
||||
lastName,
|
||||
nickName,
|
||||
gender,
|
||||
birthDate: parsedBirthDate,
|
||||
emailPrefix: watch('Alias.Email') ?? '',
|
||||
};
|
||||
|
||||
username = usernameEmailGenerator.generateUsername(identity);
|
||||
}
|
||||
|
||||
const identity: Identity = {
|
||||
firstName: watch('Alias.FirstName') ?? '',
|
||||
lastName: watch('Alias.LastName') ?? '',
|
||||
nickName: watch('Alias.NickName') ?? '',
|
||||
gender: gender,
|
||||
birthDate: new Date(watch('Alias.BirthDate') ?? ''),
|
||||
emailPrefix: watch('Alias.Email') ?? '',
|
||||
};
|
||||
|
||||
const username = usernameEmailGenerator.generateUsername(identity);
|
||||
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 }));
|
||||
}
|
||||
setValue('Username', username);
|
||||
// Update the tracking for username
|
||||
setLastGeneratedValues(prev => ({ ...prev, username }));
|
||||
} catch (error) {
|
||||
console.error('Error generating random username:', error);
|
||||
}
|
||||
}, [setValue, watch, lastGeneratedValues, setLastGeneratedValues]);
|
||||
}, [setValue, watch, setLastGeneratedValues, initializeGenerators, dbContext.sqliteClient]);
|
||||
|
||||
/**
|
||||
* Handle form submission.
|
||||
@@ -518,7 +567,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
data.Alias.FirstName = watch('Alias.FirstName');
|
||||
data.Alias.LastName = watch('Alias.LastName');
|
||||
data.Alias.NickName = watch('Alias.NickName');
|
||||
data.Alias.BirthDate = birthdate;
|
||||
data.Alias.BirthDate = watch('Alias.BirthDate');
|
||||
data.Alias.Gender = watch('Alias.Gender');
|
||||
data.Alias.Email = watch('Alias.Email');
|
||||
// Clean up ServiceUrl for random mode too
|
||||
@@ -550,14 +599,14 @@ const CredentialAddEdit: React.FC = () => {
|
||||
setLocalLoading(false);
|
||||
|
||||
if (isEditMode) {
|
||||
await dbContext.sqliteClient!.updateCredentialById(data, originalAttachmentIds, attachments);
|
||||
await dbContext.sqliteClient!.updateCredentialById(data, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes);
|
||||
|
||||
// Delete passkeys if marked for deletion
|
||||
if (passkeyMarkedForDeletion) {
|
||||
await dbContext.sqliteClient!.deletePasskeysByCredentialId(data.Id);
|
||||
}
|
||||
} else {
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(data, attachments);
|
||||
const credentialId = await dbContext.sqliteClient!.createCredential(data, attachments, totpCodes);
|
||||
data.Id = credentialId.toString();
|
||||
}
|
||||
}, {
|
||||
@@ -576,7 +625,7 @@ const CredentialAddEdit: React.FC = () => {
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments, passkeyMarkedForDeletion]);
|
||||
}, [isEditMode, dbContext.sqliteClient, executeVaultMutation, navigate, mode, watch, generateRandomAlias, webApi, clearPersistedValues, originalAttachmentIds, attachments, originalTotpCodeIds, totpCodes, passkeyMarkedForDeletion]);
|
||||
|
||||
// Set header buttons on mount and clear on unmount
|
||||
useEffect((): (() => void) => {
|
||||
@@ -953,6 +1002,15 @@ const CredentialAddEdit: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TotpEditor
|
||||
totpCodes={totpCodes}
|
||||
onTotpCodesChange={setTotpCodes}
|
||||
originalTotpCodeIds={originalTotpCodeIds}
|
||||
isAddFormVisible={totpEditorState.isAddFormVisible}
|
||||
formData={totpEditorState.formData}
|
||||
onStateChange={setTotpEditorState}
|
||||
/>
|
||||
|
||||
<AttachmentUploader
|
||||
attachments={attachments}
|
||||
onAttachmentsChange={setAttachments}
|
||||
|
||||
@@ -158,7 +158,7 @@ const EmailDetails: React.FC = (): React.ReactElement => {
|
||||
)}
|
||||
<HeaderButton
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
title={t('emails.deleteEmail')}
|
||||
title={t('emails.deleteEmailTitle')}
|
||||
iconType={HeaderIconType.DELETE}
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
|
||||
import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner';
|
||||
@@ -12,6 +11,7 @@ import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedirect';
|
||||
|
||||
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
|
||||
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
|
||||
import type { GetRequest, PasskeyGetCredentialResponse, PendingPasskeyGetRequest, StoredPasskeyRecord } from '@/utils/passkey/types';
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
|
||||
import Alert from '@/entrypoints/popup/components/Alert';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import PasskeyBypassDialog from '@/entrypoints/popup/components/Dialogs/PasskeyBypassDialog';
|
||||
@@ -16,6 +15,7 @@ import { useVaultLockRedirect } from '@/entrypoints/popup/hooks/useVaultLockRedi
|
||||
import { useVaultMutate } from '@/entrypoints/popup/hooks/useVaultMutate';
|
||||
|
||||
import { PASSKEY_DISABLED_SITES_KEY } from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import type { Passkey } from '@/utils/dist/shared/models/vault';
|
||||
import { PasskeyAuthenticator } from '@/utils/passkey/PasskeyAuthenticator';
|
||||
import { PasskeyHelper } from '@/utils/passkey/PasskeyHelper';
|
||||
|
||||
@@ -50,8 +50,8 @@ const AutoLockSettings: React.FC = () => {
|
||||
<div className="flex items-center mb-2">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</p>
|
||||
<HelpModal
|
||||
titleKey="settings.autoLockTimeout"
|
||||
contentKey="settings.autoLockTimeoutHelp"
|
||||
title={t('settings.autoLockTimeout')}
|
||||
content={t('settings.autoLockTimeoutHelp')}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AutofillMatchingMode } from '@/entrypoints/contentScript/Filter';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import {
|
||||
DISABLED_SITES_KEY,
|
||||
GLOBAL_AUTOFILL_POPUP_ENABLED_KEY,
|
||||
import {
|
||||
DISABLED_SITES_KEY,
|
||||
GLOBAL_AUTOFILL_POPUP_ENABLED_KEY,
|
||||
TEMPORARY_DISABLED_SITES_KEY,
|
||||
AUTOFILL_MATCHING_MODE_KEY
|
||||
AUTOFILL_MATCHING_MODE_KEY
|
||||
} from '@/utils/Constants';
|
||||
import { AutofillMatchingMode } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
@@ -180,7 +180,7 @@ const AutofillSettings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
{settings.isGloballyEnabled ? t('common.enabled') : t('common.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +214,7 @@ const AutofillSettings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
{settings.isEnabled ? t('common.enabled') : t('common.disabled')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ const ContextMenuSettings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{isContextMenuEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
{isContextMenuEnabled ? t('common.enabled') : t('common.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { extractDomain, extractRootDomain } from '@/entrypoints/contentScript/Filter';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import {
|
||||
PASSKEY_PROVIDER_ENABLED_KEY,
|
||||
PASSKEY_DISABLED_SITES_KEY
|
||||
} from '@/utils/Constants';
|
||||
import { extractDomain, extractRootDomain } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
|
||||
import { storage, browser } from "#imports";
|
||||
|
||||
@@ -147,7 +147,7 @@ const PasskeySettings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isGloballyEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
{settings.isGloballyEnabled ? t('common.enabled') : t('common.disabled')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ const PasskeySettings: React.FC = () => {
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{settings.isEnabled ? t('settings.enabled') : t('settings.disabled')}
|
||||
{settings.isEnabled ? t('common.enabled') : t('common.disabled')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import HeaderButton from '@/entrypoints/popup/components/HeaderButton';
|
||||
import { HeaderIconType } from '@/entrypoints/popup/components/Icons/HeaderIcons';
|
||||
@@ -13,7 +14,7 @@ import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
|
||||
import { browser } from "#imports";
|
||||
import { browser, storage } from "#imports";
|
||||
|
||||
/**
|
||||
* Settings page component.
|
||||
@@ -26,15 +27,16 @@ const Settings: React.FC = () => {
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
const navigate = useNavigate();
|
||||
const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false);
|
||||
|
||||
/**
|
||||
* Open the client tab.
|
||||
*/
|
||||
const openClientTab = async () : Promise<void> => {
|
||||
const settingClientUrl = await browser.storage.local.get('clientUrl');
|
||||
const settingClientUrl = await storage.getItem('local:clientUrl') as string | undefined;
|
||||
let clientUrl = AppInfo.DEFAULT_CLIENT_URL;
|
||||
if (settingClientUrl?.clientUrl && settingClientUrl.clientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl.clientUrl;
|
||||
if (settingClientUrl && settingClientUrl.length > 0) {
|
||||
clientUrl = settingClientUrl;
|
||||
}
|
||||
|
||||
window.open(clientUrl, '_blank');
|
||||
@@ -109,9 +111,21 @@ const Settings: React.FC = () => {
|
||||
* Handle logout.
|
||||
*/
|
||||
const handleLogout = async () : Promise<void> => {
|
||||
setShowLogoutConfirm(false);
|
||||
app.logout();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle lock vault.
|
||||
*/
|
||||
const handleLock = async () : Promise<void> => {
|
||||
// Clear the vault which will lock it and require user to login again
|
||||
await sendMessage('CLEAR_VAULT', {}, 'background');
|
||||
|
||||
// Navigate to unlock page
|
||||
navigate('/unlock');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to autofill settings.
|
||||
*/
|
||||
@@ -140,6 +154,13 @@ const Settings: React.FC = () => {
|
||||
navigate('/settings/auto-lock');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to unlock method settings.
|
||||
*/
|
||||
const navigateToUnlockMethodSettings = () : void => {
|
||||
navigate('/settings/unlock-method');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to context menu settings.
|
||||
*/
|
||||
@@ -155,331 +176,410 @@ const Settings: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('settings.title')}</h2>
|
||||
</div>
|
||||
|
||||
{/* User Menu Section */}
|
||||
<section>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{app.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text font-medium text-gray-900 dark:text-white">
|
||||
{app.username}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('settings.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{/* Logout Confirmation Modal */}
|
||||
{showLogoutConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full mx-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
||||
{t('auth.logout')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t('auth.logoutConfirm')}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setShowLogoutConfirm(false)}
|
||||
className="flex-1 px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
title={t('settings.logout')}
|
||||
className="p-2 bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 rounded-md transition-colors"
|
||||
className="flex-1 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
aria-label={t('settings.logout')}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
{t('settings.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Settings Navigation Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.preferences')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{/* Autofill Settings */}
|
||||
<button
|
||||
onClick={navigateToAutofillSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.autofillSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Passkey Settings */}
|
||||
<button
|
||||
onClick={navigateToPasskeySettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.passkeySettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Context Menu Settings */}
|
||||
<button
|
||||
onClick={navigateToContextMenuSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 6h16M4 12h16m-7 6h7"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.contextMenuSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Auto-lock Settings */}
|
||||
<button
|
||||
onClick={navigateToAutoLockSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Clipboard Settings */}
|
||||
<button
|
||||
onClick={navigateToClipboardSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.clipboardSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Language Settings */}
|
||||
<button
|
||||
onClick={navigateToLanguageSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.language')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-gray-900 dark:text-white text-xl">{t('settings.title')}</h2>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.appearance')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="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
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="system"
|
||||
checked={theme === 'system'}
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="light"
|
||||
checked={theme === 'light'}
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="dark"
|
||||
checked={theme === 'dark'}
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keyboard Shortcuts Section */}
|
||||
{import.meta.env.CHROME && (
|
||||
{/* User Menu Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.keyboardShortcuts')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
|
||||
<span className="text-primary-600 dark:text-primary-400 text-lg font-medium">
|
||||
{app.username?.[0]?.toUpperCase() || '?'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text font-medium text-gray-900 dark:text-white">
|
||||
{app.username}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{t('settings.loggedIn')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleLock}
|
||||
title={t('settings.lock')}
|
||||
className="p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-600 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 rounded-md transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
aria-label={t('settings.lock')}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowLogoutConfirm(true)}
|
||||
title={t('settings.logout')}
|
||||
className="p-2 bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 rounded-md transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
aria-label={t('settings.logout')}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={openKeyboardShortcuts}
|
||||
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('settings.configure')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
{t('settings.versionPrefix')}{AppInfo.VERSION} ({getDisplayUrl()})
|
||||
{/* Settings Navigation Section */}
|
||||
<section>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{/* Vault Unlock Method */}
|
||||
<button
|
||||
onClick={navigateToUnlockMethodSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.unlockMethod.title')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Auto-lock Settings */}
|
||||
<button
|
||||
onClick={navigateToAutoLockSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.autoLockTimeout')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Autofill Settings */}
|
||||
<button
|
||||
onClick={navigateToAutofillSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.autofillSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Passkey Settings */}
|
||||
<button
|
||||
onClick={navigateToPasskeySettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.passkeySettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Additional Settings Section */}
|
||||
<section>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{/* Clipboard Settings */}
|
||||
<button
|
||||
onClick={navigateToClipboardSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.clipboardSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Context Menu Settings */}
|
||||
<button
|
||||
onClick={navigateToContextMenuSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 6h16M4 12h16m-7 6h7"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-gray-900 dark:text-white">{t('settings.contextMenuSettings')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Language Settings */}
|
||||
<button
|
||||
onClick={navigateToLanguageSettings}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-3 text-gray-600 dark:text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
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-gray-900 dark:text-white">{t('settings.language')}</span>
|
||||
</div>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Appearance Settings Section */}
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.appearance')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<p className="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
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="system"
|
||||
checked={theme === 'system'}
|
||||
onChange={() => setThemePreference('system')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.useDefault')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="light"
|
||||
checked={theme === 'light'}
|
||||
onChange={() => setThemePreference('light')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.light')}</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme"
|
||||
value="dark"
|
||||
checked={theme === 'dark'}
|
||||
onChange={() => setThemePreference('dark')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{t('settings.dark')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keyboard Shortcuts Section */}
|
||||
{import.meta.env.CHROME && (
|
||||
<section>
|
||||
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-3">{t('settings.keyboardShortcuts')}</h3>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.configureKeyboardShortcuts')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={openKeyboardShortcuts}
|
||||
className="px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-md transition-colors"
|
||||
>
|
||||
{t('settings.configure')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="text-center text-gray-400 dark:text-gray-600">
|
||||
{t('settings.versionPrefix')}{AppInfo.VERSION} ({getDisplayUrl()})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import AlertMessage from '@/entrypoints/popup/components/AlertMessage';
|
||||
import Button from '@/entrypoints/popup/components/Button';
|
||||
import HelpModal from '@/entrypoints/popup/components/Dialogs/HelpModal';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
|
||||
import {
|
||||
isPinEnabled,
|
||||
setupPin,
|
||||
removeAndDisablePin,
|
||||
isValidPin,
|
||||
isPinLocked,
|
||||
InvalidPinFormatError
|
||||
} from '@/utils/PinUnlockService';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Vault unlock method settings page component.
|
||||
*/
|
||||
const VaultUnlockSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const dbContext = useDb();
|
||||
const { setIsInitialLoading, showLoading, hideLoading } = useLoading();
|
||||
|
||||
const [pinEnabled, setPinEnabled] = useState<boolean | undefined>(undefined);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [showPinSetup, setShowPinSetup] = useState<boolean>(false);
|
||||
const [pinSetupStep, setPinSetupStep] = useState<number>(1); // 1 = enter, 2 = confirm
|
||||
const [newPin, setNewPin] = useState<string>('');
|
||||
const [confirmPin, setConfirmPin] = useState<string>('');
|
||||
const [isLocked, setIsLocked] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Load PIN settings.
|
||||
*/
|
||||
const loadSettings = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const [enabled, locked] = await Promise.all([
|
||||
isPinEnabled(),
|
||||
isPinLocked()
|
||||
]);
|
||||
|
||||
setPinEnabled(enabled);
|
||||
setIsLocked(locked);
|
||||
setIsInitialLoading(false);
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to load PIN settings:', err);
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
setIsInitialLoading(false);
|
||||
}
|
||||
}, [setIsInitialLoading, t]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
|
||||
/**
|
||||
* Handle enable PIN - show setup modal.
|
||||
*/
|
||||
const handleEnablePin = (): void => {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
// Check if we have the encryption key in memory
|
||||
if (!dbContext.dbAvailable) {
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
return;
|
||||
}
|
||||
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setShowPinSetup(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle PIN setup submission (step 1: enter PIN).
|
||||
*/
|
||||
const handlePinSetupNext = (e: React.FormEvent): void => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
// Validate PIN format
|
||||
if (!isValidPin(newPin)) {
|
||||
setError(t('settings.unlockMethod.invalidPinFormat'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to confirmation step
|
||||
setPinSetupStep(2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle PIN setup submission (step 2: confirm PIN).
|
||||
*/
|
||||
const handlePinSetupSubmit = async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
// Validate PIN confirmation
|
||||
if (newPin !== confirmPin) {
|
||||
setError(t('settings.unlockMethod.pinMismatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
/* Get the encryption key from session storage */
|
||||
const encryptionKeyResponse = await storage.getItem('session:encryptionKey') as string | undefined;
|
||||
const encryptionKey = encryptionKeyResponse as string;
|
||||
|
||||
if (!encryptionKey) {
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
hideLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Setup PIN with the encryption key */
|
||||
await setupPin(newPin, encryptionKey);
|
||||
|
||||
setPinEnabled(true);
|
||||
setShowPinSetup(false);
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setSuccess(t('settings.unlockMethod.enableSuccess'));
|
||||
hideLoading();
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to enable PIN:', err);
|
||||
|
||||
if (err instanceof InvalidPinFormatError) {
|
||||
setError(t('settings.unlockMethod.invalidPinFormat'));
|
||||
} else {
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle disable PIN.
|
||||
*/
|
||||
const handleDisablePin = async (): Promise<void> => {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
showLoading();
|
||||
await removeAndDisablePin();
|
||||
setPinEnabled(false);
|
||||
setIsLocked(false);
|
||||
hideLoading();
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to disable PIN:', err);
|
||||
setError(t('common.errors.unknownErrorTryAgain'));
|
||||
hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start gap-2">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('settings.unlockMethod.introText')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
{/* Error/Success Messages */}
|
||||
{error && <AlertMessage type="error" message={error} className="mb-4" />}
|
||||
{success && <AlertMessage type="success" message={success} className="mb-4" />}
|
||||
|
||||
{/* Locked Warning */}
|
||||
{isLocked && (
|
||||
<AlertMessage
|
||||
type="warning"
|
||||
message={t('settings.unlockMethod.pinLocked')}
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* PIN Code option */}
|
||||
<section>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-5 h-5 mr-2 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.unlockMethod.pin')}</p>
|
||||
<HelpModal
|
||||
title={t('common.notice')}
|
||||
content={t('settings.unlockMethod.pinSecurityWarning')}
|
||||
className="ml-2"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm mt-1 text-gray-600 dark:text-gray-400">
|
||||
{t('settings.unlockMethod.pinDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{pinEnabled !== undefined && (
|
||||
<button
|
||||
onClick={pinEnabled && !isLocked ? handleDisablePin : handleEnablePin}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
pinEnabled && !isLocked
|
||||
? 'bg-green-500 hover:bg-green-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{pinEnabled && !isLocked ? t('common.enabled') : t('common.disabled')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Master Password option (always enabled, cannot be toggled) */}
|
||||
<section className="mt-4">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/30 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<svg className="w-5 h-5 mr-2 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{t('settings.unlockMethod.password')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-2 rounded-md bg-green-500 text-white hover:bg-green-600 cursor-not-allowed">
|
||||
{t('common.enabled')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* PIN Setup Modal */}
|
||||
{showPinSetup && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 relative">
|
||||
{/* Cancel button in top right corner */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowPinSetup(false);
|
||||
setPinSetupStep(1);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setError(null);
|
||||
}}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Cancel"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Step 1: Enter PIN */}
|
||||
{pinSetupStep === 1 && (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 pr-8">
|
||||
{t('settings.unlockMethod.setupPin')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{t('settings.unlockMethod.enterNewPinDescription')}
|
||||
</p>
|
||||
<form onSubmit={handlePinSetupNext}>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="password"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={8}
|
||||
value={newPin}
|
||||
onChange={(e) => setNewPin(e.target.value.replace(/\D/g, ''))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-center text-2xl tracking-widest"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{error && <AlertMessage type="error" message={error} className="mb-4" />}
|
||||
<Button type="submit">
|
||||
{t('common.next')}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 2: Confirm PIN */}
|
||||
{pinSetupStep === 2 && (
|
||||
<>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 pr-8">
|
||||
{t('settings.unlockMethod.confirmPin')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{t('settings.unlockMethod.confirmPinDescription')}
|
||||
</p>
|
||||
<form onSubmit={handlePinSetupSubmit}>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="password"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={8}
|
||||
value={confirmPin}
|
||||
onChange={(e) => setConfirmPin(e.target.value.replace(/\D/g, ''))}
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-center text-2xl tracking-widest"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
{error && <AlertMessage type="error" message={error} className="mb-4" />}
|
||||
<Button type="submit">
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VaultUnlockSettings;
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Error codes for mobile login operations.
|
||||
* These codes are used to provide translatable error messages to users.
|
||||
*/
|
||||
export enum MobileLoginErrorCode {
|
||||
/**
|
||||
* The mobile login request has timed out after 2 minutes.
|
||||
*/
|
||||
TIMEOUT = 'TIMEOUT',
|
||||
|
||||
/**
|
||||
* A generic error occurred during mobile login.
|
||||
*/
|
||||
GENERIC = 'GENERIC',
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import { MobileLoginErrorCode } from '@/entrypoints/popup/types/MobileLoginErrorCode';
|
||||
|
||||
import type { LoginResponse, MobileLoginInitiateResponse, MobileLoginPollResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import type { MobileLoginResult } from '@/utils/types/messaging/MobileLoginResult';
|
||||
import type { WebApiService } from '@/utils/WebApiService';
|
||||
|
||||
/**
|
||||
* Utility class for mobile login operations
|
||||
*/
|
||||
export class MobileLoginUtility {
|
||||
private webApi: WebApiService;
|
||||
private pollingInterval: NodeJS.Timeout | null = null;
|
||||
private requestId: string | null = null;
|
||||
private privateKey: string | null = null;
|
||||
|
||||
/**
|
||||
* Constructor for the MobileLoginUtility class.
|
||||
*
|
||||
* @param {WebApiService} webApi - The WebApiService instance.
|
||||
*/
|
||||
public constructor(webApi: WebApiService) {
|
||||
this.webApi = webApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a mobile login request and returns the QR code data
|
||||
* @throws {MobileLoginErrorCode} If initiation fails
|
||||
*/
|
||||
public async initiate(): Promise<string> {
|
||||
try {
|
||||
// Generate RSA key pair
|
||||
const keyPair = await EncryptionUtility.generateRsaKeyPair();
|
||||
this.privateKey = keyPair.privateKey;
|
||||
|
||||
// Send public key to server (no auth required)
|
||||
const response = await this.webApi.rawFetch('auth/mobile-login/initiate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientPublicKey: keyPair.publicKey,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw MobileLoginErrorCode.GENERIC;
|
||||
}
|
||||
|
||||
const data = await response.json() as MobileLoginInitiateResponse;
|
||||
this.requestId = data.requestId;
|
||||
|
||||
// Return QR code data (request ID)
|
||||
return this.requestId;
|
||||
} catch (error) {
|
||||
if (typeof error === 'string' && Object.values(MobileLoginErrorCode).includes(error as MobileLoginErrorCode)) {
|
||||
throw error;
|
||||
}
|
||||
throw MobileLoginErrorCode.GENERIC;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts polling the server for mobile login response
|
||||
*/
|
||||
public async startPolling(
|
||||
onSuccess: (result: MobileLoginResult) => void,
|
||||
onError: (errorCode: MobileLoginErrorCode) => void
|
||||
): Promise<void> {
|
||||
if (!this.requestId || !this.privateKey) {
|
||||
throw new Error('Must call initiate() before starting polling');
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls the server for mobile login response
|
||||
*/
|
||||
const pollFn = async (): Promise<void> => {
|
||||
try {
|
||||
if (!this.requestId) {
|
||||
this.stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.webApi.rawFetch(
|
||||
`auth/mobile-login/poll/${this.requestId}`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
// Request expired or not found
|
||||
this.stopPolling();
|
||||
this.privateKey = null;
|
||||
this.requestId = null;
|
||||
onError(MobileLoginErrorCode.TIMEOUT);
|
||||
return;
|
||||
}
|
||||
throw new Error(`Polling failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as MobileLoginPollResponse;
|
||||
|
||||
if (data.fulfilled && data.encryptedSymmetricKey) {
|
||||
// Stop polling
|
||||
this.stopPolling();
|
||||
|
||||
// Decrypt the encrypted decryption key with RSA private key
|
||||
const decryptionKeyBytes = await EncryptionUtility.decryptWithPrivateKey(data.encryptedDecryptionKey!, this.privateKey!);
|
||||
const decryptionKey = Buffer.from(decryptionKeyBytes).toString('base64');
|
||||
|
||||
// Decrypt the other encrypted fields with the symmetric key
|
||||
const symmetricKeyBytes = await EncryptionUtility.decryptWithPrivateKey(data.encryptedSymmetricKey, this.privateKey!);
|
||||
const symmetricKey = Buffer.from(symmetricKeyBytes).toString('base64');
|
||||
|
||||
const token = await EncryptionUtility.symmetricDecrypt(data.encryptedToken!, symmetricKey);
|
||||
const refreshToken = await EncryptionUtility.symmetricDecrypt(data.encryptedRefreshToken!, symmetricKey);
|
||||
const username = await EncryptionUtility.symmetricDecrypt(data.encryptedUsername!, symmetricKey);
|
||||
|
||||
// Clear sensitive data
|
||||
this.privateKey = null;
|
||||
this.requestId = null;
|
||||
|
||||
// Call /login endpoint with username to get salt and encryption settings
|
||||
const loginResponse = await this.webApi.rawFetch('auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
onError(MobileLoginErrorCode.GENERIC);
|
||||
return;
|
||||
}
|
||||
|
||||
const loginData = await loginResponse.json() as LoginResponse;
|
||||
|
||||
// Create result object using the MobileLoginResult type
|
||||
const result: MobileLoginResult = {
|
||||
username: username,
|
||||
token: token,
|
||||
refreshToken: refreshToken,
|
||||
decryptionKey: decryptionKey,
|
||||
salt: loginData.salt,
|
||||
encryptionType: loginData.encryptionType,
|
||||
encryptionSettings: loginData.encryptionSettings,
|
||||
};
|
||||
|
||||
// Call success callback with result object
|
||||
onSuccess(result);
|
||||
|
||||
}
|
||||
} catch {
|
||||
this.stopPolling();
|
||||
this.privateKey = null;
|
||||
this.requestId = null;
|
||||
onError(MobileLoginErrorCode.GENERIC);
|
||||
}
|
||||
};
|
||||
|
||||
// Poll every 3 seconds
|
||||
this.pollingInterval = setInterval(pollFn, 3000);
|
||||
|
||||
// Stop polling after 3.5 minutes (adds 1.5 minute buffer to default 2 minute timer for edge cases)
|
||||
setTimeout(() => {
|
||||
if (this.pollingInterval) {
|
||||
this.stopPolling();
|
||||
this.privateKey = null;
|
||||
this.requestId = null;
|
||||
onError(MobileLoginErrorCode.TIMEOUT);
|
||||
}
|
||||
}, 210000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops polling the server
|
||||
*/
|
||||
public stopPolling(): void {
|
||||
if (this.pollingInterval) {
|
||||
clearInterval(this.pollingInterval);
|
||||
this.pollingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up resources
|
||||
*/
|
||||
public cleanup(): void {
|
||||
this.stopPolling();
|
||||
this.privateKey = null;
|
||||
this.requestId = null;
|
||||
}
|
||||
}
|
||||
@@ -521,13 +521,13 @@ export default defineUnlistedScript(() => {
|
||||
|
||||
// Re-apply get if it's missing our marker
|
||||
if (!(currentGet as any).__aliasVaultPatched) {
|
||||
console.warn('[AliasVault] Re-applying credentials.get patch');
|
||||
console.debug('[AliasVault] Re-applying credentials.get patch');
|
||||
navigator.credentials.get = getOverrideRef;
|
||||
}
|
||||
|
||||
// Re-apply create if it's missing our marker
|
||||
if (!(currentCreate as any).__aliasVaultPatched) {
|
||||
console.warn('[AliasVault] Re-applying credentials.create patch');
|
||||
console.debug('[AliasVault] Re-applying credentials.create patch');
|
||||
navigator.credentials.create = createOverrideRef;
|
||||
}
|
||||
};
|
||||
@@ -542,7 +542,7 @@ export default defineUnlistedScript(() => {
|
||||
|
||||
// Check for our marker
|
||||
if (!(get as any).__aliasVaultPatched || !(create as any).__aliasVaultPatched) {
|
||||
console.error('[AliasVault] CRITICAL: Monkey patch markers missing!', {
|
||||
console.debug('[AliasVault] WebAuthn patch markers missing', {
|
||||
hasGetMarker: !!(get as any).__aliasVaultPatched,
|
||||
hasCreateMarker: !!(create as any).__aliasVaultPatched
|
||||
});
|
||||
@@ -554,7 +554,7 @@ export default defineUnlistedScript(() => {
|
||||
|
||||
// Verify immediately
|
||||
if (!verifyPatches()) {
|
||||
console.error('[AliasVault] Initial verification failed - re-applying patches');
|
||||
console.debug('[AliasVault] WebAuthn initial patch markers missing - re-applying patches');
|
||||
applyPatches();
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ export default defineUnlistedScript(() => {
|
||||
const verifyInterval = setInterval(() => {
|
||||
checkCount++;
|
||||
if (!verifyPatches()) {
|
||||
console.error('[AliasVault] Periodic verification failed - re-applying patches!');
|
||||
console.debug('[AliasVault] WebAuthn periodic patch markers missing - re-applying patches');
|
||||
applyPatches();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
import deTranslations from './locales/de.json';
|
||||
import enTranslations from './locales/en.json';
|
||||
import esTranslations from './locales/es.json';
|
||||
import fiTranslations from './locales/fi.json';
|
||||
import frTranslations from './locales/fr.json';
|
||||
import heTranslations from './locales/he.json';
|
||||
import itTranslations from './locales/it.json';
|
||||
import nlTranslations from './locales/nl.json';
|
||||
@@ -26,9 +28,15 @@ export const LANGUAGE_RESOURCES = {
|
||||
en: {
|
||||
translation: enTranslations
|
||||
},
|
||||
es: {
|
||||
translation: esTranslations
|
||||
},
|
||||
fi: {
|
||||
translation: fiTranslations
|
||||
},
|
||||
fr: {
|
||||
translation: frTranslations
|
||||
},
|
||||
he: {
|
||||
translation: heTranslations
|
||||
},
|
||||
@@ -72,12 +80,24 @@ export const AVAILABLE_LANGUAGES: ILanguageConfig[] = [
|
||||
nativeName: 'English',
|
||||
flag: '🇺🇸'
|
||||
},
|
||||
{
|
||||
code: 'es',
|
||||
name: 'Spanish',
|
||||
nativeName: 'Español',
|
||||
flag: '🇪🇸'
|
||||
},
|
||||
{
|
||||
code: 'fi',
|
||||
name: 'Finnish',
|
||||
nativeName: 'Suomi',
|
||||
flag: '🇫🇮'
|
||||
},
|
||||
{
|
||||
code: 'fr',
|
||||
name: 'French',
|
||||
nativeName: 'Français',
|
||||
flag: '🇫🇷'
|
||||
},
|
||||
{
|
||||
code: 'he',
|
||||
name: 'Hebrew',
|
||||
|
||||
@@ -6,23 +6,22 @@
|
||||
"password": "Contrasenya",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"loginButton": "Log in",
|
||||
"noAccount": "No account yet?",
|
||||
"createVault": "Create new vault",
|
||||
"twoFactorTitle": "Please enter the authentication code from your authenticator app.",
|
||||
"authCode": "Codi d'autenticació",
|
||||
"authCodePlaceholder": "Introduïu el codi de 6 dígits",
|
||||
"verify": "Verifica",
|
||||
"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": "Contrasenya Mestra",
|
||||
"unlockVault": "Unlock Vault",
|
||||
"unlockVault": "Unlock",
|
||||
"unlockWithPin": "Unlock with PIN",
|
||||
"enterPinToUnlock": "Enter your PIN to unlock your vault",
|
||||
"useMasterPassword": "Use Master Password",
|
||||
"unlockTitle": "Unlock Your Vault",
|
||||
"unlockDescription": "Enter your master password to unlock your vault.",
|
||||
"logout": "Tanca la sessió",
|
||||
"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",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Connectant a",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginWithMobile": "Log in using Mobile App",
|
||||
"unlockWithMobile": "Unlock using Mobile App",
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
|
||||
"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.",
|
||||
"sessionExpired": "Your session has expired. Please log in again."
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"mobileLoginRequestExpired": "Mobile login request timed out. Please reload the page and try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "S'està carregant...",
|
||||
"notice": "Notice",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"use": "Utilitza",
|
||||
"delete": "Suprimeix",
|
||||
"save": "Save",
|
||||
"or": "Or",
|
||||
"close": "Tanca",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Mostra la contrasenya",
|
||||
"hidePassword": "Amaga la contrasenya",
|
||||
"showDetails": "Show details",
|
||||
"hideDetails": "Hide details",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
|
||||
"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.",
|
||||
"serverVersionTooOld": "The AliasVault server needs to be updated to a newer version in order to use this feature. Please contact the server admin if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"unknownErrorTryAgain": "An unknown error occurred. Please try again.",
|
||||
"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": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"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.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
@@ -164,7 +166,6 @@
|
||||
"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"
|
||||
@@ -176,27 +177,12 @@
|
||||
"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.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"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.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"lock": "Lock",
|
||||
"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",
|
||||
@@ -331,7 +316,6 @@
|
||||
"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",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred 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"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Vault Unlock Method",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "Master Password",
|
||||
"pin": "PIN Code",
|
||||
"pinDescription": "Unlock vault with PIN code",
|
||||
"setupPin": "Setup PIN Code",
|
||||
"enterNewPinDescription": "Enter a PIN code consisting of minimum 6 digits",
|
||||
"confirmPin": "Confirm PIN",
|
||||
"confirmPinDescription": "Enter your PIN again to confirm",
|
||||
"invalidPinFormat": "Invalid PIN format",
|
||||
"pinMismatch": "PINs do not match",
|
||||
"incorrectPin": "Incorrect PIN. {{attemptsRemaining}} attempts remaining.",
|
||||
"incorrectPinSingular": "Incorrect PIN. 1 attempt remaining.",
|
||||
"enableSuccess": "PIN unlock enabled successfully!",
|
||||
"pinLocked": "PIN unlock has been disabled. Please use your master password to unlock your vault.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProviderOn": "Passkey Provider on "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"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}})"
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
"authCode": "Sicherheits-Code",
|
||||
"authCodePlaceholder": "Gib den 6-stelligen Sicherheits-Code ein.",
|
||||
"verify": "Bestätige",
|
||||
"cancel": "Abbrechen",
|
||||
"twoFactorNote": "Hinweis: Wenn Du keinen Zugriff auf Dein Authentifizierungsgerät hast, kannst Du Deine Zwei-Faktor-Authentifizierung (2FA) mit einem Wiederherstellungscode zurücksetzen, indem Du Dich über die Website anmeldest.",
|
||||
"masterPassword": "Master-Passwort",
|
||||
"unlockVault": "Tresor entsperren",
|
||||
"unlockVault": "Entsperren",
|
||||
"unlockWithPin": "Mit PIN entsperren",
|
||||
"enterPinToUnlock": "Bitte gib Deine PIN zum Entsperren des Tresors ein",
|
||||
"useMasterPassword": "Master-Passwort benutzen",
|
||||
"unlockTitle": "Entsperre Deinen Tresor",
|
||||
"unlockDescription": "Bitte gib Dein Master-Passwort zum Entsperren des Tresors ein.",
|
||||
"logout": "Abmelden",
|
||||
"logoutConfirm": "Bist Du sicher, dass Du Dich abmelden möchtest?",
|
||||
"sessionExpired": "Deine Sitzung ist abgelaufen. Bitte melde Dich erneut an.",
|
||||
"unlockSuccess": "Tresor erfolgreich entsperrt!",
|
||||
"unlockSuccessTitle": "Ihr Tresor wurde erfolgreich entsperrt",
|
||||
"unlockSuccessDescription": "Du kannst jetzt die Autofill-Funktion in Anmeldeformularen in Deinem Browser nutzen.",
|
||||
"closePopup": "Popup schließen",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Verbinde zu",
|
||||
"switchAccounts": "Konto wechseln?",
|
||||
"loggedIn": "Angemeldet",
|
||||
"loginWithMobile": "Mit mobiler App anmelden",
|
||||
"unlockWithMobile": "Mit mobiler App entsperren",
|
||||
"scanQrCode": "Scanne diesen QR-Code mit Deiner AliasVault-App, um Dich anzumelden und Deinen Tresor zu entsperren.",
|
||||
"errors": {
|
||||
"invalidCode": "Bitte gib einen gültigen 6-stelligen Sicherheits-Code ein.",
|
||||
"serverError": "Der AliasVault-Server konnte nicht erreicht werden. Bitte versuche es später noch einmal oder kontaktiere den Support, falls das Problem weiterhin besteht.",
|
||||
"noToken": "Anmeldung fehlgeschlagen -- es wurde kein Token zurückgegeben",
|
||||
"migrationError": "Beim Prüfen auf ausstehende Migrationen ist ein Fehler aufgetreten.",
|
||||
"wrongPassword": "Falsches Passwort. Bitte versuche es erneut.",
|
||||
"accountLocked": "Das Konto wurde wegen zu vieler fehlgeschlagener Anmeldeversuche vorübergehend gesperrt.",
|
||||
"networkError": "Netzwerkfehler. Bitte überprüfe Deine Verbindung und versuche es erneut.",
|
||||
"sessionExpired": "Deine Sitzung ist abgelaufen. Bitte melde Dich erneut an."
|
||||
"sessionExpired": "Deine Sitzung ist abgelaufen. Bitte melde Dich erneut an.",
|
||||
"mobileLoginRequestExpired": "Zeitüberschreitung für mobile Anmeldeanforderungen. Bitte lade die Seite neu und versuche es erneut."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Einstellungen"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Laden...",
|
||||
"notice": "Hinweis",
|
||||
"error": "Fehler",
|
||||
"success": "Aktion erfolgreich",
|
||||
"cancel": "Abbrechen",
|
||||
"back": "Back",
|
||||
"confirm": "Bestätigen",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter",
|
||||
"use": "Benutzen",
|
||||
"delete": "Löschen",
|
||||
"or": "Or",
|
||||
"save": "Speichern",
|
||||
"or": "Oder",
|
||||
"close": "Schließen",
|
||||
"copied": "Kopiert!",
|
||||
"openInNewWindow": "In neuem Fenster öffnen",
|
||||
"language": "Sprache",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen",
|
||||
"showDetails": "Details anzeigen",
|
||||
"hideDetails": "Details ausblenden",
|
||||
"copyToClipboard": "In die Zwischenablage kopieren",
|
||||
"loadingEmails": "E-Mails werden geladen...",
|
||||
"loadingTotpCodes": "TOTP-Codes werden geladen...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "Diese Version der AliasVault-Browser-Erweiterung wird vom Server nicht mehr unterstützt. Bitte aktualisiere Deine Browser-Erweiterung auf die neueste Version.",
|
||||
"browserExtensionOutdated": "Diese Browser-Erweiterung ist veraltet und kann nicht verwendet werden, um auf diesen Tresor zuzugreifen. Bitte aktualisiere die Browser-Erweiterung, um fortzufahren.",
|
||||
"serverVersionNotSupported": "Der AliasVault-Server muss auf eine neuere Version aktualisiert werden, um diese Browser-Erweiterung nutzen zu können. Bitte kontaktiere den Support, falls Du Hilfe benötigst.",
|
||||
"serverVersionTooOld": "Der AliasVault-Server muss auf eine neuere Version aktualisiert werden, um dieses Feature nutzen zu können. Bitte kontaktiere den Administrator des Servers, falls Du Hilfe benötigst.",
|
||||
"unknownError": "Ein unbekannter Fehler ist aufgetreten",
|
||||
"unknownErrorTryAgain": "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut.",
|
||||
"vaultNotAvailable": "Tresor nicht verfügbar",
|
||||
"failedToRetrieveData": "Abruf der Daten fehlgeschlagen",
|
||||
"vaultIsLocked": "Der Tresor ist gesperrt.",
|
||||
"failedToUploadVault": "Das Hochladen des Tresors ist fehlgeschlagen",
|
||||
"passwordChanged": "Dein Passwort hat sich seit Deiner letzten Anmeldung geändert. Bitte melden Dich aus Sicherheitsgründen erneut an."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Ungültiger Wiederherstellungscode. Bitte versuche es erneut.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Aktualisierungstoken ist erforderlich.",
|
||||
"INVALID_REFRESH_TOKEN": "Ungültiger Aktualisierungstoken.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Aktualisierungstoken wurde erfolgreich widerrufen.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Die Registrierung eines neuen Kontos ist auf diesem Server derzeit deaktiviert. Bitte kontaktiere den Administrator.",
|
||||
"USERNAME_REQUIRED": "Der Benutzername ist erforderlich.",
|
||||
"USERNAME_ALREADY_IN_USE": "Benutzername ist bereits vergeben.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "oder",
|
||||
"new": "Neu",
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suche",
|
||||
"vaultLocked": "AliasVault ist gesperrt.",
|
||||
"creatingNewAlias": "Neuen Alias erstellen...",
|
||||
"noMatchesFound": "Keine Treffer gefunden",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Neues Passwort erzeugen",
|
||||
"togglePasswordVisibility": "Passwort ein-/ausblenden",
|
||||
"passwordCopiedToClipboard": "Passwort in die Zwischenablage kopiert",
|
||||
"enterEmailAndOrUsernameError": "E-Mail-Adresse und/oder Benutzername eingeben",
|
||||
"openAliasVaultToUpgrade": "Zum Aktualisieren AliasVault öffnen ",
|
||||
"vaultUpgradeRequired": "Aktualisierung des Tresors erforderlich.",
|
||||
"dismissPopup": "Popup schliessen"
|
||||
@@ -176,53 +177,28 @@
|
||||
"deleteCredential": "Zugang löschen",
|
||||
"credentialDetails": "Details zum Zugang",
|
||||
"serviceName": "Name des Dienstes",
|
||||
"serviceNamePlaceholder": "z. B. Gmail, Facebook, Bank",
|
||||
"website": "Webseite",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Benutzername",
|
||||
"usernamePlaceholder": "Benutzername eingeben",
|
||||
"password": "Passwort",
|
||||
"passwordPlaceholder": "Passwort eingeben",
|
||||
"generatePassword": "Passwort generieren",
|
||||
"copyPassword": "Passwort kopieren",
|
||||
"showPassword": "Passwort anzeigen",
|
||||
"hidePassword": "Passwort verbergen",
|
||||
"notes": "Notizen",
|
||||
"notesPlaceholder": "Zusätzliche Notizen...",
|
||||
"totp": "Zwei-Faktor-Authentifizierung",
|
||||
"totpCode": "TOTP-Code",
|
||||
"copyTotp": "TOTP kopieren",
|
||||
"totpSecret": "TOTP-Geheimcode",
|
||||
"totpSecretPlaceholder": "TOTP-Geheimcode eingeben",
|
||||
"noCredentials": "Keine Zugangsdaten gefunden",
|
||||
"noCredentialsDescription": "Erstelle Deinen ersten Zugang, um loszulegen",
|
||||
"searchPlaceholder": "Zugangsdaten suchen...",
|
||||
"welcomeTitle": "Willkommen bei AliasVault!",
|
||||
"welcomeDescription": "Du möchtest die AliasVault-Browser-Erweiterung verwenden? Navigiere zu einer Website und verwende das AliasVault-Popup-Fenster um einen neuen Zugang zu erstellen.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
"noAttachmentsFound": "No credentials with attachments found",
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"noPasskeysFound": "Es wurden noch kein Passkey erstellt. Ein Passkey wird durch den Besuch einer Website erzeugt, die Passkeys als Authentifizierungsmethode anbietet.",
|
||||
"noAttachmentsFound": "Es wurden keine Zugangsdaten mit Anhang gefunden",
|
||||
"noMatchingCredentials": "Keine passenden Zugangsdaten gefunden",
|
||||
"createdAt": "Erstellt",
|
||||
"updatedAt": "Zuletzt aktualisiert",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Formular ausfüllen",
|
||||
"deleteConfirm": "Bist Du sicher, dass Du diesen Zugang löschen möchtest?",
|
||||
"saveSuccess": "Zugang erfolgreich gespeichert.",
|
||||
"tags": "Schlagwörter",
|
||||
"addTag": "Schlagwort hinzufügen",
|
||||
"removeTag": "Schlagwort entfernen",
|
||||
"folder": "Ordner",
|
||||
"selectFolder": "Ordner auswählen",
|
||||
"createFolder": "Ordner erstellen",
|
||||
"saveCredential": "Zugang speichern",
|
||||
"deleteCredentialTitle": "Zugang löschen",
|
||||
"deleteCredentialConfirm": "Bist Du sicher, dass Du diesen Zugang löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"filters": {
|
||||
"all": "(All) Credentials",
|
||||
"all": "(Alle) Zugangsdaten",
|
||||
"passkeys": "Passkeys",
|
||||
"aliases": "Aliases",
|
||||
"userpass": "Passwords",
|
||||
"attachments": "Attachments"
|
||||
"aliases": "Aliase",
|
||||
"userpass": "Passwörter",
|
||||
"attachments": "Anhänge"
|
||||
},
|
||||
"randomAlias": "Zufälliger Alias",
|
||||
"manual": "Manuell",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Vollständige E-Mail-Adresse eingeben",
|
||||
"enterEmailPrefix": "E-Mail-Präfix eingeben"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "2FA-Code hinzufügen",
|
||||
"instructions": "Gib den Secret-Key ein, der von der Website angezeigt wird, bei der Du die Zwei-Faktor-Authentifizierung hinzufügen möchtest.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret-Key",
|
||||
"saveToViewCode": "Speichern, um den Code anzuzeigen",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Ungültiges Format des Secret-Key"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-Mails",
|
||||
"deleteEmailTitle": "E-Mail löschen",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Web-App öffnen",
|
||||
"loggedIn": "Angemeldet",
|
||||
"logout": "Abmelden",
|
||||
"lock": "Sperren",
|
||||
"globalSettings": "Allgemeine Einstellungen",
|
||||
"autofillPopup": "Autofill-Popup",
|
||||
"activeOnAllSites": "Auf allen Seiten aktiv (sofern nicht unten deaktiviert)",
|
||||
"disabledOnAllSites": "Auf allen Seiten deaktiviert",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"rightClickContextMenu": "Kontextmenü mit Rechtsklick",
|
||||
"autofillMatching": "Autofill-Übereinstimmung",
|
||||
"autofillMatchingMode": "Autofill-Übereinstimmungs-Modus",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Tastaturkürzel",
|
||||
"configureKeyboardShortcuts": "Tastaturkürzel konfigurieren",
|
||||
"configure": "Konfigurieren",
|
||||
"security": "Sicherheit",
|
||||
"clipboardClearTimeout": "Zwischenablage nach dem Kopieren automatisch löschen",
|
||||
"clipboardClearTimeoutDescription": "Zwischenablage nach dem Kopieren sensibler Daten automatisch löschen",
|
||||
"clipboardClearDisabled": "Niemals löschen",
|
||||
@@ -352,72 +336,85 @@
|
||||
"autoLock8Hours": "8 Stunden",
|
||||
"autoLock24Hours": "24 Stunden",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Einstellungen",
|
||||
"autofillSettings": "Autofill-Einstellungen",
|
||||
"clipboardSettings": "Zwischenablage-Einstellungen",
|
||||
"contextMenuSettings": "Kontextmenü-Einstellungen",
|
||||
"passkeySettings": "Passkey Settings",
|
||||
"passkeySettings": "Passkey-Einstellungen",
|
||||
"contextMenu": "Kontextmenü",
|
||||
"contextMenuEnabled": "Kontextmenü ist aktiviert",
|
||||
"contextMenuDisabled": "Kontextmenü ist deaktiviert",
|
||||
"contextMenuDescription": "Rechtsklicke auf Eingabefelder, um auf AliasVault-Optionen zuzugreifen",
|
||||
"selectLanguage": "Sprache auswählen",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
|
||||
"customApiUrl": "API URL",
|
||||
"customClientUrl": "Client URL",
|
||||
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
|
||||
"clientUrlHint": "The web interface URL of your self-hosted instance",
|
||||
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"serverConfiguration": "Serverkonfiguration",
|
||||
"serverConfigurationDescription": "AliasVault-Server-URL für selbstgehostete Instanzen konfigurieren",
|
||||
"customApiUrl": "API-URL",
|
||||
"customClientUrl": "Client-URL",
|
||||
"apiUrlHint": "Die API-Endpunkt-URL (normalerweise Client-URL + /api)",
|
||||
"clientUrlHint": "Die URL der Webschnittstelle Deiner selbst gehosteten Instanz",
|
||||
"autofillSettingsDescription": "Aktiviere oder deaktiviere das Autofill-Popup auf Webseiten",
|
||||
"autofillEnabledDescription": "Autofill-Vorschläge werden auf den Anmeldeformularen angezeigt",
|
||||
"autofillDisabledDescription": "Autofill-Vorschläge sind global deaktiviert",
|
||||
"languageSettings": "Sprache",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API-URL ist erforderlich",
|
||||
"apiUrlInvalid": "Bitte gib eine gültige API-URL ein",
|
||||
"clientUrlRequired": "Client-URL ist erforderlich",
|
||||
"clientUrlInvalid": "Bitte gib eine gültige Client-URL ein"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Methode zum Entsperren des Tresors",
|
||||
"introText": "Wähle, wie Du Deinen Tresor entsperren möchtest. Du kannst Dein Master-Passwort verwenden (immer verfügbar) oder einen PIN-Code für einen schnelleren Zugriff einrichten. Nach drei fehlgeschlagenen PIN-Versuchen musst Du Dein Master-Passwort verwenden.",
|
||||
"password": "Master-Passwort",
|
||||
"pin": "PIN-Code",
|
||||
"pinDescription": "Tresor mit PIN entsperren",
|
||||
"setupPin": "PIN-Code einrichten",
|
||||
"enterNewPinDescription": "PIN-Code aus mindestens 6 Ziffern eingeben",
|
||||
"confirmPin": "PIN bestätigen",
|
||||
"confirmPinDescription": "Zur Bestätigung PIN erneut eingeben",
|
||||
"invalidPinFormat": "Ungültiges PIN-Format",
|
||||
"pinMismatch": "PINs stimmen nicht überein",
|
||||
"incorrectPin": "Falsche PIN. {{attemptsRemaining}} verbleibende Versuche.",
|
||||
"incorrectPinSingular": "Falsche PIN. Ein Versuch übrig.",
|
||||
"enableSuccess": "PIN-Sperre erfolgreich aktiviert!",
|
||||
"pinLocked": "Die PIN-Sperre wurde deaktiviert. Bitte nutze Dein Master-Passwort, um Deinen Tresor zu entsperren.",
|
||||
"pinSecurityWarning": "Die PIN-Entsperre in der Browser-Erweiterung kann weniger sicher Dein Master-Passwort sein, da PINs typischerweise eine niedrigere Entropie haben und möglicherweise gebruteforced werden können, wenn Dein Gerät kompromittiert wird. Benutze es nur auf Geräten, denen Du vollkommen vertraust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
"passkey": "Passkey",
|
||||
"site": "Site",
|
||||
"site": "Seite",
|
||||
"displayName": "Name",
|
||||
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
|
||||
"passkeyMarkedForDeletion": "Passkey marked for deletion",
|
||||
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
|
||||
"helpText": "Passkeys werden auf der Webseite erzeugt. Sie können nicht manuell erstellt oder bearbeitet werden. Um diesen Passkey zu entfernen, kannst Du ihn von diesem Zugang löschen. Um einen Passkey zu ersetzen oder neu zu erstellen, rufe die entsprechende Webseite auf und folge den dortigen Anweisungen.",
|
||||
"passkeyMarkedForDeletion": "Passkey zum Löschen vorgemerkt",
|
||||
"passkeyWillBeDeleted": "Dieser Passkey wird gelöscht, wenn Du den Zugang speicherst.",
|
||||
"bypass": {
|
||||
"title": "Use Browser Passkey",
|
||||
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
|
||||
"thisTimeOnly": "This time only",
|
||||
"alwaysForSite": "Always for this site"
|
||||
"title": "Browser-Passkey verwenden",
|
||||
"description": "Wie lange möchtest Du den Passkey-Anbieter des Browsers für {{origin}} verwenden?",
|
||||
"thisTimeOnly": "Nur dieses Mal",
|
||||
"alwaysForSite": "Für diese Seite immer"
|
||||
},
|
||||
"authenticate": {
|
||||
"title": "Sign in with Passkey",
|
||||
"signInFor": "Sign in with passkey for",
|
||||
"selectPasskey": "Select a passkey to sign in:",
|
||||
"noPasskeysFound": "No passkeys found for this site",
|
||||
"useBrowserPasskey": "Use Browser Passkey"
|
||||
"title": "Mit Passkey anmelden",
|
||||
"signInFor": "Mit Passkey anmelden für",
|
||||
"selectPasskey": "Wähle einen Passkey zum Anmelden aus:",
|
||||
"noPasskeysFound": "Kein Passkey für diese Seite gefunden",
|
||||
"useBrowserPasskey": "Browser-Passkey verwenden"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Passkey",
|
||||
"createFor": "Create a new passkey for",
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
"replacingPasskey": "Replacing passkey: {{displayName}}",
|
||||
"confirmReplace": "Confirm Replace"
|
||||
"title": "Passkey erstellen",
|
||||
"createFor": "Neuen Passkey erstellen für",
|
||||
"titleLabel": "Titel",
|
||||
"titlePlaceholder": "Gib einen Namen für diesen Passkey ein",
|
||||
"createButton": "Passkey erstellen",
|
||||
"useBrowserPasskey": "Browser-Passkey verwenden",
|
||||
"selectPasskeyToReplace": "Wähle einen Passkey zum Ersetzen:",
|
||||
"createNewPasskey": "Neuen Passkey erstellen",
|
||||
"replacingPasskey": "Ersetzen des Passkeys: {{displayName}}",
|
||||
"confirmReplace": "Ersetzen bestätigen"
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProvider": "Passkey-Anbieter",
|
||||
"passkeyProviderOn": "Passkey-Anbieter an "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "Neu in dieser Version",
|
||||
"whatsNewDescription": "Eine Aktualisierung ist erforderlich, um die folgenden Änderungen zu unterstützen:",
|
||||
"noDescriptionAvailable": "Für diese Version ist keine Beschreibung vorhanden.",
|
||||
"okay": "OK",
|
||||
"status": {
|
||||
"preparingUpgrade": "Aktualisierung wird vorbereitet...",
|
||||
"vaultAlreadyUpToDate": "Tresor ist bereits aktualisiert",
|
||||
"startingDatabaseTransaction": "Datenbanktransaktion wird gestartet...",
|
||||
"applyingDatabaseMigrations": "Datenbankmigration wird durchgeführt...",
|
||||
"applyingMigration": "Führe Migration {{current}} von {{total}} durch...",
|
||||
"committingChanges": "Änderungen werden übernommen..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Fehler",
|
||||
"unableToGetVersionInfo": "Versionsinformationen konnten nicht abgerufen werden. Bitte versuche es erneut.",
|
||||
"selfHostedServer": "Selbstgehosteter Server",
|
||||
"selfHostedWarning": "Nutzt Du einen selbst gehosteten Server, musst Du Deine Instanz ebenfalls updaten. Andernfalls kannst Du Dich im Web-Client nicht mehr anmelden.",
|
||||
"cancel": "Abbrechen",
|
||||
"continueUpgrade": "Aktualisierung fortsetzen",
|
||||
"upgradeFailed": "Aktualisierung fehlgeschlagen",
|
||||
"failedToApplyMigration": "Migration fehlgeschlagen ({{current}} von {{total}})"
|
||||
|
||||
@@ -6,23 +6,22 @@
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"loginButton": "Log in",
|
||||
"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",
|
||||
"unlockVault": "Unlock",
|
||||
"unlockWithPin": "Unlock with PIN",
|
||||
"enterPinToUnlock": "Enter your PIN to unlock your vault",
|
||||
"useMasterPassword": "Use Master Password",
|
||||
"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",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginWithMobile": "Log in using Mobile App",
|
||||
"unlockWithMobile": "Unlock using Mobile App",
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
|
||||
"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.",
|
||||
"sessionExpired": "Your session has expired. Please log in again."
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"mobileLoginRequestExpired": "Mobile login request timed out. Please reload the page and try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"notice": "Notice",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"or": "Or",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"showDetails": "Show details",
|
||||
"hideDetails": "Hide details",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
|
||||
"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.",
|
||||
"serverVersionTooOld": "The AliasVault server needs to be updated to a newer version in order to use this feature. Please contact the server admin if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"unknownErrorTryAgain": "An unknown error occurred. Please try again.",
|
||||
"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": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"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.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
@@ -164,7 +166,6 @@
|
||||
"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"
|
||||
@@ -176,27 +177,12 @@
|
||||
"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.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"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.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"lock": "Lock",
|
||||
"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",
|
||||
@@ -331,7 +316,6 @@
|
||||
"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",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred 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"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Vault Unlock Method",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "Master Password",
|
||||
"pin": "PIN Code",
|
||||
"pinDescription": "Unlock vault with PIN code",
|
||||
"setupPin": "Setup PIN Code",
|
||||
"enterNewPinDescription": "Enter a PIN code consisting of minimum 6 digits",
|
||||
"confirmPin": "Confirm PIN",
|
||||
"confirmPinDescription": "Enter your PIN again to confirm",
|
||||
"invalidPinFormat": "Invalid PIN format",
|
||||
"pinMismatch": "PINs do not match",
|
||||
"incorrectPin": "Incorrect PIN. {{attemptsRemaining}} attempts remaining.",
|
||||
"incorrectPinSingular": "Incorrect PIN. 1 attempt remaining.",
|
||||
"enableSuccess": "PIN unlock enabled successfully!",
|
||||
"pinLocked": "PIN unlock has been disabled. Please use your master password to unlock your vault.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProviderOn": "Passkey Provider on "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"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}})"
|
||||
|
||||
@@ -1,455 +1,441 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "Log in to AliasVault",
|
||||
"username": "Username or email",
|
||||
"usernamePlaceholder": "name / name@company.com",
|
||||
"loginTitle": "Iniciar sesión en AliasVault",
|
||||
"username": "Nombre de usuario o correo electrónico",
|
||||
"usernamePlaceholder": "nombre / nombre@empresa.com",
|
||||
"password": "Contraseña",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"passwordPlaceholder": "Introduzca su contraseña",
|
||||
"rememberMe": "Recordar mis datos",
|
||||
"loginButton": "Iniciar sesión",
|
||||
"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": "Contraseña maestra",
|
||||
"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",
|
||||
"noAccount": "¿Sin cuenta todavía?",
|
||||
"createVault": "Crear nueva bóveda",
|
||||
"twoFactorTitle": "Por favor, introduzca el código de autenticación de su aplicación de autenticación.",
|
||||
"authCode": "Código de autenticación",
|
||||
"authCodePlaceholder": "Introduzca código de 6 dígitos",
|
||||
"verify": "Verificar",
|
||||
"twoFactorNote": "Nota: si no tiene acceso a su dispositivo de autenticación, puede restablecer su 2FA con un código de recuperación iniciando sesión a través del sitio web.",
|
||||
"masterPassword": "Contraseña Maestra",
|
||||
"unlockVault": "Desbloquear",
|
||||
"unlockWithPin": "Desbloquear con PIN",
|
||||
"enterPinToUnlock": "Introduzca su PIN para desbloquear su bóveda",
|
||||
"useMasterPassword": "Usar Contraseña Maestra",
|
||||
"unlockTitle": "Desbloquear tu Bóveda",
|
||||
"logout": "Cerrar sesión",
|
||||
"logoutConfirm": "¿Estás seguro de que quieres cerrar sesión?",
|
||||
"unlockSuccessTitle": "Tu bóveda se ha desbloqueado correctamente",
|
||||
"unlockSuccessDescription": "Ahora puede utilizar el autocompletado en los formularios de inicio de sesión en su navegador.",
|
||||
"closePopup": "Cerrar esta ventana emergente",
|
||||
"browseVault": "Navegar en el contenido de la bóveda",
|
||||
"connectingTo": "Conectando con",
|
||||
"switchAccounts": "¿Cambiar de cuenta?",
|
||||
"loggedIn": "Sesión iniciada",
|
||||
"loginWithMobile": "Iniciar sesión con la aplicación móvil",
|
||||
"unlockWithMobile": "Desbloquear con la aplicación móvil",
|
||||
"scanQrCode": "Escanea este código QR con tu aplicación móvil de AliasVault para iniciar sesión y desbloquear tu bóveda.",
|
||||
"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.",
|
||||
"sessionExpired": "Your session has expired. Please log in again."
|
||||
"invalidCode": "Por favor, introduzca un código de autenticación de 6 dígitos válido.",
|
||||
"serverError": "No se pudo llegar al servidor AliasVault. Por favor, inténtelo de nuevo más tarde o póngase en contacto con el soporte si el problema persiste.",
|
||||
"wrongPassword": "Contraseña incorrecta. Por favor, inténtelo de nuevo.",
|
||||
"accountLocked": "Cuenta bloqueada temporalmente debido a demasiados intentos fallidos.",
|
||||
"networkError": "Error de red. Por favor, compruebe su conexión y vuelva a intentarlo.",
|
||||
"sessionExpired": "Su sesión ha caducado. Por favor, inicie sesión de nuevo.",
|
||||
"mobileLoginRequestExpired": "Se ha agotado el tiempo de inicio de sesión del móvil. Por favor, vuelva a cargar la página e inténtelo de nuevo."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"credentials": "Credentials",
|
||||
"emails": "Emails",
|
||||
"settings": "Settings"
|
||||
"credentials": "Credenciales",
|
||||
"emails": "Correos electrónicos",
|
||||
"settings": "Configuración"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"loading": "Cargando...",
|
||||
"notice": "Aviso",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"back": "Back",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"or": "Or",
|
||||
"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",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"back": "Atrás",
|
||||
"next": "Siguiente",
|
||||
"use": "Usar",
|
||||
"delete": "Borrar",
|
||||
"save": "Guardar",
|
||||
"or": "O",
|
||||
"close": "Cerrar",
|
||||
"copied": "¡Copiado!",
|
||||
"openInNewWindow": "Abrir en una ventana nueva",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Desactivado",
|
||||
"showPassword": "Mostrar contraseña",
|
||||
"hidePassword": "Ocultar contraseña",
|
||||
"showDetails": "Mostrar detalles",
|
||||
"hideDetails": "Ocultar detalles",
|
||||
"copyToClipboard": "Copiar al portapapeles",
|
||||
"loadingEmails": "Cargando email...",
|
||||
"loadingTotpCodes": "Cargando códigos TOTP...",
|
||||
"attachments": "Archivos adjuntos",
|
||||
"loadingAttachments": "Cargando archivos adjuntos...",
|
||||
"settings": "Configuración",
|
||||
"recentEmails": "Correos recientes",
|
||||
"loginCredentials": "Credenciales de inicio de sesión",
|
||||
"twoFactorAuthentication": "Autenticación de doble factor",
|
||||
"alias": "Alias",
|
||||
"notes": "Notes",
|
||||
"fullName": "Full Name",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"birthDate": "Birth Date",
|
||||
"nickname": "Nickname",
|
||||
"notes": "Notas",
|
||||
"fullName": "Nombre completo",
|
||||
"firstName": "Nombre",
|
||||
"lastName": "Apellido",
|
||||
"birthDate": "Fecha de nacimiento",
|
||||
"nickname": "Alias",
|
||||
"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",
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"syncingVault": "Sincronizando bóveda",
|
||||
"savingChangesToVault": "Guardando cambios en la bóveda",
|
||||
"uploadingVaultToServer": "Subiendo bóveda al servidor",
|
||||
"checkingVaultUpdates": "Comprobando actualizaciones de bóveda",
|
||||
"syncingUpdatedVault": "Sincronizando bóveda actualizada",
|
||||
"executingOperation": "Ejecutando operación...",
|
||||
"loadMore": "Cargar más",
|
||||
"errors": {
|
||||
"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.",
|
||||
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
|
||||
"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",
|
||||
"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."
|
||||
"serverNotAvailable": "El servidor AliasVault no está disponible. Por favor, inténtelo de nuevo más tarde o póngase en contacto con el soporte técnico si el problema persiste.",
|
||||
"clientVersionNotSupported": "Esta versión de la extensión del navegador AliasVault ya no es compatible con el servidor. Por favor, actualice la extensión de su navegador a la última versión.",
|
||||
"browserExtensionOutdated": "Esta extensión del navegador está desactualizada y no se puede utilizar para acceder a esta bóveda. Por favor, actualice esta extensión del navegador para continuar.",
|
||||
"serverVersionNotSupported": "El servidor AliasVault necesita ser actualizado a una versión más reciente para poder usar esta extensión del navegador. Póngase en contacto con el soporte si necesita ayuda.",
|
||||
"serverVersionTooOld": "El servidor AliasVault necesita ser actualizado a una versión más reciente para poder utilizar esta característica. Por favor, contacte con el administrador del servidor si necesita ayuda.",
|
||||
"unknownError": "Se produjo un error desconocido",
|
||||
"unknownErrorTryAgain": "Se ha producido un error desconocido. Por favor, inténtelo de nuevo.",
|
||||
"vaultNotAvailable": "Bóveda no disponible",
|
||||
"vaultIsLocked": "La bóveda está bloqueada",
|
||||
"passwordChanged": "Tu contraseña ha cambiado desde la última vez que iniciaste sesión. Por favor, inicia sesión de nuevo por razones de seguridad."
|
||||
},
|
||||
"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": "Se ha producido un error desconocido. Por favor, inténtelo de nuevo.",
|
||||
"ACCOUNT_LOCKED": "Cuenta bloqueada temporalmente debido a demasiados intentos fallidos. Por favor, inténtelo de nuevo.",
|
||||
"ACCOUNT_BLOCKED": "Tu cuenta ha sido desactivada. Si crees que esto es un error, ponte en contacto con soporte.",
|
||||
"USER_NOT_FOUND": "Nombre de usuario o contraseña no válidos. Por favor, inténtelo de nuevo.",
|
||||
"INVALID_AUTHENTICATOR_CODE": "Código de autenticación inválido. Por favor, inténtelo de nuevo.",
|
||||
"INVALID_RECOVERY_CODE": "Código de recuperación inválido. Por favor, inténtelo de nuevo.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Se requiere un token de actualización.",
|
||||
"INVALID_REFRESH_TOKEN": "Token de actualización inválido.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "El registro de nueva cuenta está actualmente deshabilitado en este servidor. Por favor, contacte con el administrador.",
|
||||
"USERNAME_REQUIRED": "Se requiere un nombre de usuario.",
|
||||
"USERNAME_ALREADY_IN_USE": "El nombre de usuario ya está en uso.",
|
||||
"USERNAME_AVAILABLE": "El nombre de usuario está disponible.",
|
||||
"USERNAME_MISMATCH": "El nombre de usuario no coincide con el usuario actual.",
|
||||
"PASSWORD_MISMATCH": "La contraseña proporcionada no coincide con su contraseña actual.",
|
||||
"ACCOUNT_SUCCESSFULLY_DELETED": "Cuenta eliminada con éxito.",
|
||||
"USERNAME_EMPTY_OR_WHITESPACE": "El nombre de usuario no puede estar vacío o en blanco.",
|
||||
"USERNAME_TOO_SHORT": "Nombre de usuario demasiado corto: debe tener al menos 3 caracteres.",
|
||||
"USERNAME_TOO_LONG": "Nombre de usuario demasiado largo: no puede tener más de 40 caracteres.",
|
||||
"USERNAME_INVALID_EMAIL": "Dirección de correo electrónico inválida.",
|
||||
"USERNAME_INVALID_CHARACTERS": "El nombre de usuario inválido, solo puede contener letras o dígitos.",
|
||||
"VAULT_NOT_UP_TO_DATE": "Su bóveda no está actualizada. Por favor, sincronice su bóveda e inténtelo de nuevo.",
|
||||
"INTERNAL_SERVER_ERROR": "Error interno del servidor.",
|
||||
"VAULT_ERROR": "La bóveda local no está actualizada. Por favor, sincronice su bóveda actualizando la página e inténtelo de nuevo."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
"or": "o",
|
||||
"new": "Nuevo",
|
||||
"cancel": "Cancelar",
|
||||
"vaultLocked": "AliasVault está bloqueado.",
|
||||
"creatingNewAlias": "Creando nuevo alias...",
|
||||
"noMatchesFound": "No se han encontrado resultados",
|
||||
"searchVault": "Buscar bóveda...",
|
||||
"serviceName": "Nombre del servicio",
|
||||
"email": "Correo electrónico",
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"enterServiceName": "Introduzca el nombre del servicio",
|
||||
"enterEmailAddress": "Introduzca la dirección de correo electrónico",
|
||||
"enterUsername": "Introduzca nombre de usuario",
|
||||
"hideFor1Hour": "Ocultar durante 1 hora (sitio actual)",
|
||||
"hidePermanently": "Ocultar permanentemente (sitio actual)",
|
||||
"createRandomAlias": "Crear alias aleatorio",
|
||||
"createUsernamePassword": "Crear nombre de usuario / contraseña",
|
||||
"randomAlias": "Alias aleatorio",
|
||||
"usernamePassword": "Nombre de usuario/Contraseña",
|
||||
"createAndSaveAlias": "Crear y guardar alias",
|
||||
"createAndSaveCredential": "Crear y guardar credencial",
|
||||
"randomIdentityDescription": "Generar una identidad aleatoria con una dirección de correo electrónico aleatoria accesible en AliasVault.",
|
||||
"randomIdentityDescriptionDropdown": "Identidad aleatoria con correo electrónico aleatorio",
|
||||
"manualCredentialDescription": "Especifique su propia dirección de correo electrónico y nombre de usuario.",
|
||||
"manualCredentialDescriptionDropdown": "Nombre de usuario y contraseña manual",
|
||||
"failedToCreateIdentity": "Error al crear la identidad. Por favor, inténtalo de nuevo.",
|
||||
"enterEmailAndOrUsername": "Introduzca email y/o nombre de usuario",
|
||||
"autofillWithAliasVault": "AutoFill con AliasVault",
|
||||
"generateRandomPassword": "Generar contraseña aleatoria (copiar al portapapeles)",
|
||||
"generateNewPassword": "Generar nueva contraseña",
|
||||
"togglePasswordVisibility": "Cambiar la visibilidad de la contraseña",
|
||||
"passwordCopiedToClipboard": "Contraseña copiada al portapapeles",
|
||||
"openAliasVaultToUpgrade": "Abrir AliasVault para actualizar",
|
||||
"vaultUpgradeRequired": "Actualización de bóveda requerida.",
|
||||
"dismissPopup": "Descartar aviso"
|
||||
},
|
||||
"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.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
"noAttachmentsFound": "No credentials with attachments found",
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"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.",
|
||||
"title": "Credenciales",
|
||||
"addCredential": "Añadir credencial",
|
||||
"editCredential": "Editar credencial",
|
||||
"deleteCredential": "Borrar credencial",
|
||||
"credentialDetails": "Detalles de la credencial",
|
||||
"serviceName": "Nombre del servicio",
|
||||
"notes": "Notas",
|
||||
"totp": "Autenticación de Doble Factor",
|
||||
"totpCode": "Código TOTP",
|
||||
"copyTotp": "Copiar TOTP",
|
||||
"totpSecret": "Secreto TOTP",
|
||||
"totpSecretPlaceholder": "Introduzca la clave secreta TOTP",
|
||||
"welcomeTitle": "¡Bienvenido a AliasVault!",
|
||||
"welcomeDescription": "Para utilizar la extensión del navegador AliasVault: vaya a un sitio web y utilice la ventana de autocompletado de AliasVault para crear una nueva credencial.",
|
||||
"noPasskeysFound": "No se han creado llaves de acceso todavía. Las llaves se crean visitando un sitio web que ofrece llaves de acceso como método de autenticación.",
|
||||
"noAttachmentsFound": "No se encontraron credenciales con archivos adjuntos",
|
||||
"noMatchingCredentials": "No se han encontrado credenciales coincidentes",
|
||||
"createdAt": "Creado",
|
||||
"updatedAt": "Última actualización",
|
||||
"saveCredential": "Guardar credencial",
|
||||
"deleteCredentialTitle": "Borrar credencial",
|
||||
"deleteCredentialConfirm": "¿Está seguro de que desea eliminar estas credenciales? Esta acción no se puede deshacer.",
|
||||
"filters": {
|
||||
"all": "(All) Credentials",
|
||||
"passkeys": "Passkeys",
|
||||
"aliases": "Aliases",
|
||||
"userpass": "Passwords",
|
||||
"attachments": "Attachments"
|
||||
"all": "(Todas) Credenciales",
|
||||
"passkeys": "Llaves de acceso",
|
||||
"aliases": "Alias",
|
||||
"userpass": "Contraseñas",
|
||||
"attachments": "Archivos adjuntos"
|
||||
},
|
||||
"randomAlias": "Random Alias",
|
||||
"randomAlias": "Alias aleatorio",
|
||||
"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",
|
||||
"service": "Servicio",
|
||||
"serviceUrl": "URL del servicio",
|
||||
"loginCredentials": "Credenciales de inicio de sesión",
|
||||
"generateRandomUsername": "Generar nombre de usuario aleatorio",
|
||||
"generateRandomPassword": "Generar contraseña aleatoria",
|
||||
"changePasswordComplexity": "Cambiar complejidad de contraseña",
|
||||
"passwordLength": "Longitud de la contraseña",
|
||||
"includeLowercase": "Incluye letras minúsculas",
|
||||
"includeUppercase": "Incluye letras mayúsculas",
|
||||
"includeNumbers": "Incluye números",
|
||||
"includeSpecialChars": "Incluye caracteres especiales",
|
||||
"avoidAmbiguousChars": "Evita caracteres ambiguos (o, 0, etc.)",
|
||||
"generateNewPreview": "Crear nueva vista previa",
|
||||
"generateRandomAlias": "Crear alias aleatorio",
|
||||
"clearAliasFields": "Limpiar campos de alias",
|
||||
"alias": "Alias",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"nickName": "Nick Name",
|
||||
"gender": "Gender",
|
||||
"birthDate": "Birth Date",
|
||||
"birthDatePlaceholder": "YYYY-MM-DD",
|
||||
"metadata": "Metadata",
|
||||
"firstName": "Nombre",
|
||||
"lastName": "Apellido",
|
||||
"nickName": "Apodo",
|
||||
"gender": "Género",
|
||||
"birthDate": "Fecha de nacimiento",
|
||||
"birthDatePlaceholder": "AAAA-MM-DD",
|
||||
"metadata": "Metadatos",
|
||||
"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": "Se requiere este campo",
|
||||
"serviceNameRequired": "Se requiere Nombre del servicio",
|
||||
"invalidEmail": "Formato de correo electrónico inválido",
|
||||
"invalidDateFormat": "La fecha debe estar en formato AAAA-MM-DD"
|
||||
},
|
||||
"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": "Correo electrónico privado",
|
||||
"privateEmailAliasVaultServer": "Servidor AliasVault",
|
||||
"privateEmailDescription": "E2E cifrado, totalmente privado.",
|
||||
"publicEmailTitle": "Proveedores de Correo Temporal Públicos",
|
||||
"publicEmailDescription": "Privacidad anónima pero limitada. Contenido de correo electrónico puede ser leído por cualquiera que conozca la dirección.",
|
||||
"useDomainChooser": "Usar selector de dominio",
|
||||
"enterCustomDomain": "Introduzca dominio personalizado",
|
||||
"enterFullEmail": "Introduzca la dirección de correo completa",
|
||||
"enterEmailPrefix": "Introduzca prefijo de correo"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Añadir código 2FA",
|
||||
"instructions": "Introduzca la clave secreta mostrada por el sitio web donde desea añadir autenticación de dos factores.",
|
||||
"nameOptional": "Nombre (Opcional)",
|
||||
"secretKey": "Clave secreta",
|
||||
"saveToViewCode": "Guardar para ver el código",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Formato de clave secreta inválida."
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
"title": "Correos electrónicos",
|
||||
"deleteEmailTitle": "Eliminar correo",
|
||||
"deleteEmailConfirm": "¿Está seguro que desea eliminar permanentemente este correo electrónico?",
|
||||
"from": "De",
|
||||
"to": "Para",
|
||||
"date": "Fecha",
|
||||
"emailContent": "Contenido del correo electrónico",
|
||||
"attachments": "Archivos adjuntos",
|
||||
"emailNotFound": "Correo electrónico no encontrado",
|
||||
"noEmails": "No se han encontrado correos electrónicos",
|
||||
"noEmailsDescription": "No has recibido ningún correo electrónico en tus direcciones privadas todavía. Cuando recibas un nuevo correo electrónico, aparecerá aquí.",
|
||||
"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": "ahora mismo",
|
||||
"minutesAgo_single": "Hace {{count}} min",
|
||||
"minutesAgo_plural": "Hace {{count}} min",
|
||||
"hoursAgo_single": "Hace {{count}} h",
|
||||
"hoursAgo_plural": "Hace {{count}} h",
|
||||
"yesterday": "ayer"
|
||||
},
|
||||
"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": "Se ha producido un error al cargar los correos electrónicos. Por favor, inténtalo de nuevo más tarde.",
|
||||
"emailUnexpectedError": "Se ha producido un error inesperado al cargar los correos electrónicos. Por favor, inténtalo de nuevo más tarde."
|
||||
},
|
||||
"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": "La dirección de correo electrónico elegida actualmente ya está en uso. Por favor, cambie la dirección de correo electrónico editando esta credencial.",
|
||||
"CLAIM_DOES_NOT_EXIST": "Ocurrió un error mientras se trataba de cargar los correos electrónicos. Por favor, intente editar y guardar la entrada de credenciales para sincronizar la base de datos, y vuelva a intentarlo."
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"passkeySettings": "Passkey 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",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
|
||||
"title": "Ajustes",
|
||||
"serverUrl": "URL del servidor",
|
||||
"language": "Idioma",
|
||||
"autofillEnabled": "Activar autocompletado",
|
||||
"version": "Versión",
|
||||
"openInNewWindow": "Abrir en una ventana nueva",
|
||||
"openWebApp": "Abrir la aplicación web",
|
||||
"loggedIn": "Sesión iniciada",
|
||||
"logout": "Cerrar sesión",
|
||||
"lock": "Bloquear",
|
||||
"globalSettings": "Ajustes globales",
|
||||
"autofillPopup": "Ventana de autorrellenado",
|
||||
"activeOnAllSites": "Activo en todos los sitios (a menos que se deshabilite debajo)",
|
||||
"disabledOnAllSites": "Deshabilitado en todos los sitios",
|
||||
"rightClickContextMenu": "Menú contextual del clic derecho",
|
||||
"autofillMatching": "Coincidencia de autocompletado",
|
||||
"autofillMatchingMode": "Modo de coincidencia de autocompletado",
|
||||
"autofillMatchingModeDescription": "Determina qué credenciales se consideran coincidentes y se muestran como sugerencias en el popup de autorrelleno de un sitio web determinado.",
|
||||
"autofillMatchingDefault": "URL + subdominio + nombre wildcard",
|
||||
"autofillMatchingUrlSubdomain": "URL + subdominio",
|
||||
"autofillMatchingUrlExact": "Solo URL exacta",
|
||||
"siteSpecificSettings": "Ajustes específicos del sitio",
|
||||
"autofillPopupOn": "Ventana de autorrelleno ",
|
||||
"enabledForThisSite": "Habilitado para este sitio",
|
||||
"disabledForThisSite": "Deshabilitado para este sitio",
|
||||
"temporarilyDisabledUntil": "Deshabilitado temporalmente hasta ",
|
||||
"resetAllSiteSettings": "Reiniciar todos los ajustes específicos del sitio",
|
||||
"appearance": "Apariencia",
|
||||
"theme": "Tema",
|
||||
"useDefault": "Uso por defecto",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro",
|
||||
"keyboardShortcuts": "Atajos de teclado",
|
||||
"configureKeyboardShortcuts": "Configurar atajos de teclado",
|
||||
"configure": "Configurar",
|
||||
"clipboardClearTimeout": "Limpiar portapapeles después de copiar",
|
||||
"clipboardClearTimeoutDescription": "Limpiar automáticamente el portapapeles después de copiar datos sensibles",
|
||||
"clipboardClearDisabled": "Nunca limpiar",
|
||||
"clipboardClear5Seconds": "Limpiar después 5 segundos",
|
||||
"clipboardClear10Seconds": "Limpiar después 10 segundos",
|
||||
"clipboardClear15Seconds": "Limpiar después 15 segundos",
|
||||
"autoLockTimeout": "Tiempo de bloqueo automático",
|
||||
"autoLockTimeoutDescription": "Bloquear automáticamente la bóveda después de un período de inactividad",
|
||||
"autoLockTimeoutHelp": "La bóveda sólo se bloqueará después del período especificado de inactividad (no se abrirá el uso de autocompletado o la ventana emergente). La bóveda siempre se bloqueará cuando el navegador esté cerrado, independientemente de esta configuración.",
|
||||
"autoLockNever": "Nunca",
|
||||
"autoLock15Seconds": "15 segundos",
|
||||
"autoLock1Minute": "1 minuto",
|
||||
"autoLock5Minutes": "5 minutos",
|
||||
"autoLock15Minutes": "15 minutos",
|
||||
"autoLock30Minutes": "30 minutos",
|
||||
"autoLock1Hour": "1 hora",
|
||||
"autoLock4Hours": "4 horas",
|
||||
"autoLock8Hours": "8 horas",
|
||||
"autoLock24Hours": "24 horas",
|
||||
"versionPrefix": "Versión ",
|
||||
"autofillSettings": "Ajustes de autocompletado",
|
||||
"clipboardSettings": "Ajustes del portapapeles",
|
||||
"contextMenuSettings": "Ajustes del menú contextual",
|
||||
"passkeySettings": "Ajustes de llaves de acceso",
|
||||
"contextMenu": "Menú Contextual",
|
||||
"contextMenuEnabled": "El menú contextual está activado",
|
||||
"contextMenuDisabled": "El menú contextual está desactivado",
|
||||
"contextMenuDescription": "Haga clic derecho en los campos de entrada para acceder a las opciones de AliasVault",
|
||||
"selectLanguage": "Seleccionar idioma",
|
||||
"serverConfiguration": "Configuración del servidor",
|
||||
"serverConfigurationDescription": "Configurar la URL del servidor AliasVault para instancias self-hosted",
|
||||
"customApiUrl": "API URL",
|
||||
"customClientUrl": "Client URL",
|
||||
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
|
||||
"clientUrlHint": "The web interface URL of your self-hosted instance",
|
||||
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"customClientUrl": "URL de Cliente",
|
||||
"apiUrlHint": "La URL del endpoint de la API (generalmente URL del cliente + /api)",
|
||||
"clientUrlHint": "La URL de la interfaz web de su instancia self-hosted",
|
||||
"autofillSettingsDescription": "Activar o desactivar la ventana emergente de autorrelleno en páginas web",
|
||||
"autofillEnabledDescription": "Las sugerencias de autorrelleno aparecerán en los formularios de inicio de sesión",
|
||||
"autofillDisabledDescription": "Las sugerencias de autorrelleno están desactivadas globalmente",
|
||||
"languageSettings": "Idioma",
|
||||
"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": "La URL de la API es necesaria",
|
||||
"apiUrlInvalid": "Por favor, introduzca una URL de API válida",
|
||||
"clientUrlRequired": "La URL de cliente es necesaria",
|
||||
"clientUrlInvalid": "Por favor, introduzca una URL de cliente válida"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Método de desbloqueo de la bóveda",
|
||||
"introText": "Elija cómo desea desbloquear su bóveda. Puede utilizar su contraseña maestra (siempre disponible) o configurar un código PIN para un acceso más rápido. Después de 3 intentos fallidos de PIN, necesitarás usar tu contraseña maestra.",
|
||||
"password": "Contraseña Maestra",
|
||||
"pin": "Código PIN",
|
||||
"pinDescription": "Desbloquear bóveda con código PIN",
|
||||
"setupPin": "Configurar código PIN",
|
||||
"enterNewPinDescription": "Introduzca un código PIN que contenga un mínimo de 6 dígitos",
|
||||
"confirmPin": "Confirmar PIN",
|
||||
"confirmPinDescription": "Introduzca su PIN de nuevo para confirmar",
|
||||
"invalidPinFormat": "Formato PIN inválido",
|
||||
"pinMismatch": "Los PINs no coinciden",
|
||||
"incorrectPin": "PIN incorrecto, {{attemptsRemaining}} intentos restantes.",
|
||||
"incorrectPinSingular": "PIN incorrecto. 1 intento restante.",
|
||||
"enableSuccess": "¡PIN activado con éxito!",
|
||||
"pinLocked": "El desbloqueo con PIN ha sido deshabilitado. Por favor, utiliza tu contraseña maestra para desbloquear tu bóveda.",
|
||||
"pinSecurityWarning": "El PIN de desbloqueo en la extensión del navegador puede ser menos seguro que su contraseña maestra, ya que los PIN típicamente tienen menos entropía y se pueden obtener por fuerza bruta si su dispositivo está en peligro. Úselo sólo en dispositivos en los que confíe plenamente."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
"passkey": "Passkey",
|
||||
"site": "Site",
|
||||
"displayName": "Name",
|
||||
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
|
||||
"passkeyMarkedForDeletion": "Passkey marked for deletion",
|
||||
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
|
||||
"passkey": "Llave de acceso",
|
||||
"site": "Sitio",
|
||||
"displayName": "Nombre",
|
||||
"helpText": "Las llaves de acceso se crean en el sitio web cuando se le solicite. No pueden ser editadas manualmente. Para eliminar esta clave, puede eliminarla de esta credencial. Para reemplazar esta clave de acceso o crear una nueva, visite el sitio web y siga sus indicaciones.",
|
||||
"passkeyMarkedForDeletion": "Llave de acceso marcada para eliminar",
|
||||
"passkeyWillBeDeleted": "Esta llave de acceso se eliminará cuando guarde esta credencial.",
|
||||
"bypass": {
|
||||
"title": "Use Browser Passkey",
|
||||
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
|
||||
"thisTimeOnly": "This time only",
|
||||
"alwaysForSite": "Always for this site"
|
||||
"title": "Usar llave de acceso del navegador",
|
||||
"description": "¿Cuánto tiempo quieres usar el proveedor de llaves de acceso del navegador para {{origin}}?",
|
||||
"thisTimeOnly": "Solo esta vez",
|
||||
"alwaysForSite": "Siempre para este sitio"
|
||||
},
|
||||
"authenticate": {
|
||||
"title": "Sign in with Passkey",
|
||||
"signInFor": "Sign in with passkey for",
|
||||
"selectPasskey": "Select a passkey to sign in:",
|
||||
"noPasskeysFound": "No passkeys found for this site",
|
||||
"useBrowserPasskey": "Use Browser Passkey"
|
||||
"title": "Iniciar sesión con llave de acceso",
|
||||
"signInFor": "Iniciar sesión con llave para",
|
||||
"selectPasskey": "Seleccione una llave de acceso para iniciar sesión:",
|
||||
"noPasskeysFound": "No se encontraron llaves de acceso para este sitio",
|
||||
"useBrowserPasskey": "Usar llave de acceso del navegador"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Passkey",
|
||||
"createFor": "Create a new passkey for",
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
"replacingPasskey": "Replacing passkey: {{displayName}}",
|
||||
"confirmReplace": "Confirm Replace"
|
||||
"title": "Crear llave de acceso",
|
||||
"createFor": "Crear nueva llave de acceso para",
|
||||
"titleLabel": "Título",
|
||||
"titlePlaceholder": "Introduzca un nombre para esta llave de acceso",
|
||||
"createButton": "Crear llave de acceso",
|
||||
"useBrowserPasskey": "Usar llave de acceso del navegador",
|
||||
"selectPasskeyToReplace": "Seleccione una llave de acceso para reemplazar:",
|
||||
"createNewPasskey": "Crear nueva llave de acceso",
|
||||
"replacingPasskey": "Reemplazando llave de acceso: {{displayName}}",
|
||||
"confirmReplace": "Confirmar reemplazo"
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProvider": "Proveedor de llave de acceso",
|
||||
"passkeyProviderOn": "Proveedor de llave de acceso en "
|
||||
}
|
||||
},
|
||||
"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 version:",
|
||||
"newVersion": "New available 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..."
|
||||
},
|
||||
"title": "Actualizar bóveda",
|
||||
"subtitle": "AliasVault se ha actualizado y tu bóveda necesita ser actualizada. Esto solo debería tardar unos segundos.",
|
||||
"versionInformation": "Información de versión",
|
||||
"yourVault": "Tu versión de bóveda:",
|
||||
"newVersion": "Nueva versión disponible:",
|
||||
"upgrade": "Actualizar bóveda",
|
||||
"upgrading": "Actualizando...",
|
||||
"logout": "Cerrar sesión",
|
||||
"whatsNew": "Novedades",
|
||||
"whatsNewDescription": "Se requiere una actualización para soportar los siguientes cambios:",
|
||||
"noDescriptionAvailable": "Ninguna descripción disponible para esta versión.",
|
||||
"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}})"
|
||||
"unableToGetVersionInfo": "No se pudo obtener información de la versión. Por favor, inténtalo de nuevo.",
|
||||
"selfHostedServer": "Servidor autoalojado",
|
||||
"selfHostedWarning": "Si está utilizando un servidor autoalojado, asegúrese de actualizar su instancia autoalojada, ya que de lo contrario iniciar sesión en el cliente web dejará de funcionar.",
|
||||
"continueUpgrade": "Continuar actualización",
|
||||
"upgradeFailed": "Actualización fallida",
|
||||
"failedToApplyMigration": "Error al migrar de ({{current}} a {{total}})"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,16 +13,15 @@
|
||||
"authCode": "Tunnistautumiskoodi",
|
||||
"authCodePlaceholder": "Syötä 6-numeroinen koodi",
|
||||
"verify": "Vahvista",
|
||||
"cancel": "Peruuta",
|
||||
"twoFactorNote": "Huomautus: jos sinulla ei ole pääsyä tunnistautumislaitteeseen, voit palauttaa 2FA:n palautuskoodilla kirjautumalla sisään sivuston kautta.",
|
||||
"masterPassword": "Pääsalasana",
|
||||
"unlockVault": "Avaa holvin lukitus",
|
||||
"unlockVault": "Avaa lukitus",
|
||||
"unlockWithPin": "Avaa PIN-koodilla",
|
||||
"enterPinToUnlock": "Syötä PIN-koodi avataksesi holvisi",
|
||||
"useMasterPassword": "Käytä pääsalasanaa",
|
||||
"unlockTitle": "Avaa holvisi lukitus",
|
||||
"unlockDescription": "Syötä pääsalasanasi avataksesi holvisi lukituksen.",
|
||||
"logout": "Uloskirjautuminen",
|
||||
"logoutConfirm": "Oletko varma, että haluat kirjautua ulos?",
|
||||
"sessionExpired": "Istuntosi on vanhentunut. Kirjaudu sisään uudelleen.",
|
||||
"unlockSuccess": "Holvin lukitus avattu!",
|
||||
"unlockSuccessTitle": "Holvisi lukitus on avattu",
|
||||
"unlockSuccessDescription": "Voit nyt käyttää automaattista täyttöä sisäänkirjautumislomakkeissa selaimessasi.",
|
||||
"closePopup": "Sulje tämä ponnahdusikkuna",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Yhdistetään kohteeseen",
|
||||
"switchAccounts": "Vaihdetaanko tiliä?",
|
||||
"loggedIn": "Sisäänkirjautuneena",
|
||||
"loginWithMobile": "Kirjaudu sisään mobiilisovelluksella",
|
||||
"unlockWithMobile": "Avaa käyttämällä mobiilisovellusta",
|
||||
"scanQrCode": "Skannaa tämä QR-koodi AliasVault-mobiilisovelluksellasi kirjautuaksesi sisään ja avataksesi holvisi.",
|
||||
"errors": {
|
||||
"invalidCode": "Syötä kelvollinen 6-numeroinen tunnistautumiskoodi.",
|
||||
"serverError": "AliasVault-palvelimeen ei saatu yhteyttä. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
|
||||
"noToken": "Sisäänkirjautuminen epäonnistui -- polettia ei palautettu",
|
||||
"migrationError": "Tapahtui virhe tarkistettaessa odottavia siirtoja.",
|
||||
"wrongPassword": "Virheellinen salasana. Yritä uudelleen.",
|
||||
"accountLocked": "Tili tilapäisesti lukittu liian monen epäonnistuneen yrityksen vuoksi.",
|
||||
"networkError": "Verkkovirhe: tarkista yhteytesi ja yritä uudelleen.",
|
||||
"sessionExpired": "Istuntosi on vanhentunut. Kirjaudu sisään uudelleen."
|
||||
"sessionExpired": "Istuntosi on vanhentunut. Kirjaudu sisään uudelleen.",
|
||||
"mobileLoginRequestExpired": "Mobiilikirjautumispyyntö aikakatkaistiin. Lataa sivu uudelleen ja yritä uudelleen."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Asetukset"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Ladataan...",
|
||||
"notice": "Huomautus",
|
||||
"error": "Virhe",
|
||||
"success": "Onnistui",
|
||||
"cancel": "Peruuta",
|
||||
"confirm": "Vahvista",
|
||||
"back": "Takaisin",
|
||||
"next": "Seuraava",
|
||||
"use": "Käytä",
|
||||
"delete": "Poista",
|
||||
"save": "Tallenna",
|
||||
"or": "Tai",
|
||||
"close": "Sulje",
|
||||
"copied": "Kopioitu!",
|
||||
"openInNewWindow": "Avaa uudessa ikkunassa",
|
||||
"language": "Kieli",
|
||||
"enabled": "Otettu käyttöön",
|
||||
"disabled": "Pois käytöstä",
|
||||
"showPassword": "Näytä salasana",
|
||||
"hidePassword": "Piilota salasana",
|
||||
"showDetails": "Näytä tiedot",
|
||||
"hideDetails": "Piilota tiedot",
|
||||
"copyToClipboard": "Kopioi leikepöydälle",
|
||||
"loadingEmails": "Ladataan sähköposteja...",
|
||||
"loadingTotpCodes": "Ladataan TOTP-koodeja...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "Palvelin ei enää tue tätä AliasVault-selainlaajennuksen versiota. Päivitä selaimen laajennus uusimpaan versioon.",
|
||||
"browserExtensionOutdated": "Tämä selainlaajennus on vanhentunut, eikä sillä voi saada pääsyä tähän holviin. Päivitä tämä selainlaajennus jatkaaksesi.",
|
||||
"serverVersionNotSupported": "AliasVault-palvelin on päivitettävä uudempaan versioon, jotta voit käyttää tätä selainlaajennusta. Ota yhteyttä tukeen, jos tarvitset apua.",
|
||||
"serverVersionTooOld": "AliasVault-palvelin on päivitettävä uudempaan versioon, jotta se voi käyttää tätä ominaisuutta. Ota yhteyttä palvelimen ylläpitäjään jos tarvitset apua.",
|
||||
"unknownError": "Tapahtui tuntematon virhe",
|
||||
"unknownErrorTryAgain": "Tapahtui tuntematon virhe. Yritä uudelleen.",
|
||||
"vaultNotAvailable": "Holvi ei käytettävissä",
|
||||
"failedToRetrieveData": "Tietojen nouto epäonnistui",
|
||||
"vaultIsLocked": "Holvi on lukittu",
|
||||
"failedToUploadVault": "Holvin ulospäinlataaminen epäonnistui",
|
||||
"passwordChanged": "Salasanasi on muuttunut edellisen sisäänkirjautumisen jälkeen. Kirjaudu sisään uudelleen turvallisuussyistä."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Virheellinen palautuskoodi. Yritä uudelleen.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Virkistyspoletti vaaditaan.",
|
||||
"INVALID_REFRESH_TOKEN": "Virheellinen virkistyspoletti.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Virkistyspoletti peruutettu.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Uuden tilin rekisteröinti on tällä hetkellä poistettu käytöstä tällä palvelimella. Ota yhteyttä järjestelmänvalvojaan.",
|
||||
"USERNAME_REQUIRED": "Käyttäjänimi vaaditaan.",
|
||||
"USERNAME_ALREADY_IN_USE": "Käyttäjänimi on jo käytössä.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "tai",
|
||||
"new": "Uusi",
|
||||
"cancel": "Peruuta",
|
||||
"search": "Etsi",
|
||||
"vaultLocked": "AliasVault on lukittu.",
|
||||
"creatingNewAlias": "Luodaan uutta aliasta...",
|
||||
"noMatchesFound": "Osumia ei löytynyt",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Luo uusi salasana",
|
||||
"togglePasswordVisibility": "Salasanan näkyvyyden päälle/pois päältä kytkeminen",
|
||||
"passwordCopiedToClipboard": "Salasana kopioitu leikepöydälle",
|
||||
"enterEmailAndOrUsernameError": "Syötä sähköpostiosoite ja/tai käyttäjänimi",
|
||||
"openAliasVaultToUpgrade": "Avaa AliasVault päivittääksesi",
|
||||
"vaultUpgradeRequired": "Holvin päivitys vaaditaan.",
|
||||
"dismissPopup": "Hylkää ponnahdusikkuna"
|
||||
@@ -176,27 +177,12 @@
|
||||
"deleteCredential": "Poista tunnistetieto",
|
||||
"credentialDetails": "Tunnistetietojen yksityiskohdat",
|
||||
"serviceName": "Palvelun nimi",
|
||||
"serviceNamePlaceholder": "esim. Gmail, Facebook, pankki",
|
||||
"website": "Verkkosivusto",
|
||||
"websitePlaceholder": "https://esimerkki.fi",
|
||||
"username": "Käyttäjänimi",
|
||||
"usernamePlaceholder": "Syötä käyttäjänimi",
|
||||
"password": "Salasana",
|
||||
"passwordPlaceholder": "Syötä salasana",
|
||||
"generatePassword": "Luo salasana",
|
||||
"copyPassword": "Kopioi salasana",
|
||||
"showPassword": "Näytä salasana",
|
||||
"hidePassword": "Piilota salasana",
|
||||
"notes": "Huomautukset",
|
||||
"notesPlaceholder": "Lisähuomautukset...",
|
||||
"totp": "Kaksivaiheinen tunnistautuminen",
|
||||
"totpCode": "TOTP-koodi",
|
||||
"copyTotp": "Kopioi TOTP-koodi",
|
||||
"totpSecret": "TOTP-salaisuus",
|
||||
"totpSecretPlaceholder": "Syötä TOTP-salainen avain",
|
||||
"noCredentials": "Tunnistetietoja ei löytynyt",
|
||||
"noCredentialsDescription": "Lisää ensimmäinen tunnistetietosi aloittaaksesi",
|
||||
"searchPlaceholder": "Etsi tunnistetietoja...",
|
||||
"welcomeTitle": "Tervetuloa AliasVaultiin!",
|
||||
"welcomeDescription": "Käyttääksesi AliasVault-selainlaajennusta, siirry jollekin verkkosivustolle ja käytä AliasVaultin automaattisen täytön ponnahdusikkunaa luodaksesi uuden tunnistetiedon.",
|
||||
"noPasskeysFound": "Todennusavaimia, Passkey ei ole vielä luotu. Todennusavaimet on luotu vierailemalla verkkosivustolla, joka tarjoaa todennusavaimia todennusmenetelmänä.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "Vastaavia tunnistetietoja ei löytynyt",
|
||||
"createdAt": "Luotu",
|
||||
"updatedAt": "Viimeksi päivitetty",
|
||||
"autofill": "Automaattinen täyttö",
|
||||
"fillForm": "Täytä lomake",
|
||||
"deleteConfirm": "Oletko varma, että haluat poistaa tämän tunnistetiedon?",
|
||||
"saveSuccess": "Tunnistetieto tallennettu",
|
||||
"tags": "Tunnisteet",
|
||||
"addTag": "Lisää tunniste",
|
||||
"removeTag": "Poista tunniste",
|
||||
"folder": "Kansio",
|
||||
"selectFolder": "Valitse kansio",
|
||||
"createFolder": "Luo kansio",
|
||||
"saveCredential": "Tallenna tunnistetieto",
|
||||
"deleteCredentialTitle": "Poista tunnistetieto",
|
||||
"deleteCredentialConfirm": "Oletko varma, että haluat poistaa tämän tunnistetiedon? Tätä toimintoa ei voi perua.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Syötä koko sähköpostiosoite",
|
||||
"enterEmailPrefix": "Syötä sähköpostin etuliite"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Lisää 2FA TOTP -koodi",
|
||||
"instructions": "Syötä salainen avain, joka näkyy sivustossa, jossa haluat lisätä kaksivaiheisen tunnistautumisen",
|
||||
"nameOptional": "Nimi (valinnainen)",
|
||||
"secretKey": "Salainen avain",
|
||||
"saveToViewCode": "Tallenna nähdäksesi koodin",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Virheellinen salatun avaimen muoto."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Sähköpostit",
|
||||
"deleteEmailTitle": "Poista sähköposti",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Avaa verkkosovellus",
|
||||
"loggedIn": "Sisäänkirjautuneena",
|
||||
"logout": "Uloskirjautuminen",
|
||||
"lock": "Lukitse",
|
||||
"globalSettings": "Yleisesti pätevät asetukset",
|
||||
"autofillPopup": "Automaattisen täytön ponnahdusikkuna",
|
||||
"activeOnAllSites": "Aktiivisena kaikilla sivustoilla (ellei sitä ole poistettu käytöstä alla)",
|
||||
"disabledOnAllSites": "Pois käytöstä kaikilla sivustoilla",
|
||||
"enabled": "Otettu käyttöön",
|
||||
"disabled": "Pois käytöstä",
|
||||
"rightClickContextMenu": "Oikea-napsauta kontekstivalikkoa",
|
||||
"autofillMatching": "Automaattisen täytön täsmäytys",
|
||||
"autofillMatchingMode": "Automaattisen täytön täsmäytystila",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Näppäimistön pikanppäimet",
|
||||
"configureKeyboardShortcuts": "Määritä näppäimistön pikanäppäimet",
|
||||
"configure": "Määritä",
|
||||
"security": "Tietoturva",
|
||||
"clipboardClearTimeout": "Tyhjennä leikepöytä kopioimisen jälkeen",
|
||||
"clipboardClearTimeoutDescription": "Tyhjennä leikepöytä automaattisesti arkaluonteisten tietojen kopioimisen jälkeen",
|
||||
"clipboardClearDisabled": "Älä koskaan tyhjennä",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 tuntia",
|
||||
"autoLock24Hours": "24 tuntia",
|
||||
"versionPrefix": "Versio",
|
||||
"preferences": "Määritykset",
|
||||
"autofillSettings": "Automaatisen täytön asetukset",
|
||||
"clipboardSettings": "Leikepöydän asetukset",
|
||||
"contextMenuSettings": "Kontekstivalikon asetukset",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Automaattisen täytön ehdotukset näkyvät kirjautumislomakkeissa",
|
||||
"autofillDisabledDescription": "Automaattitäyttöehdotukset on poistettu käytöstä kaikkialla",
|
||||
"languageSettings": "Keili",
|
||||
"languageSettingsDescription": "Valitse ensisijainen kieli",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API-URL-osoite vaaditaan",
|
||||
"apiUrlInvalid": "Syötä kelvollinen API-URL-osoite",
|
||||
"clientUrlRequired": "Asiakkaan URL-osoite vaaditaan",
|
||||
"clientUrlInvalid": "Syötä kelvollinen asiakas-URL-osoite"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Holvin lukituksen avausmenetelmä",
|
||||
"introText": "Valitse, miten haluat avata holvisi. Voit käyttää pääsalasanaa (aina saatavilla) tai määrittää nopean 4-8 numeroisen PIN-koodin nopeampaa käyttöä varten. Kolmen epäonnistuneen PIN-yrityksen jälkeen sinun on käytettävä pääsalasanaa.",
|
||||
"password": "Pääsalasana",
|
||||
"pin": "PIN-koodi",
|
||||
"pinDescription": "Avaa holvi PIN-koodilla",
|
||||
"setupPin": "PIN-koodin määrittäminen",
|
||||
"enterNewPinDescription": "Syötä PIN-koodi, jossa on vähintään 6 numeroa",
|
||||
"confirmPin": "Vahvista PIN",
|
||||
"confirmPinDescription": "Syötä PIN-koodi uudelleen vahvistaaksesi",
|
||||
"invalidPinFormat": "Virheellinen PIN-muoto",
|
||||
"pinMismatch": "PIN-koodit eivät täsmää",
|
||||
"incorrectPin": "Väärä PIN-koodi. {{attemptsRemaining}} yritystä jäljellä.",
|
||||
"incorrectPinSingular": "Virheellinen PIN-koodi. 1 yritys jäljellä.",
|
||||
"enableSuccess": "PIN-lukituksen avaus käytössä onnistuneesti!",
|
||||
"pinLocked": "PIN-lukituksen avaus on poistettu käytöstä. Ole hyvä ja käytä pääsalasanaa avataksesi holvisi.",
|
||||
"pinSecurityWarning": "PIN-koodin lukituksen poisto selaimen laajennuksessa voi olla vähemmän turvallista kuin pääsalasana, koska PIN-koodit ovat tyypillisesti alempia entropia ja voi olla brute-pakko jos laite on vaarassa. Käytä sitä vain laitteissa, joihin täysin luotat."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Otsikko",
|
||||
"titlePlaceholder": "Anna nimi tälle todennusavaimelle",
|
||||
"createButton": "Luo todennusavain, Passkey",
|
||||
"creatingButton": "Luodaan...",
|
||||
"useBrowserPasskey": "Käytä selaimen sala-avainta",
|
||||
"selectPasskeyToReplace": "Valitse todennusavain, johon korvataan",
|
||||
"createNewPasskey": "Luo uusi sala-avain",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Todennusavaimen toimittaja",
|
||||
"passkeyProviderOn": "Todennusavaimen toimittaja käytössä ",
|
||||
"enable": "Ota AliasVault käyttöön todennusavainten tarjoajana",
|
||||
"description": "Kun AliasVault on käytössä, se käsittelee todennusavaimia verkkosivustoilta. Kun sivusto pyytää todennusavainta, AliasVaultin ponnahdusikkuna näytetään natiivin selaimen tai käyttöjärjestelmän todennusavaimen sijaan."
|
||||
"passkeyProviderOn": "Todennusavaimen toimittaja käytössä "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "Mitä uutta?",
|
||||
"whatsNewDescription": "Päivitys vaaditaan seuraavien muutosten tukemiseksi:",
|
||||
"noDescriptionAvailable": "Tälle versiolle ei ole saatavilla kuvausta.",
|
||||
"okay": "Hyvä on",
|
||||
"status": {
|
||||
"preparingUpgrade": "Valmistellaan päivitystä...",
|
||||
"vaultAlreadyUpToDate": "Holvi on jo ajan tasalla",
|
||||
"startingDatabaseTransaction": "Aloitetaan tietokannan transaktiota...",
|
||||
"applyingDatabaseMigrations": "Toteutetaan tietokannan siirtoja...",
|
||||
"applyingMigration": "Otetaan siirto käyttöön {{current}} / {{total}}...",
|
||||
"committingChanges": "Otetaan muutokset käyttöön..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Virhe",
|
||||
"unableToGetVersionInfo": "Versiotietojen hakeminen epäonnistui. Yritä uudelleen.",
|
||||
"selfHostedServer": "Itseisännöity palvelin",
|
||||
"selfHostedWarning": "Jos käytät itseisännöityä palvelinta, muista päivittää myös itseisännöity instanssisi, koska muuten verkkoasiakassovellukseen kirjautuminen lakkaa toimimasta.",
|
||||
"cancel": "Peruuta",
|
||||
"continueUpgrade": "Jatka päivitystä",
|
||||
"upgradeFailed": "Päivitys epäonnistui",
|
||||
"failedToApplyMigration": "Siirron käyttöönotto epäonnistui ({{current}} / {{total}})"
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
"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",
|
||||
"unlockVault": "Déverrouiller",
|
||||
"unlockWithPin": "Déverrouiller avec un code PIN",
|
||||
"enterPinToUnlock": "Entrez votre code PIN pour déverrouiller votre coffre",
|
||||
"useMasterPassword": "Utiliser le mot de passe principal",
|
||||
"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",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Connexion à",
|
||||
"switchAccounts": "Changer de compte ?",
|
||||
"loggedIn": "Connecté(e)",
|
||||
"loginWithMobile": "Se connecter à l'aide de l'application mobile",
|
||||
"unlockWithMobile": "Déverrouiller en utilisant l'application mobile",
|
||||
"scanQrCode": "Scannez ce code QR avec votre application mobile AliasVault pour vous connecter et déverrouiller votre coffre.",
|
||||
"errors": {
|
||||
"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.",
|
||||
"sessionExpired": "Votre session a expiré. Veuillez vous reconnecter."
|
||||
"sessionExpired": "Votre session a expiré. Veuillez vous reconnecter.",
|
||||
"mobileLoginRequestExpired": "La demande de connexion de l'application mobile a expiré. Veuillez recharger la page et réessayer."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Réglages"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Chargement...",
|
||||
"notice": "Notification",
|
||||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"cancel": "Annuler",
|
||||
"back": "Back",
|
||||
"confirm": "Confirmer",
|
||||
"back": "Retour",
|
||||
"next": "Suivant",
|
||||
"use": "Utiliser",
|
||||
"delete": "Supprimer",
|
||||
"or": "Or",
|
||||
"save": "Sauvegarder",
|
||||
"or": "Ou",
|
||||
"close": "Fermer",
|
||||
"copied": "Copié !",
|
||||
"openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
|
||||
"language": "Language",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"showPassword": "Afficher le mot de passe",
|
||||
"hidePassword": "Cacher le mot de passe",
|
||||
"showDetails": "Afficher les détails",
|
||||
"hideDetails": "Masquer les détails",
|
||||
"copyToClipboard": "Copier dans le presse-papiers",
|
||||
"loadingEmails": "Chargement des emails...",
|
||||
"loadingTotpCodes": "Chargement des codes TOTP...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"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.",
|
||||
"browserExtensionOutdated": "Cette extension de navigateur est obsolète et ne peut pas être utilisée pour accéder à ce coffre-fort. Veuillez la mettre à jour pour continuer.",
|
||||
"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.",
|
||||
"serverVersionTooOld": "Le serveur AliasVault doit être mis à jour vers une version plus récente pour pouvoir utiliser cette fonctionnalité. Veuillez contacter l'administrateur du serveur si vous avez besoin d'aide.",
|
||||
"unknownError": "Une erreur inconnue s'est produite",
|
||||
"unknownErrorTryAgain": "Une erreur inconnue s'est produite. Merci de réessayer.",
|
||||
"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": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"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 d’utilisateur requis.",
|
||||
"USERNAME_ALREADY_IN_USE": "Nom d'utilisateur déjà utilisé.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "ou",
|
||||
"new": "Nouveautés",
|
||||
"cancel": "Annuler",
|
||||
"search": "Rechercher",
|
||||
"vaultLocked": "AliasVault est verrouillé.",
|
||||
"creatingNewAlias": "Création de nouveaux alias...",
|
||||
"noMatchesFound": "Aucun résultat trouvé",
|
||||
@@ -164,7 +166,6 @@
|
||||
"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"
|
||||
@@ -176,44 +177,19 @@
|
||||
"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": "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": "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.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
"noAttachmentsFound": "No credentials with attachments found",
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"noAttachmentsFound": "Aucun identifiant avec des pièces jointes trouvé",
|
||||
"noMatchingCredentials": "Aucun identifiant correspondant trouvé",
|
||||
"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.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Entrez l'adresse email complète",
|
||||
"enterEmailPrefix": "Entrez le préfixe de l'email"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Nom (facultatif)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Supprimer l'email",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Ouvrir l’application web",
|
||||
"loggedIn": "Connecté(e)",
|
||||
"logout": "Se déconnecter",
|
||||
"lock": "Lock",
|
||||
"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",
|
||||
@@ -331,7 +316,6 @@
|
||||
"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",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 heures",
|
||||
"autoLock24Hours": "24 heures",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Préférences",
|
||||
"autofillSettings": "Paramètres du remplissage automatique",
|
||||
"clipboardSettings": "Paramètres du presse-papiers",
|
||||
"contextMenuSettings": "Paramètres du menu contextuel",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"validation": {
|
||||
"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"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Vault Unlock Method",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "Master Password",
|
||||
"pin": "Code PIN",
|
||||
"pinDescription": "Unlock vault with PIN code",
|
||||
"setupPin": "Setup PIN Code",
|
||||
"enterNewPinDescription": "Enter a PIN code consisting of minimum 6 digits",
|
||||
"confirmPin": "Confirm PIN",
|
||||
"confirmPinDescription": "Enter your PIN again to confirm",
|
||||
"invalidPinFormat": "Invalid PIN format",
|
||||
"pinMismatch": "PINs do not match",
|
||||
"incorrectPin": "Incorrect PIN. {{attemptsRemaining}} attempts remaining.",
|
||||
"incorrectPinSingular": "Incorrect PIN. 1 attempt remaining.",
|
||||
"enableSuccess": "PIN unlock enabled successfully!",
|
||||
"pinLocked": "PIN unlock has been disabled. Please use your master password to unlock your vault.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProviderOn": "Passkey Provider on "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"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": "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": "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}})"
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
"authCode": "קוד אימות",
|
||||
"authCodePlaceholder": "נא למלא קוד באורך 6 ספרות",
|
||||
"verify": "אימות",
|
||||
"cancel": "ביטול",
|
||||
"twoFactorNote": "לתשומת ליבך: אם אין לך גישה להתקן המאמת (authenticator) שלך, אפשר לאפס אימות דו־שלבי עם קוד שחזור על ידי כניסה דרך האתר.",
|
||||
"masterPassword": "סיסמת על",
|
||||
"unlockVault": "שחרור נעילת כספת",
|
||||
"unlockVault": "Unlock",
|
||||
"unlockWithPin": "שחרור נעילה עם קוד אישי",
|
||||
"enterPinToUnlock": "נא למלא את הקוד האישי שלך כדי לשחרר את הכספת שלך",
|
||||
"useMasterPassword": "להשתמש בסיסמה על",
|
||||
"unlockTitle": "שחרור נעילת הכספת שלך",
|
||||
"unlockDescription": "נא למלא את סיסמת העל שלך כדי לשחרר את הכספת שלך.",
|
||||
"logout": "יציאה",
|
||||
"logoutConfirm": "לצאת?",
|
||||
"sessionExpired": "תוקף ההפעלה שלך פג. נא להיכנס מחדש.",
|
||||
"unlockSuccess": "נעילת הכספת שוחררה בהצלחה!",
|
||||
"unlockSuccessTitle": "נעילת הכספת שלך נפתחה בהצלחה",
|
||||
"unlockSuccessDescription": "מעתה ניתן להשתמש בהשלמה אוטומטית בטופסי כניסה בדפדפן שלך.",
|
||||
"closePopup": "סגירת החלונית הצצה הזאת",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "מתבצעת התחברות אל",
|
||||
"switchAccounts": "להחליף חשבונות?",
|
||||
"loggedIn": "נכנסת",
|
||||
"loginWithMobile": "Log in using Mobile App",
|
||||
"unlockWithMobile": "Unlock using Mobile App",
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
|
||||
"errors": {
|
||||
"invalidCode": "נא למלא קוד אימות באורך 6 ספרות.",
|
||||
"serverError": "לא ניתן ליצור קשר עם השרת של AliasVault. נא לנסות שוב מאוחר יותר או ליצור קשר עם התמיכה אם הבעיה נשנית.",
|
||||
"noToken": "הכניסה נכשלה - לא הוחזר אסימון",
|
||||
"migrationError": "אירעה שגיאה בעת בדיקה לאיתור הסבות ממתינות.",
|
||||
"wrongPassword": "סיסמה שגויה. נא לנסות שוב.",
|
||||
"accountLocked": "החשבון נעול זמנית עקב ריבוי ניסיונות כושלים.",
|
||||
"networkError": "שגיאת רשת. נא לבדוק את החיבור ולנסות שוב.",
|
||||
"sessionExpired": "תוקף ההפעלה שלך פג. נא להיכנס מחדש."
|
||||
"sessionExpired": "תוקף ההפעלה שלך פג. נא להיכנס מחדש.",
|
||||
"mobileLoginRequestExpired": "Mobile login request timed out. Please reload the page and try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "הגדרות"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "בטעינה…",
|
||||
"notice": "לתשומת ליבך",
|
||||
"error": "שגיאה",
|
||||
"success": "הצליח",
|
||||
"cancel": "ביטול",
|
||||
"confirm": "אישור",
|
||||
"back": "חזרה",
|
||||
"next": "הבא",
|
||||
"use": "להשתמש",
|
||||
"delete": "מחיקה",
|
||||
"save": "שמירה",
|
||||
"or": "או",
|
||||
"close": "סגירה",
|
||||
"copied": "הועתק!",
|
||||
"openInNewWindow": "פתיחה בחלון חדש",
|
||||
"language": "שפה",
|
||||
"enabled": "פעיל",
|
||||
"disabled": "כבוי",
|
||||
"showPassword": "הצגת סיסמה",
|
||||
"hidePassword": "הסתרת סיסמה",
|
||||
"showDetails": "הצגת פרטים",
|
||||
"hideDetails": "הסתרת פרטים",
|
||||
"copyToClipboard": "העתקה ללוח הגזירים",
|
||||
"loadingEmails": "הודעות הדוא״ל נטענות…",
|
||||
"loadingTotpCodes": "הקודים החד־פעמיים הזמניים נטענים…",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "הגרסה הזאת של הרחבת הדפדפן של AliasVault לא נתמכת עוד על ידי השרת. נא לעדכן את הרחבת הדפדפן שלך לגרסה העדכנית ביותר.",
|
||||
"browserExtensionOutdated": "הרחבת הדפדפן הזאת לא עדכנית ואי אפשר להשתמש בה כדי לגשת לכספת הזאת. נא לעדכן את הרחבת הדפדפן הזאת כדי להמשיך.",
|
||||
"serverVersionNotSupported": "יש לעדכן את שרת AliasVault לגרסה חדשה יותר כדי להשתמש בהרחבת הדפדפן הזאת. נא ליצור קשר עם התמיכה לקבלת עזרה.",
|
||||
"serverVersionTooOld": "The AliasVault server needs to be updated to a newer version in order to use this feature. Please contact the server admin if you need help.",
|
||||
"unknownError": "אירעה שגיאה לא ידועה",
|
||||
"unknownErrorTryAgain": "אירעה שגיאה לא ידועה, נא לנסות שוב.",
|
||||
"vaultNotAvailable": "הכספת לא זמינה",
|
||||
"failedToRetrieveData": "משיכת הנתונים נכשלה",
|
||||
"vaultIsLocked": "הכספת נעולה",
|
||||
"failedToUploadVault": "העלאת הכספת נכשלה",
|
||||
"passwordChanged": "הסיסמה שלך השתנתה מאז הפעם האחרונה שנכנסת למערכת. נא להיכנס שוב מטעמי אבטחת מידע."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "קוד שחזור שגוי. נא לנסות שוב.",
|
||||
"REFRESH_TOKEN_REQUIRED": "אסימון ריענון חובה.",
|
||||
"INVALID_REFRESH_TOKEN": "אסימון ריענון שגוי.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "אסימון הריענון נשלל בהצלחה.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "רישום חשבון חדש מושבת כרגע בשרת הזה. נא ליצור קשר עם ההנהלה.",
|
||||
"USERNAME_REQUIRED": "שם משתמש חובה.",
|
||||
"USERNAME_ALREADY_IN_USE": "שם המשתמש כבר תפוס.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "או",
|
||||
"new": "חדש",
|
||||
"cancel": "ביטול",
|
||||
"search": "חיפוש",
|
||||
"vaultLocked": "AliasVault נעול.",
|
||||
"creatingNewAlias": "נוצר כינוי חדש...",
|
||||
"noMatchesFound": "לא נמצאו תוצאות",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "יצירת סיסמה חדשה",
|
||||
"togglePasswordVisibility": "הצגת/הסתרת סיסמה",
|
||||
"passwordCopiedToClipboard": "הסיסמה הועתקה ללוח הגזירים",
|
||||
"enterEmailAndOrUsernameError": "נא למלא דוא״ל ו/או שם משתמש",
|
||||
"openAliasVaultToUpgrade": "יש לפתוח את AliasVault כדי לשדרג",
|
||||
"vaultUpgradeRequired": "יש לשדרג את הכספת.",
|
||||
"dismissPopup": "התעלמות מחלונית"
|
||||
@@ -176,27 +177,12 @@
|
||||
"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 כדי ליצור פרטי גישה חדשים.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"createdAt": "יצירה",
|
||||
"updatedAt": "עדכון אחרון",
|
||||
"autofill": "השלמה אוטומטית",
|
||||
"fillForm": "מילוי טופס",
|
||||
"deleteConfirm": "למחוק את פרטי הגישה האלה?",
|
||||
"saveSuccess": "פרטי הגישה נשמרו בהצלחה",
|
||||
"tags": "תגיות",
|
||||
"addTag": "הוספת תגית",
|
||||
"removeTag": "הסרת תגית",
|
||||
"folder": "תיקייה",
|
||||
"selectFolder": "בחירת תיקייה",
|
||||
"createFolder": "יצירת תיקייה",
|
||||
"saveCredential": "שמירת פרטי גישה",
|
||||
"deleteCredentialTitle": "מחיקת פרטי גישה",
|
||||
"deleteCredentialConfirm": "למחוק את פרטי הגישה? זאת פעולה בלתי הפיכה.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "נא למלא כתובת דוא״ל מלאה",
|
||||
"enterEmailPrefix": "נא למלא קידומת דוא״ל"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "שם (רשות)",
|
||||
"secretKey": "מפתח סודי",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "הודעות דוא״ל",
|
||||
"deleteEmailTitle": "מחיקת הודעת דוא״ל",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "פתיחת אתר",
|
||||
"loggedIn": "נכנסת",
|
||||
"logout": "יציאה",
|
||||
"lock": "נעילה",
|
||||
"globalSettings": "הגדרות מקיפות",
|
||||
"autofillPopup": "חלונית השלמה אוטומטית",
|
||||
"activeOnAllSites": "פעיל בכל האתרים (למעט אם נכבה להלן)",
|
||||
"disabledOnAllSites": "כבוי בכל האתרים",
|
||||
"enabled": "פעיל",
|
||||
"disabled": "כבוי",
|
||||
"rightClickContextMenu": "תפריט הקשר בלחיצה ימנית",
|
||||
"autofillMatching": "התאמת השלמה אוטומטית",
|
||||
"autofillMatchingMode": "מצב התאמת השלמה אוטומטית",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "קיצורי מקלדת",
|
||||
"configureKeyboardShortcuts": "הגדרת קיצורי מקלדת",
|
||||
"configure": "הגדרה",
|
||||
"security": "אבטחה",
|
||||
"clipboardClearTimeout": "לפנות את לוח הגזירים לאחר העתקה",
|
||||
"clipboardClearTimeoutDescription": "לפנות את לוח הגזירים אוטומטית לאחר העתקת נתונים רגישים",
|
||||
"clipboardClearDisabled": "אף פעם לא לפנות",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 שעות",
|
||||
"autoLock24Hours": "24 שעות",
|
||||
"versionPrefix": "גרסה ",
|
||||
"preferences": "העדפות",
|
||||
"autofillSettings": "הגדרות השלמה אוטומטית",
|
||||
"clipboardSettings": "הגדרות לוח הגזירים",
|
||||
"contextMenuSettings": "הגדרות תפריט הקשר",
|
||||
@@ -362,22 +345,39 @@
|
||||
"contextMenuDisabled": "תפריט הקשר כבוי",
|
||||
"contextMenuDescription": "ניתן ללחוץ על שדה עם הלחצן הימני כדי לגשת לאפשרויות AliasVault",
|
||||
"selectLanguage": "בחירת שפה",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
|
||||
"serverConfiguration": "הגדרות שרת",
|
||||
"serverConfigurationDescription": "הגדרת כתובת שרת AliasVault לעותקים באירוח עצמי",
|
||||
"customApiUrl": "כתובת API",
|
||||
"customClientUrl": "כתובת לקוח",
|
||||
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
|
||||
"clientUrlHint": "The web interface URL of your self-hosted instance",
|
||||
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"apiUrlHint": "נקודת הקצה של ה־API (בדרך כלל כתובת לקוח + /api)",
|
||||
"clientUrlHint": "כתובת ממשק המשתמש הדפדפני של העותק באירוע העצמי שלך",
|
||||
"autofillSettingsDescription": "הפעלת או כיבוי חלוניות השלמה אוטומטית בעמודי אתרים",
|
||||
"autofillEnabledDescription": "הצעות השלמה אוטומטית בטופסי כניסה למערכות",
|
||||
"autofillDisabledDescription": "הצעות השלמה אוטומטית כבויות באופן מקיף",
|
||||
"languageSettings": "שפה",
|
||||
"validation": {
|
||||
"apiUrlRequired": "כתובת API חובה",
|
||||
"apiUrlInvalid": "נא למלא כתובת API תקפה",
|
||||
"clientUrlRequired": "כתובת לקוח חובה",
|
||||
"clientUrlInvalid": "נא למלא כתובת לקוח תקפה"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "שיטת שחרור נעילת כספת",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "סיסמת על",
|
||||
"pin": "קוד אישי",
|
||||
"pinDescription": "שחרור כספת עם קוד אישי",
|
||||
"setupPin": "הגדרת קוד גישה אישי",
|
||||
"enterNewPinDescription": "נא למלא קוד אישי שמורכב מ־6 ספרות לפחות",
|
||||
"confirmPin": "אישור קוד אישי",
|
||||
"confirmPinDescription": "נא למלא את הקוד האישי שלך שוב לאישור",
|
||||
"invalidPinFormat": "תבנית הקוד האישי שגויה",
|
||||
"pinMismatch": "הקודים האישיים לא תואמים",
|
||||
"incorrectPin": "קוד אישי שגוי. נותרו {{attemptsRemaining}} ניסיונות.",
|
||||
"incorrectPinSingular": "קוד אישי שגוי. נותר עוד ניסיון.",
|
||||
"enableSuccess": "שחרור קוד אישי הופעל בהצלחה!",
|
||||
"pinLocked": "שחרור נעילה עם קוד אישי הושבתה. נא להשתמש בסיסמת העל שלך כדי לשחרר את הכספת שלך.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "כותרת",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "נוצר…",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProviderOn": "Passkey Provider on "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "מה חדש",
|
||||
"whatsNewDescription": "יש לשדרג כדי שתהיה תמיכה בשינויים הבאים:",
|
||||
"noDescriptionAvailable": "אין תיאור זמין לגרסה הזאת.",
|
||||
"okay": "אישור",
|
||||
"status": {
|
||||
"preparingUpgrade": "השדרוג בהכנה…",
|
||||
"vaultAlreadyUpToDate": "הכספת כבר עדכנית",
|
||||
"startingDatabaseTransaction": "הסבת מסד הנתונים מתחילה…",
|
||||
"applyingDatabaseMigrations": "השינויים חלים על מסד הנתונים…",
|
||||
"applyingMigration": "חלה ההסבה {{current}} מתוך {{total}}…",
|
||||
"committingChanges": "השינויים מקובעים…"
|
||||
},
|
||||
"alerts": {
|
||||
"error": "שגיאה",
|
||||
"unableToGetVersionInfo": "לא ניתן לקבל את פרטי הגרסה. נא לנסות שוב מאוחר יותר.",
|
||||
"selfHostedServer": "שרת באירוח עצמי",
|
||||
"selfHostedWarning": "אם מדובר בשרת שמתארח עצמאית, נא לוודא שהעותק שמתארח אצלך גם כן מתעדכן כי אחרת הכניסה לאתר תפסיק לעבוד.",
|
||||
"cancel": "ביטול",
|
||||
"continueUpgrade": "להמשיך בשדרוג",
|
||||
"upgradeFailed": "השדרוג נכשל",
|
||||
"failedToApplyMigration": "החלת ההסבה נכשלה ({{current}} מתוך {{total}})"
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
"authCode": "Codice di Autenticazione",
|
||||
"authCodePlaceholder": "Inserisci il codice a 6 cifre",
|
||||
"verify": "Verifica",
|
||||
"cancel": "Annulla",
|
||||
"twoFactorNote": "Nota: se non hai accesso al tuo dispositivo di autenticazione, puoi reimpostare il tuo 2FA con un codice di recupero accedendo tramite il sito web.",
|
||||
"masterPassword": "Password principale",
|
||||
"unlockVault": "Sblocca Cassaforte",
|
||||
"unlockVault": "Sblocca",
|
||||
"unlockWithPin": "Sblocca con PIN",
|
||||
"enterPinToUnlock": "Inserisci il PIN per sbloccare la cassaforte",
|
||||
"useMasterPassword": "Usa La Password Principale",
|
||||
"unlockTitle": "Sblocca la tua cassaforte",
|
||||
"unlockDescription": "Inserisci la tua password principale per sbloccare la tua cassaforte.",
|
||||
"logout": "Disconnetti",
|
||||
"logoutConfirm": "Sei sicuro di volerti disconnettere?",
|
||||
"sessionExpired": "La sessione è scaduta. Effettua di nuovo il login.",
|
||||
"unlockSuccess": "Cassaforte sbloccata con successo!",
|
||||
"unlockSuccessTitle": "La cassaforte è stata sbloccata con successo",
|
||||
"unlockSuccessDescription": "Ora puoi usare l'auto-riempimento nei moduli di accesso nel tuo browser.",
|
||||
"closePopup": "Chiudi questo popup",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Connessione a",
|
||||
"switchAccounts": "Cambia account",
|
||||
"loggedIn": "Accesso effettuato",
|
||||
"loginWithMobile": "Accedi con l'App Mobile",
|
||||
"unlockWithMobile": "Sblocca con l'App Mobile",
|
||||
"scanQrCode": "Scansiona questo codice QR con l'app mobile di AliasVault per accedere e sbloccare la cassaforte.",
|
||||
"errors": {
|
||||
"invalidCode": "Inserisci un codice di autenticazione a 6 cifre valido.",
|
||||
"serverError": "Impossibile connettersi al server di AliasVault. Riprova più tardi o contatta il supporto se il problema persiste.",
|
||||
"noToken": "Accesso fallito — nessun token ricevuto",
|
||||
"migrationError": "Si è verificato un errore nel controllo delle migrazioni pendenti.",
|
||||
"wrongPassword": "Password non corretta. Riprova nuovamente.",
|
||||
"accountLocked": "Account temporaneamente bloccato a causa di troppi tentativi falliti.",
|
||||
"networkError": "Errore di rete: Controlla la tua connessione e riprova.",
|
||||
"sessionExpired": "La tua sessione è scaduta. Effettua di nuovo il login."
|
||||
"sessionExpired": "La tua sessione è scaduta. Effettua di nuovo il login.",
|
||||
"mobileLoginRequestExpired": "Richiesta di accesso mobile scaduta. Per favore ricarica la pagina e riprova."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Impostazioni"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Caricamento in corso...",
|
||||
"notice": "Avviso",
|
||||
"error": "Errore",
|
||||
"success": "Riuscito",
|
||||
"cancel": "Annulla",
|
||||
"confirm": "Conferma",
|
||||
"back": "Indietro",
|
||||
"next": "Avanti",
|
||||
"use": "Usa",
|
||||
"delete": "Elimina",
|
||||
"save": "Salva",
|
||||
"or": "O",
|
||||
"close": "Chiudi",
|
||||
"copied": "Copiato!",
|
||||
"openInNewWindow": "Apri in una nuova finestra",
|
||||
"language": "Lingua",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"showPassword": "Mostra password",
|
||||
"hidePassword": "Nascondi password",
|
||||
"showDetails": "Mostra dettagli",
|
||||
"hideDetails": "Nascondi dettagli",
|
||||
"copyToClipboard": "Copia negli appunti",
|
||||
"loadingEmails": "Caricamento e-mail in corso...",
|
||||
"loadingTotpCodes": "Caricamento codici TOTP in corso...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "Questa versione dell'estensione del browser AliasVault non è più supportata dal server. Aggiorna l'estensione alla versione più recente.",
|
||||
"browserExtensionOutdated": "Questa estensione del browser è obsoleta e non può essere utilizzata per accedere a questa cassaforte. Si prega di aggiornare questa estensione per continuare.",
|
||||
"serverVersionNotSupported": "Il server di AliasVault necessita un aggiornamento a una versione più recente per poter usare questa estensione. Contatta il supporto se hai bisogno di assistenza.",
|
||||
"serverVersionTooOld": "Il server AliasVault deve essere aggiornato a una versione più recente per poter utilizzare questa app mobile. Se hai bisogno di aiuto, contatta il supporto.",
|
||||
"unknownError": "Si è verificato un errore sconosciuto",
|
||||
"unknownErrorTryAgain": "Si è verificato un errore sconosciuto. Riprova.",
|
||||
"vaultNotAvailable": "Cassaforte non disponibile",
|
||||
"failedToRetrieveData": "Recupero dati non riuscito",
|
||||
"vaultIsLocked": "La cassaforte è bloccata",
|
||||
"failedToUploadVault": "Caricare della cassaforte non riuscito.",
|
||||
"passwordChanged": "La tua password è cambiata dall'ultima volta che hai effettuato l'accesso. Effettua nuovamente l'accesso per motivi di sicurezza."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Codice di recupero non valido. Riprova.",
|
||||
"REFRESH_TOKEN_REQUIRED": "È necessario aggiornare il token.",
|
||||
"INVALID_REFRESH_TOKEN": "Token di aggiornamento non valido",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Aggiornamento token revocato con successo.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "La registrazione di nuovi account è attualmente disabilitata su questo server. Contatta l'amministratore.",
|
||||
"USERNAME_REQUIRED": "È richiesto il nome utente.",
|
||||
"USERNAME_ALREADY_IN_USE": "Il nome utente è già in uso.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "o",
|
||||
"new": "Nuovo",
|
||||
"cancel": "Annulla",
|
||||
"search": "Cerca",
|
||||
"vaultLocked": "AliasVault è bloccato.",
|
||||
"creatingNewAlias": "Creazione nuovo alias...",
|
||||
"noMatchesFound": "Nessun risultato trovato",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Genera nuova password",
|
||||
"togglePasswordVisibility": "Mostra/Nascondi password",
|
||||
"passwordCopiedToClipboard": "Password copiata negli appunti",
|
||||
"enterEmailAndOrUsernameError": "Inserisci email e/o nome utente",
|
||||
"openAliasVaultToUpgrade": "Apri AliasVault per aggiornare",
|
||||
"vaultUpgradeRequired": "Aggiornamento della cassaforte richiesto.",
|
||||
"dismissPopup": "Chiudi finestra"
|
||||
@@ -176,44 +177,19 @@
|
||||
"deleteCredential": "Elimina credenziali",
|
||||
"credentialDetails": "Dettagli credenziali",
|
||||
"serviceName": "Nome servizio",
|
||||
"serviceNamePlaceholder": "es. Gmail, Facebook, Banca",
|
||||
"website": "Sito web",
|
||||
"websitePlaceholder": "https://esempio.com",
|
||||
"username": "Nome utente",
|
||||
"usernamePlaceholder": "Inserisci nome utente",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Inserisci password",
|
||||
"generatePassword": "Genera password",
|
||||
"copyPassword": "Copia password",
|
||||
"showPassword": "Mostra password",
|
||||
"hidePassword": "Nascondi password",
|
||||
"notes": "Note",
|
||||
"notesPlaceholder": "Note aggiuntive...",
|
||||
"totp": "Autenticazione a due fattori",
|
||||
"totpCode": "Codice TOTP",
|
||||
"copyTotp": "Copia TOTP",
|
||||
"totpSecret": "Segreto TOTP",
|
||||
"totpSecretPlaceholder": "Inserisci chiave segreta TOTP",
|
||||
"noCredentials": "Credenziali non trovate",
|
||||
"noCredentialsDescription": "Aggiungi le tue prime credenziali per iniziare",
|
||||
"searchPlaceholder": "Cerca credenziali...",
|
||||
"welcomeTitle": "Benvenuto in AliasVault!",
|
||||
"welcomeDescription": "Per usare l'estensione browser AliasVault: naviga su un sito e usa la finestra di compilazione automatica per creare una nuova credenziale.",
|
||||
"noPasskeysFound": "Non sono state ancora create chiavi di accesso. Le passkey vengono create visitando un sito web che offre le chiavi di accesso come metodo di autenticazione.",
|
||||
"noAttachmentsFound": "No credentials with attachments found",
|
||||
"noAttachmentsFound": "Non sono state trovate credenziali con allegati",
|
||||
"noMatchingCredentials": "Nessuna credenziale corrispondente trovata",
|
||||
"createdAt": "Creato",
|
||||
"updatedAt": "Ultimo aggiornamento",
|
||||
"autofill": "Compilazione automatica",
|
||||
"fillForm": "Compila modulo",
|
||||
"deleteConfirm": "Sei sicuro di voler eliminare questa credenziale?",
|
||||
"saveSuccess": "Credenziali salvate con successo",
|
||||
"tags": "Tag",
|
||||
"addTag": "Aggiungi tag",
|
||||
"removeTag": "Rimuovi tag",
|
||||
"folder": "Cartella",
|
||||
"selectFolder": "Seleziona cartella",
|
||||
"createFolder": "Crea cartella",
|
||||
"saveCredential": "Salva credenziale",
|
||||
"deleteCredentialTitle": "Elimina credenziale",
|
||||
"deleteCredentialConfirm": "Sei sicuro di voler eliminare queste credenziali? Questa azione non può essere annullata.",
|
||||
@@ -222,7 +198,7 @@
|
||||
"passkeys": "Passkey",
|
||||
"aliases": "Alias",
|
||||
"userpass": "Password",
|
||||
"attachments": "Attachments"
|
||||
"attachments": "Allegati"
|
||||
},
|
||||
"randomAlias": "Alias casuale",
|
||||
"manual": "Manuale",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Inserisci l'indirizzo email completo",
|
||||
"enterEmailPrefix": "Inserisci prefisso email"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Aggiungi Codice 2FA",
|
||||
"instructions": "Inserisci la chiave segreta mostrata dal sito in cui vuoi aggiungere un'autenticazione a due fattori.",
|
||||
"nameOptional": "Nome (facoltativo)",
|
||||
"secretKey": "Chiave segreta",
|
||||
"saveToViewCode": "Salva per visualizzare il codice",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Formato chiave segreta non valido."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Email",
|
||||
"deleteEmailTitle": "Elimina Email",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Apri app web",
|
||||
"loggedIn": "Accesso effettuato",
|
||||
"logout": "Disconnetti",
|
||||
"lock": "Blocca",
|
||||
"globalSettings": "Impostazioni globali",
|
||||
"autofillPopup": "Popup compilazione automatica",
|
||||
"activeOnAllSites": "Attivo su tutti i siti (a meno che non sia disabilitato sotto)",
|
||||
"disabledOnAllSites": "Disabilitato su tutti i siti",
|
||||
"enabled": "Abilitato",
|
||||
"disabled": "Disabilitato",
|
||||
"rightClickContextMenu": "Menu contestuale clic destro",
|
||||
"autofillMatching": "Riconoscimento campi automatica.",
|
||||
"autofillMatchingMode": "Modalità riconoscimento capi automatica",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Scorciatoie da tastiera",
|
||||
"configureKeyboardShortcuts": "Configura scorciatoie da tastiera",
|
||||
"configure": "Configura",
|
||||
"security": "Sicurezza",
|
||||
"clipboardClearTimeout": "Cancella appunti dopo la copia",
|
||||
"clipboardClearTimeoutDescription": "Cancella automaticamente gli appunti dopo aver copiato i dati sensibili",
|
||||
"clipboardClearDisabled": "Non pulire mai",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 ore",
|
||||
"autoLock24Hours": "24 ore",
|
||||
"versionPrefix": "Versione ",
|
||||
"preferences": "Preferenze",
|
||||
"autofillSettings": "Impostazioni di riempimento automatico",
|
||||
"clipboardSettings": "Impostazioni appunti",
|
||||
"contextMenuSettings": "Preferenze menu contestuale",
|
||||
@@ -362,22 +345,39 @@
|
||||
"contextMenuDisabled": "Il menu contestuale è disabilitato",
|
||||
"contextMenuDescription": "Click destro sui campi di input per accedere alle opzioni di AliasVault",
|
||||
"selectLanguage": "Seleziona la lingua",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
|
||||
"serverConfiguration": "Configurazione del server",
|
||||
"serverConfigurationDescription": "Configurare l'URL del server AliasVault per le istanze self-hosted",
|
||||
"customApiUrl": "API URL",
|
||||
"customClientUrl": "Client URL",
|
||||
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
|
||||
"clientUrlHint": "The web interface URL of your self-hosted instance",
|
||||
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"customClientUrl": "Url Client",
|
||||
"apiUrlHint": "L'URL endpoint API (di solito URL client + /api)",
|
||||
"clientUrlHint": "L'URL dell'interfaccia web della tua istanza self-hosted",
|
||||
"autofillSettingsDescription": "Abilita o disabilita il popup di riempimento automatico sulle pagine web",
|
||||
"autofillEnabledDescription": "I suggerimenti di riempimento automatico appariranno nei moduli di accesso",
|
||||
"autofillDisabledDescription": "I suggerimenti di riempimento automatico sono disabilitati",
|
||||
"languageSettings": "Lingua",
|
||||
"validation": {
|
||||
"apiUrlRequired": "L'URL API è obbligatorio",
|
||||
"apiUrlInvalid": "Inserisci un URL API valido",
|
||||
"clientUrlRequired": "L'URL del client è obbligatorio",
|
||||
"clientUrlInvalid": "Inserisci un URL del client valido"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Metodo di sblocco cassaforte",
|
||||
"introText": "Scegli come sbloccare la cassaforte. Puoi usare la tua password principale (sempre disponibile) o impostare un codice PIN per un accesso più veloce. Dopo 3 tentativi di PIN falliti, è necessario utilizzare la password principale.",
|
||||
"password": "Password Principale",
|
||||
"pin": "Codice PIN",
|
||||
"pinDescription": "Sblocca la cassaforte con codice PIN",
|
||||
"setupPin": "Imposta Codice Pin",
|
||||
"enterNewPinDescription": "Inserisci un codice PIN composto da almeno 6 cifre",
|
||||
"confirmPin": "Conferma PIN",
|
||||
"confirmPinDescription": "Inserisci di nuovo il tuo PIN per confermare",
|
||||
"invalidPinFormat": "Formato PIN non valido",
|
||||
"pinMismatch": "I PIN non corrispondono",
|
||||
"incorrectPin": "PIN errato. {{attemptsRemaining}} tentativi rimanenti.",
|
||||
"incorrectPinSingular": "Pin non corretto. 1 tentativo rimanente.",
|
||||
"enableSuccess": "Sblocco PIN abilitato con successo!",
|
||||
"pinLocked": "Il PIN sblocco è stato disabilitato. Utilizza la tua password principale per sbloccare la cassaforte.",
|
||||
"pinSecurityWarning": "Lo sblocco PIN nell'estensione del browser può essere meno sicuro della tua password principale, poichè i PIN hanno tipicamente entropia inferiore e possono essere brutalmente forzati se il dispositivo è compromesso. Usalo solo su dispositivi che ti fidi completamente."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Titolo",
|
||||
"titlePlaceholder": "Inserisci un nome per questa passkey",
|
||||
"createButton": "Crea Passkey",
|
||||
"creatingButton": "Creazione in corso...",
|
||||
"useBrowserPasskey": "Usa Browser Passkey",
|
||||
"selectPasskeyToReplace": "Selezionare una chiave di accesso da sostituire:",
|
||||
"createNewPasskey": "Crea Nuova Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Provider Passkey",
|
||||
"passkeyProviderOn": "Passkey Provider attivo",
|
||||
"enable": "Abilita AliasVault come provider di passkey",
|
||||
"description": "Quando abilitato, AliasVault gestirà le richieste di passkey dai siti web. Quando un sito web richiede una passkey, verrà mostrato il popup di AliasVault invece dell'interfaccia nativa del browser o dell'interfaccia di password del sistema operativo."
|
||||
"passkeyProviderOn": "Passkey Provider attivo"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "Novità",
|
||||
"whatsNewDescription": "È richiesto un aggiornamento per supportare le seguenti modifiche:",
|
||||
"noDescriptionAvailable": "Nessuna descrizione disponibile per questa versione.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparazione aggiornamento...",
|
||||
"vaultAlreadyUpToDate": "La cassaforte è già aggiornata",
|
||||
"startingDatabaseTransaction": "Avvio transazione database...",
|
||||
"applyingDatabaseMigrations": "Applicazione migrazioni database...",
|
||||
"applyingMigration": "Applicazione migrazione {{current}} di {{total}}...",
|
||||
"committingChanges": "Modifica in corso..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Errore",
|
||||
"unableToGetVersionInfo": "Impossibile ottenere informazioni sulla versione. Riprova.",
|
||||
"selfHostedServer": "Server Autospitato",
|
||||
"selfHostedWarning": "Se usi un server autospitato, assicurati di aggiornare anche la tua istanza, altrimenti l'accesso al client web smetterà di funzionare.",
|
||||
"cancel": "Annulla",
|
||||
"continueUpgrade": "Continua aggiornamento",
|
||||
"upgradeFailed": "Aggiornamento non riuscito",
|
||||
"failedToApplyMigration": "Impossibile eseguire la migrazione ({{current}} di {{total}})"
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
"authCode": "Authenticatiecode",
|
||||
"authCodePlaceholder": "Voer 6-cijferige code in",
|
||||
"verify": "Verifiëren",
|
||||
"cancel": "Annuleren",
|
||||
"twoFactorNote": "Opmerking: als je geen toegang hebt tot je authenticator, kunt je je 2FA resetten door met een in te loggen via de website.",
|
||||
"masterPassword": "Hoofdwachtwoord",
|
||||
"unlockVault": "Vault ontgrendelen",
|
||||
"unlockVault": "Ontgrendelen",
|
||||
"unlockWithPin": "Ontgrendelen met PIN",
|
||||
"enterPinToUnlock": "Voer je pincode in om je vault te ontgrendelen",
|
||||
"useMasterPassword": "Gebruik hoofdwachtwoord",
|
||||
"unlockTitle": "Ontgrendel je vault",
|
||||
"unlockDescription": "Voer je hoofdwachtwoord in om je vault te ontgrendelen.",
|
||||
"logout": "Uitloggen",
|
||||
"logoutConfirm": "Weet je zeker dat je wilt uitloggen?",
|
||||
"sessionExpired": "Je sessie is verlopen. Log opnieuw in.",
|
||||
"unlockSuccess": "Vault succesvol ontgrendeld!",
|
||||
"unlockSuccessTitle": "Je vault is succesvol ontgrendeld",
|
||||
"unlockSuccessDescription": "Je kunt nu automatisch invullen gebruiken in inlogformulieren in je browser.",
|
||||
"closePopup": "Sluit deze popup",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Verbinden met",
|
||||
"switchAccounts": "Wisselen van account?",
|
||||
"loggedIn": "Ingelogd",
|
||||
"loginWithMobile": "Log in via de mobiele app",
|
||||
"unlockWithMobile": "Ontgrendel met mobiele app",
|
||||
"scanQrCode": "Scan deze QR-code met de AliasVault mobiele app om in te loggen en je kluis te ontgrendelen.",
|
||||
"errors": {
|
||||
"invalidCode": "Voer een geldige 6-cijferige code in.",
|
||||
"serverError": "Kon de AliasVault server niet bereiken. Probeer het later opnieuw of neem contact op met support als het probleem aanhoudt.",
|
||||
"noToken": "Inloggen mislukt -- geen token ontvangen",
|
||||
"migrationError": "Er is een fout opgetreden bij het controleren op updates.",
|
||||
"wrongPassword": "Onjuist wachtwoord. Probeer het opnieuw.",
|
||||
"accountLocked": "Account tijdelijk vergrendeld vanwege te veel mislukte pogingen.",
|
||||
"networkError": "Netwerkfout. Controleer de verbinding en probeer het opnieuw.",
|
||||
"sessionExpired": "Je sessie is verlopen. Log opnieuw in."
|
||||
"sessionExpired": "Je sessie is verlopen. Log opnieuw in.",
|
||||
"mobileLoginRequestExpired": "Time-out van de mobiele inlogaanvraag. Herlaad de pagina en probeer opnieuw."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Instellingen"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Laden...",
|
||||
"notice": "Melding",
|
||||
"error": "Fout",
|
||||
"success": "Succes",
|
||||
"cancel": "Annuleren",
|
||||
"confirm": "Bevestigen",
|
||||
"back": "Terug",
|
||||
"next": "Volgende",
|
||||
"use": "Gebruik",
|
||||
"delete": "Verwijderen",
|
||||
"save": "Opslaan",
|
||||
"or": "Of",
|
||||
"close": "Sluiten",
|
||||
"copied": "Gekopieerd!",
|
||||
"openInNewWindow": "Openen in nieuw venster",
|
||||
"language": "Taal",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"showPassword": "Wachtwoord tonen",
|
||||
"hidePassword": "Wachtwoord verbergen",
|
||||
"showDetails": "Toon details",
|
||||
"hideDetails": "Verberg details",
|
||||
"copyToClipboard": "Naar klembord kopiëren",
|
||||
"loadingEmails": "E-mails laden...",
|
||||
"loadingTotpCodes": "TOTP-codes laden...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "Deze versie van de AliasVault browserextensie wordt niet meer ondersteund door de server. Update je browserextensie naar de nieuwste versie.",
|
||||
"browserExtensionOutdated": "Deze browserextensie is verouderd en kan niet worden gebruikt om toegang te krijgen tot deze vault. Update deze browserextensie om door te gaan.",
|
||||
"serverVersionNotSupported": "De AliasVault server moet worden bijgewerkt naar een nieuwere versie om deze browserextensie te kunnen gebruiken. Neem contact op met support als je hulp nodig hebt.",
|
||||
"serverVersionTooOld": "De AliasVault server moet bijgewerkt worden naar een nieuwere versie om deze functie te kunnen gebruiken. Neem contact op met support als je hulp nodig hebt.",
|
||||
"unknownError": "Er is een onbekende fout opgetreden",
|
||||
"unknownErrorTryAgain": "Er is een onbekende fout opgetreden. Probeer het opnieuw.",
|
||||
"vaultNotAvailable": "Vault niet beschikbaar",
|
||||
"failedToRetrieveData": "Gegevens ophalen mislukt",
|
||||
"vaultIsLocked": "Vault is vergrendeld",
|
||||
"failedToUploadVault": "Vault uploaden mislukt",
|
||||
"passwordChanged": "Je wachtwoord is veranderd sinds de laatste keer dat je bent ingelogd. Log opnieuw in."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Ongeldige herstelcode. Probeer het opnieuw.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Refresh token is vereist.",
|
||||
"INVALID_REFRESH_TOKEN": "Ongeldig refresh token.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Refresh token succesvol ingetrokken.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Registratie van nieuwe accounts is momenteel uitgeschakeld op deze server. Neem contact op met de beheerder.",
|
||||
"USERNAME_REQUIRED": "Gebruikersnaam is vereist.",
|
||||
"USERNAME_ALREADY_IN_USE": "Gebruikersnaam is al in gebruik.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "of",
|
||||
"new": "Nieuw",
|
||||
"cancel": "Annuleren",
|
||||
"search": "Zoeken",
|
||||
"vaultLocked": "AliasVault is vergrendeld.",
|
||||
"creatingNewAlias": "Nieuwe alias aanmaken...",
|
||||
"noMatchesFound": "Geen resultaten gevonden",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Genereer nieuw wachtwoord",
|
||||
"togglePasswordVisibility": "Schakel zichtbaarheid van wachtwoord in/uit",
|
||||
"passwordCopiedToClipboard": "Wachtwoord gekopieerd naar klembord",
|
||||
"enterEmailAndOrUsernameError": "Voer e-mail en/of gebruikersnaam in",
|
||||
"openAliasVaultToUpgrade": "Open AliasVault om te upgraden",
|
||||
"vaultUpgradeRequired": "Update is vereist.",
|
||||
"dismissPopup": "Pop-up sluiten"
|
||||
@@ -176,27 +177,12 @@
|
||||
"deleteCredential": "Credential verwijderen",
|
||||
"credentialDetails": "Credential details",
|
||||
"serviceName": "Naam",
|
||||
"serviceNamePlaceholder": "bijv. Gmail, Facebook, Bank",
|
||||
"website": "Website",
|
||||
"websitePlaceholder": "https://voorbeeld.nl",
|
||||
"username": "Gebruikersnaam",
|
||||
"usernamePlaceholder": "Voer gebruikersnaam in",
|
||||
"password": "Wachtwoord",
|
||||
"passwordPlaceholder": "Voer wachtwoord in",
|
||||
"generatePassword": "Wachtwoord genereren",
|
||||
"copyPassword": "Wachtwoord kopiëren",
|
||||
"showPassword": "Wachtwoord tonen",
|
||||
"hidePassword": "Wachtwoord verbergen",
|
||||
"notes": "Notities",
|
||||
"notesPlaceholder": "Aanvullende notities...",
|
||||
"totp": "Tweestapsverificatie",
|
||||
"totpCode": "TOTP-code",
|
||||
"copyTotp": "Kopiëren",
|
||||
"totpSecret": "TOTP secret",
|
||||
"totpSecretPlaceholder": "Voer TOTP secret in",
|
||||
"noCredentials": "Geen credentials gevonden",
|
||||
"noCredentialsDescription": "Voeg je eerste credentials toe om te beginnen",
|
||||
"searchPlaceholder": "Credentials zoeken...",
|
||||
"welcomeTitle": "Welkom bij AliasVault!",
|
||||
"welcomeDescription": "Om de AliasVault browser extensie te gebruiken: navigeer naar een website en gebruik de AliasVault autofill popup om nieuwe credentials aan te maken.",
|
||||
"noPasskeysFound": "Er zijn nog geen passkeys aangemaakt. Passkeys worden gemaakt door een website te bezoeken die passkeys als een authenticatiemethode biedt.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "Geen credentials gevonden",
|
||||
"createdAt": "Aangemaakt",
|
||||
"updatedAt": "Laatst bijgewerkt",
|
||||
"autofill": "Autofill",
|
||||
"fillForm": "Formulier invullen",
|
||||
"deleteConfirm": "Weet je zeker dat je deze credential wilt verwijderen?",
|
||||
"saveSuccess": "Credential succesvol opgeslagen",
|
||||
"tags": "Labels",
|
||||
"addTag": "Label toevoegen",
|
||||
"removeTag": "Label verwijderen",
|
||||
"folder": "Map",
|
||||
"selectFolder": "Map selecteren",
|
||||
"createFolder": "Map aanmaken",
|
||||
"saveCredential": "Credential opslaan",
|
||||
"deleteCredentialTitle": "Credential verwijderen",
|
||||
"deleteCredentialConfirm": "Weet je zeker dat je deze credential wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Voer volledig e-mailadres in",
|
||||
"enterEmailPrefix": "E-mailprefix invoeren"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "2FA-code toevoegen",
|
||||
"instructions": "Voer de secret key in die door de website wordt weergegeven waar je tweestapsverificatie wilt toevoegen.",
|
||||
"nameOptional": "Naam (optioneel)",
|
||||
"secretKey": "Secret key",
|
||||
"saveToViewCode": "Opslaan om code te bekijken",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Ongeldig formaat."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-mails",
|
||||
"deleteEmailTitle": "E-mail verwijderen",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Web-app openen",
|
||||
"loggedIn": "Ingelogd",
|
||||
"logout": "Uitloggen",
|
||||
"lock": "Vergrendelen",
|
||||
"globalSettings": "Globale Instellingen",
|
||||
"autofillPopup": "Autofill popup",
|
||||
"activeOnAllSites": "Actief voor alle sites (tenzij hieronder uitgeschakeld)",
|
||||
"disabledOnAllSites": "Uitgeschakeld op alle sites",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"rightClickContextMenu": "Rechtermuisknop menu",
|
||||
"autofillMatching": "Autofill matching",
|
||||
"autofillMatchingMode": "Autofill matching modus",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Snelkoppelingen",
|
||||
"configureKeyboardShortcuts": "Snelkoppelingen configureren",
|
||||
"configure": "Configureren",
|
||||
"security": "Beveiliging",
|
||||
"clipboardClearTimeout": "Automatisch klembord wissen na kopiëren",
|
||||
"clipboardClearTimeoutDescription": "Automatisch het klembord wissen na kopiëren van gevoelige gegevens",
|
||||
"clipboardClearDisabled": "Nooit wissen",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 uur",
|
||||
"autoLock24Hours": "24 uur",
|
||||
"versionPrefix": "Versie ",
|
||||
"preferences": "Voorkeuren",
|
||||
"autofillSettings": "Autofill instellingen",
|
||||
"clipboardSettings": "Klembord instellingen",
|
||||
"contextMenuSettings": "Context menu instellingen",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Autofill suggesties verschijnen op login formulieren",
|
||||
"autofillDisabledDescription": "Autofill suggesties zijn uitgeschakeld",
|
||||
"languageSettings": "Taal",
|
||||
"languageSettingsDescription": "Kies je voorkeurstaal",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL is vereist",
|
||||
"apiUrlInvalid": "Voer een geldige API URL in",
|
||||
"clientUrlRequired": "Client URL is vereist",
|
||||
"clientUrlInvalid": "Voer een geldige client URL in"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Vault ontgrendelmethode",
|
||||
"introText": "Kies hoe je je vault wilt ontgrendelen. Je kunt je hoofdwachtwoord gebruiken (altijd beschikbaar) of een pincode instellen voor snellere toegang. Na 3 mislukte PIN-pogingen moet je je hoofdwachtwoord gebruiken.",
|
||||
"password": "Hoofdwachtwoord",
|
||||
"pin": "Pincode",
|
||||
"pinDescription": "Ontgrendel vault met pincode",
|
||||
"setupPin": "Pincode instellen",
|
||||
"enterNewPinDescription": "Voer een pincode in van minimaal 6 cijfers",
|
||||
"confirmPin": "Bevestig pincode",
|
||||
"confirmPinDescription": "Voer je pincode nogmaals in om te bevestigen",
|
||||
"invalidPinFormat": "Ongeldige pincode",
|
||||
"pinMismatch": "Pincodes komen niet overeen",
|
||||
"incorrectPin": "Onjuiste PIN. {{attemptsRemaining}} pogingen resterend.",
|
||||
"incorrectPinSingular": "Onjuiste PIN. 1 poging resterend.",
|
||||
"enableSuccess": "Pincode succesvol ingeschakeld!",
|
||||
"pinLocked": "Pincode is uitgeschakeld. Gebruik je hoofdwachtwoord om je kluis te ontgrendelen.",
|
||||
"pinSecurityWarning": "Pincode ontgrendeling in de browserextensie kan minder veilig zijn dan je hoofdwachtwoord, omdat een pincode makkelijker kan worden gebruteforced als iemand toegang heeft tot je apparaat. Gebruik deze optie daarom alleen op apparaten die je volledig vertrouwt."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Titel",
|
||||
"titlePlaceholder": "Voer een naam in voor deze passkey",
|
||||
"createButton": "Passkey aanmaken",
|
||||
"creatingButton": "Aanmaken...",
|
||||
"useBrowserPasskey": "Gebruik browser passkey",
|
||||
"selectPasskeyToReplace": "Selecteer een passkey om te vervangen:",
|
||||
"createNewPasskey": "Passkey aanmaken",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey provider",
|
||||
"passkeyProviderOn": "Passkey provider ingeschakeld",
|
||||
"enable": "AliasVault als passkey provider inschakelen",
|
||||
"description": "Wanneer ingeschakeld, behandelt AliasVault passkey verzoeken van websites. Wanneer een website een passkey aanvraagt, wordt de AliasVault pop-up getoond in plaats van de browser of OS pop-up."
|
||||
"passkeyProviderOn": "Passkey provider ingeschakeld"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "Wat is er nieuw",
|
||||
"whatsNewDescription": "Een upgrade is vereist vanwege de volgende wijzigingen:",
|
||||
"noDescriptionAvailable": "Voor deze versie is geen beschrijving beschikbaar.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Upgrade voorbereiden...",
|
||||
"vaultAlreadyUpToDate": "De vault is al bijgewerkt",
|
||||
"startingDatabaseTransaction": "Starten van database transactie...",
|
||||
"applyingDatabaseMigrations": "Databasemigratie toepassen...",
|
||||
"applyingMigration": "Toepassen van migratie {{current}} van {{total}}...",
|
||||
"committingChanges": "Wijzigingen doorvoeren..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Fout",
|
||||
"unableToGetVersionInfo": "Kan versie-informatie niet ophalen. Probeer het opnieuw.",
|
||||
"selfHostedServer": "Self-hosted server",
|
||||
"selfHostedWarning": "Als je een self-hosted server gebruikt, zorg er dan voor dat je ook je eigen self-hosted instantie bijwerkt, omdat anders het inloggen via de web client niet meer zal werken.",
|
||||
"cancel": "Annuleren",
|
||||
"continueUpgrade": "Verdergaan",
|
||||
"upgradeFailed": "Upgrade mislukt",
|
||||
"failedToApplyMigration": "Kon migratie niet toepassen ({{current}} van {{total}})"
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
"authCode": "Kod uwierzytelniania",
|
||||
"authCodePlaceholder": "Wpisz 6-cyfrowy kod",
|
||||
"verify": "Potwierdź",
|
||||
"cancel": "Anuluj",
|
||||
"twoFactorNote": "Uwaga: Jeśli nie masz dostępu do swojego urządzenia uwierzytelniającego, możesz zresetować swój kod 2FA za pomocą kodu odzyskiwania, logując się za pośrednictwem strony internetowej.",
|
||||
"masterPassword": "Hasło główne",
|
||||
"unlockVault": "Odblokuj sejf",
|
||||
"unlockVault": "Odblokuj",
|
||||
"unlockWithPin": "Odblokuj za pomocą kodu PIN",
|
||||
"enterPinToUnlock": "Wprowadź swój kod PIN, aby odblokować sejf",
|
||||
"useMasterPassword": "Użyj hasła głównego",
|
||||
"unlockTitle": "Odblokuj swój sejf",
|
||||
"unlockDescription": "Wprowadź hasło główne, aby odblokować sejf.",
|
||||
"logout": "Wyloguj się",
|
||||
"logoutConfirm": "Czy na pewno chcesz się wylogować?",
|
||||
"sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.",
|
||||
"unlockSuccess": "Sejf odblokowany pomyślnie!",
|
||||
"unlockSuccessTitle": "Twój sejf został pomyślnie odblokowany.",
|
||||
"unlockSuccessDescription": "Możesz teraz używać automatycznego uzupełniania w formularzach logowania w przeglądarce.",
|
||||
"closePopup": "Zamknij to okno.",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Łączenie z",
|
||||
"switchAccounts": "Przełącz konto",
|
||||
"loggedIn": "Zalogowano",
|
||||
"loginWithMobile": "Zaloguj się za pomocą aplikacji mobilnej",
|
||||
"unlockWithMobile": "Odblokuj za pomocą aplikacji mobilnej",
|
||||
"scanQrCode": "Zeskanuj ten kod QR za pomocą aplikacji mobilnej AliasVault, aby się zalogować i odblokować sejf.",
|
||||
"errors": {
|
||||
"invalidCode": "Wprowadź prawidłowy 6-cyfrowy kod uwierzytelniający.",
|
||||
"serverError": "Nie można połączyć się z serwerem AliasVault. Spróbuj ponownie później lub skontaktuj się z pomocą techniczną, jeśli problem będzie się powtarzał.",
|
||||
"noToken": "Logowanie nie powiodło się — nie zwrócono tokena.",
|
||||
"migrationError": "Wystąpił błąd podczas sprawdzania oczekujących migracji.",
|
||||
"wrongPassword": "Hasło jest nieprawidłowe. Spróbuj ponownie.",
|
||||
"accountLocked": "Konto tymczasowo zostało zablokowane z powodu zbyt wielu nieudanych prób.",
|
||||
"networkError": "Błąd sieci. Sprawdź swoje połączenie i spróbuj ponownie.",
|
||||
"sessionExpired": "Twoja sesja wygasła. Prosimy o zalogowanie się ponownie."
|
||||
"sessionExpired": "Twoja sesja wygasła. Prosimy o zalogowanie się ponownie.",
|
||||
"mobileLoginRequestExpired": "Limit czasu logowania upłynął. Proszę odświeżyć stronę i spróbować ponownie."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Ustawienia"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Wczytywanie…",
|
||||
"notice": "Uwaga",
|
||||
"error": "Błąd",
|
||||
"success": "Gotowe",
|
||||
"cancel": "Anuluj",
|
||||
"confirm": "Potwierdź",
|
||||
"back": "Powrót",
|
||||
"next": "Dalej",
|
||||
"use": "Użyj",
|
||||
"delete": "Usuń",
|
||||
"save": "Zapisz",
|
||||
"or": "lub",
|
||||
"close": "Zamknąć",
|
||||
"copied": "Skopiowano",
|
||||
"openInNewWindow": "Otwórz w nowym oknie.",
|
||||
"language": "Język",
|
||||
"enabled": "Aktywne",
|
||||
"disabled": "Nie aktywne",
|
||||
"showPassword": "Pokaż hasło",
|
||||
"hidePassword": "Ukryj hasło",
|
||||
"showDetails": "Pokaż szczegóły",
|
||||
"hideDetails": "Ukryj szczegóły",
|
||||
"copyToClipboard": "Skopiuj do schowka",
|
||||
"loadingEmails": "Ładowanie wiadomości e-mail...",
|
||||
"loadingTotpCodes": "Ładowanie kodów TOTP...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "Ta wersja rozszerzenia przeglądarki AliasVault nie jest już obsługiwana przez serwer. Zaktualizuj rozszerzenie przeglądarki do najnowszej wersji.",
|
||||
"browserExtensionOutdated": "To rozszerzenie przeglądarki jest nieaktualne i nie można go używać do uzyskania dostępu do tego sejfu. Aby kontynuować, zaktualizuj to rozszerzenie przeglądarki.",
|
||||
"serverVersionNotSupported": "Aby korzystać z tego rozszerzenia przeglądarki, należy zaktualizować serwer AliasVault do nowszej wersji. Jeśli potrzebujesz pomocy, skontaktuj się z działem pomocy technicznej.",
|
||||
"serverVersionTooOld": "Aby korzystać z tej funkcji, serwer AliasVault musi zostać zaktualizowany do nowszej wersji. Jeśli potrzebujesz pomocy, skontaktuj się z administratorem serwera.",
|
||||
"unknownError": "Wystąpił nieznany błąd",
|
||||
"unknownErrorTryAgain": "Wystąpił nieznany błąd. Spróbuj ponownie.",
|
||||
"vaultNotAvailable": "Sejf niedostępny",
|
||||
"failedToRetrieveData": "Nie udało się pobrać danych",
|
||||
"vaultIsLocked": "Sejf jest zablokowany",
|
||||
"failedToUploadVault": "Nie udało się załadować sejfu",
|
||||
"passwordChanged": "Twoje hasło uległo zmianie od czasu ostatniego logowania. Ze względów bezpieczeństwa prosimy o ponowne zalogowanie się."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Nieprawidłowy kod odzyskiwania. Spróbuj ponownie.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Wymagany jest token odświeżania.",
|
||||
"INVALID_REFRESH_TOKEN": "Nieprawidłowy token odświeżania.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Odnowienie tokenu zakończyło się powodzeniem.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Rejestracja nowych kont jest obecnie wyłączona na tym serwerze. Skontaktuj się z administratorem.",
|
||||
"USERNAME_REQUIRED": "Wymagane jest podanie nazwy użytkownika.",
|
||||
"USERNAME_ALREADY_IN_USE": "Nazwa użytkownika jest już używana.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "lub",
|
||||
"new": "Nowy",
|
||||
"cancel": "Anulować",
|
||||
"search": "Wyszukiwanie",
|
||||
"vaultLocked": "AliasVault jest zablokowany.",
|
||||
"creatingNewAlias": "Tworzenie nowego aliasu...",
|
||||
"noMatchesFound": "Nie znaleziono żadnych wyników",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Wygeneruj nowe hasło",
|
||||
"togglePasswordVisibility": "Przełącz widoczność hasła",
|
||||
"passwordCopiedToClipboard": "Hasło skopiowane do schowka",
|
||||
"enterEmailAndOrUsernameError": "Wprowadź adres e-mail i/lub nazwę użytkownika",
|
||||
"openAliasVaultToUpgrade": "Otwórz AliasVault, aby dokonać aktualizacji",
|
||||
"vaultUpgradeRequired": "Wymagana aktualizacja sejfu.",
|
||||
"dismissPopup": "Zamknij wyskakujące okienko"
|
||||
@@ -176,27 +177,12 @@
|
||||
"deleteCredential": "Usuń dane logowania",
|
||||
"credentialDetails": "Dane uwierzytelniające",
|
||||
"serviceName": "Nazwa usługi",
|
||||
"serviceNamePlaceholder": "np. Gmail, Facebook, Bank",
|
||||
"website": "Strona internetowa",
|
||||
"websitePlaceholder": "https://adresstronywww.com",
|
||||
"username": "Nazwa użytkownika",
|
||||
"usernamePlaceholder": "Wprowadź nazwę użytkownika",
|
||||
"password": "Hasło",
|
||||
"passwordPlaceholder": "Wprowadź hasło",
|
||||
"generatePassword": "Utwórz hasło",
|
||||
"copyPassword": "Skopiuj hasło",
|
||||
"showPassword": "Pokaż hasło",
|
||||
"hidePassword": "Ukryj hasło",
|
||||
"notes": "Notatki",
|
||||
"notesPlaceholder": "Dodatkowe informacje...",
|
||||
"totp": "Weryfikacja dwuetapowa (2FA)",
|
||||
"totpCode": "Kod TOTP",
|
||||
"copyTotp": "Skopiuj kod TOTP",
|
||||
"totpSecret": "Tajny klucz TOTP",
|
||||
"totpSecretPlaceholder": "Wprowadź tajny klucz TOTP",
|
||||
"noCredentials": "Nie znaleziono zapisanych danych uwierzytelniających",
|
||||
"noCredentialsDescription": "Dodaj pierwsze dane, aby rozpocząć",
|
||||
"searchPlaceholder": "Wyszukaj dane uwierzytelniające...",
|
||||
"welcomeTitle": "Witamy w AliasVault!",
|
||||
"welcomeDescription": "Aby skorzystać z rozszerzenia przeglądarki AliasVault - przejdź do strony internetowej i użyj okienka autozupelniania, aby utworzyć nowa tożsamość.",
|
||||
"noPasskeysFound": "Nie utworzono jeszcze żadnych kluczy dostępu. Klucze dostępu tworzy się, odwiedzając stronę internetową, która oferuje klucze dostępu jako metodę uwierzytelniania.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "Nie znaleziono pasujących danych uwierzytelniających",
|
||||
"createdAt": "Utworzono",
|
||||
"updatedAt": "Ostatnia aktualizacja",
|
||||
"autofill": "Autouzupełnianie",
|
||||
"fillForm": "Wypełnij formularz",
|
||||
"deleteConfirm": "Czy na pewno chcesz usunąć te dane uwierzytelniające?",
|
||||
"saveSuccess": "Dane logowania zostały zapisane",
|
||||
"tags": "Znaczniki",
|
||||
"addTag": "Dodaj znacznik",
|
||||
"removeTag": "Usuń znacznik",
|
||||
"folder": "Katalog",
|
||||
"selectFolder": "Wybierz folder",
|
||||
"createFolder": "Utwórz folder",
|
||||
"saveCredential": "Zapisz dane logowania",
|
||||
"deleteCredentialTitle": "Usuń dane logowania",
|
||||
"deleteCredentialConfirm": "Czy na pewno chcesz usunąć te dane logowania? Tej akcji nie można cofnąć.",
|
||||
@@ -262,12 +238,22 @@
|
||||
"publicEmailDescription": "Anonimowa, ale ograniczona prywatność. Treści e-mail są czytelne dla każdego, kto zna adres.",
|
||||
"useDomainChooser": "Użyj wybierania domen",
|
||||
"enterCustomDomain": "Wprowadź własną domenę",
|
||||
"enterFullEmail": "Wprowadź pełny adres e-mail",
|
||||
"enterFullEmail": "Wprowadź adres e-mail",
|
||||
"enterEmailPrefix": "Wprowadź prefiks e-mail"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Dodaj kod 2FA",
|
||||
"instructions": "Wprowadź tajny klucz wyświetlony na stronie internetowej, na której chcesz dodać uwierzytelnianie dwuskładnikowe.",
|
||||
"nameOptional": "Nazwa (opcjonalnie)",
|
||||
"secretKey": "Tajny klucz",
|
||||
"saveToViewCode": "Zapisz, aby wyświetlić kod",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Nieprawidłowy format tajnego klucza."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Skrzynka odbiorcza",
|
||||
"deleteEmailTitle": "Usuń adres e-mail",
|
||||
"deleteEmailTitle": "Usuń e-mail",
|
||||
"deleteEmailConfirm": "Czy na pewno chcesz trwale usunąć ten e-mail?",
|
||||
"from": "Od",
|
||||
"to": "Do",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Otwórz aplikację internetową",
|
||||
"loggedIn": "Zalogowano",
|
||||
"logout": "Wyloguj się",
|
||||
"lock": "Zablokuj",
|
||||
"globalSettings": "Ustawienia ogólne",
|
||||
"autofillPopup": "Okno autouzupełniania",
|
||||
"activeOnAllSites": "Aktywne we wszystkich witrynach (chyba że jest wyłączone)",
|
||||
"disabledOnAllSites": "Wyłączone na wszystkich witrynach",
|
||||
"enabled": "Włączone",
|
||||
"disabled": "Wyłączone",
|
||||
"rightClickContextMenu": "Aktywacja menu kontekstowego",
|
||||
"autofillMatching": "Dopasowanie autouzupełniania",
|
||||
"autofillMatchingMode": "Tryb dopasowania autouzupełniania",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Skróty klawiaturowe",
|
||||
"configureKeyboardShortcuts": "Skonfiguruj skróty klawiaturowe",
|
||||
"configure": "Konfiguracja",
|
||||
"security": "Bezpieczeństwo",
|
||||
"clipboardClearTimeout": "Wyczyść schowek po skopiowaniu",
|
||||
"clipboardClearTimeoutDescription": "Automatycznie wyczyść schowek po skopiowaniu poufnych danych",
|
||||
"clipboardClearDisabled": "Nigdy nie czyść",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 godzin",
|
||||
"autoLock24Hours": "24 godziny",
|
||||
"versionPrefix": "Wersja ",
|
||||
"preferences": "Ustawienia",
|
||||
"autofillSettings": "Ustawienia autouzupełniania",
|
||||
"clipboardSettings": "Ustawienia schowka",
|
||||
"contextMenuSettings": "Ustawienia menu kontekstowego",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Sugestie autouzupełniania pojawią się na formularzach logowania",
|
||||
"autofillDisabledDescription": "Podpowiedzi wyłączone we wszystkich polach",
|
||||
"languageSettings": "Język",
|
||||
"languageSettingsDescription": "Wybierz preferowany język interfejsu",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL jest wymagane",
|
||||
"apiUrlInvalid": "Wprowadź poprawny adres URL API",
|
||||
"clientUrlRequired": "Adres URL klienta jest wymagany",
|
||||
"clientUrlInvalid": "Wprowadź prawidłowy adres URL klienta"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Metoda odblokowania skarbca",
|
||||
"introText": "Wybierz sposób odblokowania sejfu. Możesz użyć hasła głównego (zawsze dostępnego) lub skonfigurować kod PIN, aby uzyskać szybszy dostęp. Po 3 nieudanych próbach wprowadzenia kodu PIN konieczne będzie użycie hasła głównego.",
|
||||
"password": "Hasło główne",
|
||||
"pin": "Kod PIN",
|
||||
"pinDescription": "Odblokuj sejf za pomocą kodu PIN",
|
||||
"setupPin": "Ustaw kod PIN",
|
||||
"enterNewPinDescription": "Wprowadź kod PIN składający się z co najmniej 6 cyfr",
|
||||
"confirmPin": "Potwierdź kod PIN",
|
||||
"confirmPinDescription": "Wprowadź ponownie swój kod PIN, aby potwierdzić",
|
||||
"invalidPinFormat": "Nieprawidłowy format kodu PIN",
|
||||
"pinMismatch": "Kody PIN nie są zgodne",
|
||||
"incorrectPin": "Nieprawidłowy kod PIN. Pozostało {{attemptsRemaining}} prób.",
|
||||
"incorrectPinSingular": "Nieprawidłowy kod PIN. Pozostała 1 próba.",
|
||||
"enableSuccess": "Odblokowanie za pomocą kodu PIN zostało aktywowane!",
|
||||
"pinLocked": "Odblokowanie za pomocą kodu PIN zostało wyłączone. Aby odblokować sejf, użyj hasła głównego.",
|
||||
"pinSecurityWarning": "Odblokowanie za pomocą kodu PIN w rozszerzeniu przeglądarki może być mniej bezpieczne niż hasło główne, ponieważ kody PIN mają zazwyczaj niższą entropię i mogą zostać złamane metodą Brute-Force, jeśli urządzenie zostanie naruszone. Używaj tej funkcji tylko na urządzeniach, którym w pełni ufasz."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Tytuł",
|
||||
"titlePlaceholder": "Wprowadź nazwę dla tego klucza dostępu",
|
||||
"createButton": "Utwórz klucz dostępu",
|
||||
"creatingButton": "Tworzenie...",
|
||||
"useBrowserPasskey": "Użyj klucza dostępu przeglądarki",
|
||||
"selectPasskeyToReplace": "Wybierz klucz dostępu, który chcesz zastąpić:",
|
||||
"createNewPasskey": "Utwórz nowy klucz dostępu",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Dostawca klucza dostępu",
|
||||
"passkeyProviderOn": "Dostawca klucza dostępu włączony ",
|
||||
"enable": "Włącz AliasVault jako dostawcę kluczy dostępu",
|
||||
"description": "Po włączeniu AliasVault będzie obsługiwać żądania kluczy dostępu od stron internetowych. Gdy strona internetowa zażąda klucza dostępu, zamiast natywnego interfejsu przeglądarki lub systemu operacyjnego wyświetli się okienko AliasVault."
|
||||
"passkeyProviderOn": "Dostawca klucza dostępu włączony "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "Co nowego",
|
||||
"whatsNewDescription": "Uaktualnienie jest wymagane do obsługi następujących zmian:",
|
||||
"noDescriptionAvailable": "Brak dostępnego opisu dla tej wersji.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Przygotowywanie aktualizacji...",
|
||||
"vaultAlreadyUpToDate": "Sejf jest już aktualny",
|
||||
"startingDatabaseTransaction": "Rozpoczynanie transakcji w bazie danych...",
|
||||
"applyingDatabaseMigrations": "Stosowanie migracji do bazy danych...",
|
||||
"applyingMigration": "Aplikacja migracji {{current}} z {{total}}...",
|
||||
"committingChanges": "Wprowadzanie zmian..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Błąd",
|
||||
"unableToGetVersionInfo": "Nie można uzyskać informacji o wersji. Spróbuj ponownie.",
|
||||
"selfHostedServer": "Serwer własny",
|
||||
"selfHostedWarning": "Jeśli używasz własnego serwera, upewnij się, że zaktualizujesz swoją samodzielną instancję, ponieważ w przeciwnym razie logowanie się do klienta internetowego przestanie działać.",
|
||||
"cancel": "Anuluj",
|
||||
"continueUpgrade": "Kontynuuj aktualizację",
|
||||
"upgradeFailed": "Aktualizacja nie powiodła się",
|
||||
"failedToApplyMigration": "Nie udało się zastosować migracji ({{current}} of {{total}})"
|
||||
|
||||
@@ -6,23 +6,22 @@
|
||||
"password": "Senha",
|
||||
"passwordPlaceholder": "Digite sua senha",
|
||||
"rememberMe": "Lembrar-me",
|
||||
"loginButton": "Login",
|
||||
"loginButton": "Entrar",
|
||||
"noAccount": "Não possui conta?",
|
||||
"createVault": "Criar novo cofre",
|
||||
"twoFactorTitle": "Por favor, insira o código de autenticação do seu aplicativo de autenticação.",
|
||||
"authCode": "Código de Autenticação",
|
||||
"authCodePlaceholder": "Digite o código de 6 dígitos",
|
||||
"verify": "Verificar",
|
||||
"cancel": "Cancelar",
|
||||
"twoFactorNote": "Nota: se você não tem acesso ao seu aparelho de verificação, você pode resetar seu 2FA com um código de recuperação fazendo login no site.",
|
||||
"masterPassword": "Senha Mestre",
|
||||
"unlockVault": "Desbloquear cofre",
|
||||
"unlockVault": "Desbloquear",
|
||||
"unlockWithPin": "Desbloquear com PIN",
|
||||
"enterPinToUnlock": "Digite seu PIN para desbloquear seu cofre",
|
||||
"useMasterPassword": "Utilizar Senha Mestre",
|
||||
"unlockTitle": "Desbloquear Seu Cofre",
|
||||
"unlockDescription": "Digite sua senha para desbloquear o cofre.",
|
||||
"logout": "Sair",
|
||||
"logoutConfirm": "Tem certeza que deseja sair?",
|
||||
"sessionExpired": "Sua sessão expirou. Por favor, faça login novamente.",
|
||||
"unlockSuccess": "Cofre sincronizado com sucesso!",
|
||||
"unlockSuccessTitle": "O seu cofre foi desbloqueado com sucesso",
|
||||
"unlockSuccessDescription": "Agora você pode utilizar o preenchimento automático nos formulários de login no seu navegador.",
|
||||
"closePopup": "Fechar este pop-up",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Conectando à",
|
||||
"switchAccounts": "Mudar de conta?",
|
||||
"loggedIn": "Logado",
|
||||
"loginWithMobile": "Entrar utilizado Dispositivo Móvel",
|
||||
"unlockWithMobile": "Desbloquear utilizando Dispositivo Móvel",
|
||||
"scanQrCode": "Escaneie este código QR com seu aplicativo AliasVault para dispositivo móvel para entrar e desbloquear seu cofre.",
|
||||
"errors": {
|
||||
"invalidCode": "Por favor digite o código de autenticação de 6 dígitos.",
|
||||
"serverError": "Não foi possível conectar ao servidor do AliasVault. Por favor tente novamente mais tarde ou entre em contato com o suporte caso o problema persista.",
|
||||
"noToken": "Login falhou -- nenhum token retornado",
|
||||
"migrationError": "Ocorreu um erro durante a verificação de migrações pendentes.",
|
||||
"wrongPassword": "Senha incorreta. Por favor tente novamente.",
|
||||
"accountLocked": "Conta temporariamente bloqueada por muitas tentativas de login falhas. Por favor, tente novamente mais tarde.",
|
||||
"networkError": "Conexão falhou. Por favor verifique sua conexão com a internet e tente novamente.",
|
||||
"sessionExpired": "Sua sessão expirou. Por favor, faça login novamente."
|
||||
"sessionExpired": "Sua sessão expirou. Por favor, faça login novamente.",
|
||||
"mobileLoginRequestExpired": "Requisição de login pelo dispositivo móvel demorou muito. Por favor, atualize a página e tente novamente."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Configurações"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Carregando...",
|
||||
"notice": "Aviso",
|
||||
"error": "Erro",
|
||||
"success": "Sucesso",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"back": "Voltar",
|
||||
"next": "Próximo",
|
||||
"use": "Utilizar",
|
||||
"delete": "Excluir",
|
||||
"save": "Salvar",
|
||||
"or": "Ou",
|
||||
"close": "Fechar",
|
||||
"copied": "Copiado!",
|
||||
"openInNewWindow": "Abrir em uma nova janela",
|
||||
"language": "Idioma",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Desabilitado",
|
||||
"showPassword": "Mostrar senha",
|
||||
"hidePassword": "Ocultar senha",
|
||||
"showDetails": "Exibir detalhes",
|
||||
"hideDetails": "Esconder detalhes",
|
||||
"copyToClipboard": "Copiar para a área de transferência",
|
||||
"loadingEmails": "Carregando e-mails...",
|
||||
"loadingTotpCodes": "Carregando códigos TOTP...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "Esta versão da extensão AliasVault para o navegador não é mais suportada pelo servidor. Por favor, atualize sua extensão para a última versão.",
|
||||
"browserExtensionOutdated": "Esta extensão do navegador está desatualizada e não pode ser utilizada para acessar este cofre. Por favor, atualize esta extensão para continuar.",
|
||||
"serverVersionNotSupported": "O servidor AliasVault precisa ser atualizado para uma nova versão para poder utilizar esta extensão de navegador. Por favor, entre em contato com o suporte caso precise de ajuda.",
|
||||
"serverVersionTooOld": "O servidor do AliasVault deve ser atualizado para uma versão mais recente para utilizar esta função. Por favor, entre em contato com o administrador do servidor se precisar de ajuda.",
|
||||
"unknownError": "Ocorreu um erro desconhecido",
|
||||
"unknownErrorTryAgain": "Ocorreu um erro inesperado. Por favor, tente novamente.",
|
||||
"vaultNotAvailable": "Cofre não disponível",
|
||||
"failedToRetrieveData": "Falha ao recuperar dados",
|
||||
"vaultIsLocked": "O cofre está bloqueado",
|
||||
"failedToUploadVault": "Falha ao fazer upload do cofre",
|
||||
"passwordChanged": "Sua senha mudou desde o último login. Por favor, realize login novamente por questões de segurança."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Código de recuperação inválido. Por favor, tente novamente.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Token de atualização é obrigatório.",
|
||||
"INVALID_REFRESH_TOKEN": "Token de atualização inválido.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Token de atualização revogado com sucesso.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Registro de novas contas está atualmente desabilitado neste servidor. Por favor entre em contato com o administrador.",
|
||||
"USERNAME_REQUIRED": "Nome de usuário é obrigatório.",
|
||||
"USERNAME_ALREADY_IN_USE": "Nome de usuário já está em uso.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "ou",
|
||||
"new": "Novo",
|
||||
"cancel": "Cancelar",
|
||||
"search": "Pesquisar",
|
||||
"vaultLocked": "AliasVault está bloqueado.",
|
||||
"creatingNewAlias": "Criando novo alias...",
|
||||
"noMatchesFound": "Nenhum resultado encontrado",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Gerar nova senha",
|
||||
"togglePasswordVisibility": "Alternar visibilidade da senha",
|
||||
"passwordCopiedToClipboard": "Senha copiada para a área de transferência",
|
||||
"enterEmailAndOrUsernameError": "Digite o e-mail e/ou nome de usuário",
|
||||
"openAliasVaultToUpgrade": "Abra o AliasVault para atualizar",
|
||||
"vaultUpgradeRequired": "Atualização de cofre necessária.",
|
||||
"dismissPopup": "Ignorar pop-up"
|
||||
@@ -176,27 +177,12 @@
|
||||
"deleteCredential": "Excluir Credencial",
|
||||
"credentialDetails": "Detalhes da Credencial",
|
||||
"serviceName": "Nome do Serviço",
|
||||
"serviceNamePlaceholder": "ex: G-mail, Facebook, Banco",
|
||||
"website": "Site",
|
||||
"websitePlaceholder": "https://exemplo.com",
|
||||
"username": "Nome de Usuário",
|
||||
"usernamePlaceholder": "Digite o nome de usuário",
|
||||
"password": "Senha",
|
||||
"passwordPlaceholder": "Digite a senha",
|
||||
"generatePassword": "Gerar Senha",
|
||||
"copyPassword": "Copiar Senha",
|
||||
"showPassword": "Mostrar Senha",
|
||||
"hidePassword": "Ocultar Senha",
|
||||
"notes": "Notas",
|
||||
"notesPlaceholder": "Notas adicionais...",
|
||||
"totp": "Autenticação de Dois Fatores",
|
||||
"totpCode": "Código TOTP",
|
||||
"copyTotp": "Copiar TOTP",
|
||||
"totpSecret": "Segredo TOTP",
|
||||
"totpSecretPlaceholder": "Digite a chave secreta TOTP",
|
||||
"noCredentials": "Nenhuma credencial encontrada",
|
||||
"noCredentialsDescription": "Adicione sua primeira credencial para começar",
|
||||
"searchPlaceholder": "Pesquisar credenciais...",
|
||||
"welcomeTitle": "Boas-vindas ao AliasVault!",
|
||||
"welcomeDescription": "Para utilizar a extensão de navegador do AliasVault: navegue para um site e utilize o pop-up de preenchimento automático do AliasVault para criar uma nova credencial.",
|
||||
"noPasskeysFound": "Nenhuma passkey foi criada ainda. Passkeys são veiadas visitando um website que ofereça passkey como método de autenticação.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "Nenhuma credencial foi encontrada",
|
||||
"createdAt": "Criado",
|
||||
"updatedAt": "Última atualização há",
|
||||
"autofill": "Preenchimento Automático",
|
||||
"fillForm": "Preencher Formulário",
|
||||
"deleteConfirm": "Tem certeza que deseja excluir esta credencial?",
|
||||
"saveSuccess": "Credencial salva com sucesso",
|
||||
"tags": "Tags",
|
||||
"addTag": "Adicionar Tag",
|
||||
"removeTag": "Remover Tag",
|
||||
"folder": "Pasta",
|
||||
"selectFolder": "Selecionar Pasta",
|
||||
"createFolder": "Criar Pasta",
|
||||
"saveCredential": "Salvar credencial",
|
||||
"deleteCredentialTitle": "Excluir Credencial",
|
||||
"deleteCredentialConfirm": "Tem certeza que deseja excluir esta credencial? Essa operação não pode ser desfeita.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Digite o endereço de e-mail completo",
|
||||
"enterEmailPrefix": "Digite o prefixo do e-mail"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Adicionar Código de Autenticação de Dois Fatores",
|
||||
"instructions": "Digite a chave secreta exibida pelo website que você quer adicionar a autenticação de dois fatores.",
|
||||
"nameOptional": "Nome (opcional)",
|
||||
"secretKey": "Chave Secreta",
|
||||
"saveToViewCode": "Salvar para ver código",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Formato de chave secreta inválido."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "E-mails",
|
||||
"deleteEmailTitle": "Excluir E-mail",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Abrir aplicativo web",
|
||||
"loggedIn": "Logado",
|
||||
"logout": "Sair",
|
||||
"lock": "Bloquear",
|
||||
"globalSettings": "Configurações Gerais",
|
||||
"autofillPopup": "Pop-up de preenchimento automático",
|
||||
"activeOnAllSites": "Ativado em todos os sites (a menos que esteja desabilitado abaixo)",
|
||||
"disabledOnAllSites": "Desativado em todos os sites",
|
||||
"enabled": "Habilitado",
|
||||
"disabled": "Desabilitado",
|
||||
"rightClickContextMenu": "Menu do botão direito",
|
||||
"autofillMatching": "Correspondência de Preenchimento Automático",
|
||||
"autofillMatchingMode": "Modo de correspondência de preenchimento automático",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Atalhos do Teclado",
|
||||
"configureKeyboardShortcuts": "Configurar atalhos do teclado",
|
||||
"configure": "Configurar",
|
||||
"security": "Segurança",
|
||||
"clipboardClearTimeout": "Limpar área de transferência após copiar",
|
||||
"clipboardClearTimeoutDescription": "Limpar automaticamente a área de transferência após copiar dados sensíveis",
|
||||
"clipboardClearDisabled": "Nunca limpar",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 horas",
|
||||
"autoLock24Hours": "24 horas",
|
||||
"versionPrefix": "Versão ",
|
||||
"preferences": "Preferências",
|
||||
"autofillSettings": "Configurações de Preenchimento Automático",
|
||||
"clipboardSettings": "Configurações da Área de Transferência",
|
||||
"contextMenuSettings": "Configurações do Menu de Contexto",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Sugestões de autopreenchimento aparecerão em formulários de login",
|
||||
"autofillDisabledDescription": "Sugestões de autopreenchimento estão desabilitadas globalmente",
|
||||
"languageSettings": "Idioma",
|
||||
"languageSettingsDescription": "Selecione seu idioma preferido",
|
||||
"validation": {
|
||||
"apiUrlRequired": "URL de API é obrigatório",
|
||||
"apiUrlInvalid": "Por favor, digite um URL de API válido",
|
||||
"clientUrlRequired": "URL de Cliente é obrigatório",
|
||||
"clientUrlInvalid": "Por favor, digite um URL de cliente válido"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Método de Desbloqueio do Cofre",
|
||||
"introText": "Escolha como você quer desbloquear seu cofre. Você pode utilizar sua senha mestre (sempre disponível), ou configurar um PIN para facilitar o acesso. Após 3 tentativas incorretas de PIN, você deverá utilizar sua senha mestre.",
|
||||
"password": "Senha Mestre",
|
||||
"pin": "Código PIN",
|
||||
"pinDescription": "Desbloquear cofre com código PIN",
|
||||
"setupPin": "Configurar Código PIN",
|
||||
"enterNewPinDescription": "Digite um PIN que consista de, pelo menos, 6 dígitos",
|
||||
"confirmPin": "Confirmar PIN",
|
||||
"confirmPinDescription": "Digite seu PIN novamente para confirmar",
|
||||
"invalidPinFormat": "Formato de PIN inválido",
|
||||
"pinMismatch": "PINs não correspondem",
|
||||
"incorrectPin": "PIN incorreto. {{attemptsRemaining}} tentativas restantes.",
|
||||
"incorrectPinSingular": "PIN incorreto. 1 tentativa restante.",
|
||||
"enableSuccess": "Desbloqueio por PIN habilitado com sucesso!",
|
||||
"pinLocked": "Desbloqueio por PIN foi desabilitado. Por favor, utilize sua senha mestre para desbloquear o cofre.",
|
||||
"pinSecurityWarning": "Desbloqueio por PIN na extensão de navegador pode ser menos segura que sua senha mestre, uma vez que PINs geralmente tem uma entropia menor e podem ser vulneráveis à tentativas forçadas caso seu dispositivo esteja comprometido. Utilize apenas em dispositivos que tenha total confiança."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Título",
|
||||
"titlePlaceholder": "Digite um nome para esta passkey",
|
||||
"createButton": "Criar Passkey",
|
||||
"creatingButton": "Criando...",
|
||||
"useBrowserPasskey": "Utilizar Passkey do Navegador",
|
||||
"selectPasskeyToReplace": "Selecione uma passkey para alterar:",
|
||||
"createNewPasskey": "Criar Nova Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Provedor de Passkey",
|
||||
"passkeyProviderOn": "Provedor de Passkey em ",
|
||||
"enable": "Habilitar AliasVault como provedor de passkey",
|
||||
"description": "Quando habilitado, o AliasVault cuidará de solicitações de passkey de websites. Quando um website solicita uma passkey, o AliasVault mostrará um popup ao invés da interface de passkey padrão do navegador ou do OS."
|
||||
"passkeyProviderOn": "Provedor de Passkey em "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "O Que Há de Novo",
|
||||
"whatsNewDescription": "Uma atualização é necessária para utilizar as seguintes mudanças:",
|
||||
"noDescriptionAvailable": "Nenhuma descrição disponível para esta versão.",
|
||||
"okay": "Ok",
|
||||
"status": {
|
||||
"preparingUpgrade": "Preparando atualização...",
|
||||
"vaultAlreadyUpToDate": "Cofre já está atualizado",
|
||||
"startingDatabaseTransaction": "Iniciando transação no banco de dados...",
|
||||
"applyingDatabaseMigrations": "Aplicando migrações do banco de dados...",
|
||||
"applyingMigration": "Aplicando migração {{current}} de {{total}}...",
|
||||
"committingChanges": "Confirmando mudanças..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Erro",
|
||||
"unableToGetVersionInfo": "Não foi possível solicitar informações da versão. Por favor, tente novamente.",
|
||||
"selfHostedServer": "Servidor Self-Hosted",
|
||||
"selfHostedWarning": "Se você está utilizando um servidor self-hosted, faça também a atualização da instância self-hosted, caso contrário o login no cliente web vai parar de funcionar.",
|
||||
"cancel": "Cancelar",
|
||||
"continueUpgrade": "Continuar Atualização",
|
||||
"upgradeFailed": "Atualização Falhou",
|
||||
"failedToApplyMigration": "Falha ao aplicar migração ({{current}} de {{total}})"
|
||||
|
||||
@@ -6,23 +6,22 @@
|
||||
"password": "Пароль",
|
||||
"passwordPlaceholder": "Введите ваш пароль",
|
||||
"rememberMe": "Запомнить меня",
|
||||
"loginButton": "Логин",
|
||||
"loginButton": "Войти",
|
||||
"noAccount": "Нет аккаунта?",
|
||||
"createVault": "Создать новое хранилище",
|
||||
"twoFactorTitle": "Пожалуйста, введите код аутентификации из вашего приложения-аутентификатора.",
|
||||
"authCode": "Код аутентификации",
|
||||
"authCodePlaceholder": "Введите 6-значный код",
|
||||
"verify": "Проверить",
|
||||
"cancel": "Отменить",
|
||||
"twoFactorNote": "Примечание: если у вас нет доступа к устройству аутентификации, вы можете сбросить ваш 2FA с помощью кода восстановления, войдя в систему через сайт.",
|
||||
"masterPassword": "Мастер пароль",
|
||||
"unlockVault": "Разблокировать хранилище",
|
||||
"unlockVault": "Разблокировать",
|
||||
"unlockWithPin": "Разблокировать ПИН-кодом",
|
||||
"enterPinToUnlock": "Введите ПИН-код для разблокировки хранилища",
|
||||
"useMasterPassword": "Использовать мастер-пароль",
|
||||
"unlockTitle": "Разблокировать ваше хранилище",
|
||||
"unlockDescription": "Введите ваш мастер пароль для разблокировки вашего хранилища.",
|
||||
"logout": "Выйти",
|
||||
"logoutConfirm": "Вы уверены, что хотите выйти?",
|
||||
"sessionExpired": "Время сеанса истекло. Пожалуйста, войдите снова.",
|
||||
"unlockSuccess": "Хранилище успешно разблокировано!",
|
||||
"unlockSuccessTitle": "Ваше хранилище успешно разблокировано",
|
||||
"unlockSuccessDescription": "Теперь вы можете использовать автозаполнение форм входа в Вашем браузере.",
|
||||
"closePopup": "Закрыть окно",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Подключение к",
|
||||
"switchAccounts": "Переключить аккаунт?",
|
||||
"loggedIn": "Вход выполнен",
|
||||
"loginWithMobile": "Войти с помощью мобильного приложения",
|
||||
"unlockWithMobile": "Разблокировать через мобильное приложение",
|
||||
"scanQrCode": "Сканируйте QR-код в приложении AliasVault, чтобы войти.",
|
||||
"errors": {
|
||||
"invalidCode": "Пожалуйста, введите правильный 6-значный код аутентификации.",
|
||||
"serverError": "Не удалось подключиться к серверу AliasVault. Пожалуйста, повторите попытку позже или обратитесь в службу поддержки, если проблема не устранится.",
|
||||
"noToken": "Вход не удался -- токен не возвращён",
|
||||
"migrationError": "Возникла ошибка при проверке ожидающих перемещений.",
|
||||
"wrongPassword": "Неверный пароль. Пожалуйста, повторите попытку.",
|
||||
"accountLocked": "Аккаунт временно заблокирован из-за слишком большого числа неудачных попыток.",
|
||||
"networkError": "Ошибка сети. Пожалуйста, проверьте соединение и повторите еще раз.",
|
||||
"sessionExpired": "Время сеанса истекло. Пожалуйста, войдите снова."
|
||||
"sessionExpired": "Время сеанса истекло. Пожалуйста, войдите снова.",
|
||||
"mobileLoginRequestExpired": "Истекло время ожидания входа. Перезагрузите страницу и повторите попытку."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Настройки"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Загрузка...",
|
||||
"notice": "Примечание",
|
||||
"error": "Ошибка",
|
||||
"success": "Успешно",
|
||||
"cancel": "Отмена",
|
||||
"back": "Back",
|
||||
"confirm": "Подтвердить",
|
||||
"back": "Назад",
|
||||
"next": "Далее",
|
||||
"use": "Использовать",
|
||||
"delete": "Удалить",
|
||||
"or": "Or",
|
||||
"save": "Сохранить",
|
||||
"or": "Или",
|
||||
"close": "Закрыть",
|
||||
"copied": "Скопировано!",
|
||||
"openInNewWindow": "Открыть в новом окне",
|
||||
"language": "Язык",
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"showPassword": "Показать пароль",
|
||||
"hidePassword": "Скрыть пароль",
|
||||
"showDetails": "Показать подробности",
|
||||
"hideDetails": "Скрыть подробности",
|
||||
"copyToClipboard": "Скопировать в буфер обмена",
|
||||
"loadingEmails": "Загрузка писем...",
|
||||
"loadingTotpCodes": "Загрузка TOTP кодов...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "Эта версия браузерного расширения AliasVault больше не поддерживается сервером. Пожалуйста, обновите расширение вашего браузера до последней версии.",
|
||||
"browserExtensionOutdated": "Это расширение браузера устарело и не может быть использовано для доступа к этому хранилищу. Пожалуйста, обновите расширение, чтобы продолжить.",
|
||||
"serverVersionNotSupported": "Чтобы использовать это расширение для браузера, сервер AliasVault необходимо обновить до более новой версии. Пожалуйста, обратитесь в службу поддержки, если вам нужна помощь.",
|
||||
"serverVersionTooOld": "Сервер AliasVault необходимо обновить до более новой версии, чтобы использовать эту функцию. Если вам нужна помощь, обратитесь к администратору сервера.",
|
||||
"unknownError": "Произошла неизвестная ошибка",
|
||||
"unknownErrorTryAgain": "Произошла неизвестная ошибка. Попробуйте снова.",
|
||||
"vaultNotAvailable": "Хранилище недоступно",
|
||||
"failedToRetrieveData": "Не удалось получить данные",
|
||||
"vaultIsLocked": "Хранилище заблокировано",
|
||||
"failedToUploadVault": "Не удалось загрузить хранилище",
|
||||
"passwordChanged": "С момента вашего последнего входа ваш пароль изменился. Пожалуйста, войдите еще раз в целях безопасности."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Неверный код восстановления. Пожалуйста, попробуйте снова.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Требуется токен обновления.",
|
||||
"INVALID_REFRESH_TOKEN": "Недопустимый токен обновления.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Токен обновления успешно отозван.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "В настоящее время регистрация новой учетной записи на этом сервере отключена. Пожалуйста, свяжитесь с администратором.",
|
||||
"USERNAME_REQUIRED": "Требуется ввести имя пользователя.",
|
||||
"USERNAME_ALREADY_IN_USE": "Имя пользователя уже используется.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "или",
|
||||
"new": "Новый",
|
||||
"cancel": "отмена",
|
||||
"search": "Поиск",
|
||||
"vaultLocked": "AliasVault заблокирован.",
|
||||
"creatingNewAlias": "Создание нового псевдонима...",
|
||||
"noMatchesFound": "Совпадений не найдено",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Сгенерировать новый пароль",
|
||||
"togglePasswordVisibility": "Переключение видимости пароля",
|
||||
"passwordCopiedToClipboard": "Пароль скопирован в буфер обмена",
|
||||
"enterEmailAndOrUsernameError": "Введите адрес электронной почты и/или имя пользователя",
|
||||
"openAliasVaultToUpgrade": "Откройте AliasVault для обновления",
|
||||
"vaultUpgradeRequired": "Требуется обновление хранилища.",
|
||||
"dismissPopup": "Закрыть окно"
|
||||
@@ -176,53 +177,28 @@
|
||||
"deleteCredential": "Удалить учетные данные",
|
||||
"credentialDetails": "Подробности учетных данных",
|
||||
"serviceName": "Название сервиса",
|
||||
"serviceNamePlaceholder": "например, Gmail, Facebook, Банк",
|
||||
"website": "Сайт",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Имя пользователя",
|
||||
"usernamePlaceholder": "Введите имя пользователя",
|
||||
"password": "Пароль",
|
||||
"passwordPlaceholder": "Введите пароль",
|
||||
"generatePassword": "Сгенерировать пароль",
|
||||
"copyPassword": "Скопировать пароль",
|
||||
"showPassword": "Показать пароль",
|
||||
"hidePassword": "Скрыть пароль",
|
||||
"notes": "Заметки",
|
||||
"notesPlaceholder": "Дополнительные заметки...",
|
||||
"totp": "Двухфакторная аутентификация",
|
||||
"totpCode": "TOTP код",
|
||||
"copyTotp": "Скопировать TOTP",
|
||||
"totpSecret": "TOTP секрет",
|
||||
"totpSecretPlaceholder": "Введите секретный ключ TOTP",
|
||||
"noCredentials": "Учетные данные не найдены",
|
||||
"noCredentialsDescription": "Добавьте свои первые учетные данные, чтобы начать работу",
|
||||
"searchPlaceholder": "Поиск учетных данных...",
|
||||
"welcomeTitle": "Добро пожаловать в AliasVault!",
|
||||
"welcomeDescription": "Чтобы использовать браузерное расширение AliasVault: перейдите на сайт и используйте всплывающее окно автозаполнения AliasVault для создания новых учетных данных.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
"noAttachmentsFound": "No credentials with attachments found",
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"noPasskeysFound": "Ключи доступа еще не созданы. Ключи доступа создаются при посещении веб-сайта, который предлагает их в качестве метода аутентификации.",
|
||||
"noAttachmentsFound": "Не найдено учетных данных с вложениями",
|
||||
"noMatchingCredentials": "Соответствующие учетные данные не найдены",
|
||||
"createdAt": "Создан",
|
||||
"updatedAt": "Последнее обновление",
|
||||
"autofill": "Автозаполнение",
|
||||
"fillForm": "Заполнить форму",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить эти учетные данные?",
|
||||
"saveSuccess": "Учетные данные успешно сохранены",
|
||||
"tags": "Теги",
|
||||
"addTag": "Добавить тег",
|
||||
"removeTag": "Удалить тег",
|
||||
"folder": "Папка",
|
||||
"selectFolder": "Выбрать папку",
|
||||
"createFolder": "Создать папку",
|
||||
"saveCredential": "Сохранить учетные данные",
|
||||
"deleteCredentialTitle": "Удалить учетные данные",
|
||||
"deleteCredentialConfirm": "Вы уверены, что хотите удалить эти учетные данные? Это действие невозможно отменить.",
|
||||
"filters": {
|
||||
"all": "(All) Credentials",
|
||||
"passkeys": "Passkeys",
|
||||
"aliases": "Aliases",
|
||||
"userpass": "Passwords",
|
||||
"attachments": "Attachments"
|
||||
"all": "(Все) учетные данные",
|
||||
"passkeys": "Ключи доступа",
|
||||
"aliases": "Псевдонимы",
|
||||
"userpass": "Пароли",
|
||||
"attachments": "Вложения"
|
||||
},
|
||||
"randomAlias": "Случайный псевдоним",
|
||||
"manual": "Инструкция",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Введите полный адрес электронной почты",
|
||||
"enterEmailPrefix": "Введите префикс электронной почты"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Добавить код 2FA",
|
||||
"instructions": "Введите секретный ключ, указанный на веб-сайте, где вы хотите добавить двухфакторную аутентификацию.",
|
||||
"nameOptional": "Имя (необязательно)",
|
||||
"secretKey": "Секретный ключ",
|
||||
"saveToViewCode": "Сохранить для просмотра кода",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Неверный формат секретного ключа."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Письма",
|
||||
"deleteEmailTitle": "Удалить письмо",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Открыть веб-приложение",
|
||||
"loggedIn": "Вход выполнен",
|
||||
"logout": "Выйти",
|
||||
"lock": "Заблокировать",
|
||||
"globalSettings": "Глобальные настройки",
|
||||
"autofillPopup": "Всплывающее окно автозаполнения",
|
||||
"activeOnAllSites": "Активен на всех сайтах (если не отключен ниже)",
|
||||
"disabledOnAllSites": "Отключено на всех сайтах",
|
||||
"enabled": "Включен",
|
||||
"disabled": "Выключен",
|
||||
"rightClickContextMenu": "Контекстное меню правым щелчком мыши",
|
||||
"autofillMatching": "Соответствие автозаполнения",
|
||||
"autofillMatchingMode": "Режим сопоставления автозаполнения",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
"configureKeyboardShortcuts": "Настройка горячих клавиш",
|
||||
"configure": "Настройка",
|
||||
"security": "Безопасность",
|
||||
"clipboardClearTimeout": "Очистить буфер обмена после копирования",
|
||||
"clipboardClearTimeoutDescription": "Автоматическая очистка буфера обмена после копирования конфиденциальных данных",
|
||||
"clipboardClearDisabled": "Никогда не очищать",
|
||||
@@ -352,72 +336,85 @@
|
||||
"autoLock8Hours": "8 часов",
|
||||
"autoLock24Hours": "24 часов",
|
||||
"versionPrefix": "Версия ",
|
||||
"preferences": "Предпочтения",
|
||||
"autofillSettings": "Настройки автозаполнения",
|
||||
"clipboardSettings": "Настройки буфера обмена",
|
||||
"contextMenuSettings": "Настройки контекстного меню",
|
||||
"passkeySettings": "Passkey Settings",
|
||||
"passkeySettings": "Настройки ключа доступа",
|
||||
"contextMenu": "Контекстное меню",
|
||||
"contextMenuEnabled": "Контекстное меню включено",
|
||||
"contextMenuDisabled": "Контекстное меню отключено",
|
||||
"contextMenuDescription": "Щелкните правой кнопкой мыши на полях ввода, чтобы получить доступ к параметрам AliasVault",
|
||||
"selectLanguage": "Выбрать язык",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
|
||||
"customApiUrl": "API URL",
|
||||
"customClientUrl": "Client URL",
|
||||
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
|
||||
"clientUrlHint": "The web interface URL of your self-hosted instance",
|
||||
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"serverConfiguration": "Конфигурация сервера",
|
||||
"serverConfigurationDescription": "Настройте URL-адрес сервера AliasVault для самостоятельно размещенных экземпляров",
|
||||
"customApiUrl": "URL-адрес API",
|
||||
"customClientUrl": "URL-адрес клиента",
|
||||
"apiUrlHint": "URL-адрес конечной точки API (обычно URL-адрес клиента + /api)",
|
||||
"clientUrlHint": "URL-адрес веб-интерфейса вашего самостоятельно размещенного экземпляра",
|
||||
"autofillSettingsDescription": "Включить или отключить всплывающее окно автозаполнения на веб-страницах",
|
||||
"autofillEnabledDescription": "В формах входа будут появляться подсказки для автозаполнения",
|
||||
"autofillDisabledDescription": "Подсказки автозаполнения отключены глобально",
|
||||
"languageSettings": "Язык",
|
||||
"validation": {
|
||||
"apiUrlRequired": "Требуется URL-адрес API",
|
||||
"apiUrlInvalid": "Пожалуйста, введите корректный URL-адрес API",
|
||||
"clientUrlRequired": "Требуется URL-адрес клиента",
|
||||
"clientUrlInvalid": "Пожалуйста, введите корректный URL-адрес клиента"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Способ разблокировки хранилища",
|
||||
"introText": "Выберите способ разблокировки хранилища. Вы можете использовать свой мастер-пароль (он доступен всегда) или установить ПИН-код для быстрого доступа. После 3 неудачных попыток ввода ПИН-кода необходимо будет ввести мастер-пароль.",
|
||||
"password": "Мастер-пароль",
|
||||
"pin": "ПИН-код",
|
||||
"pinDescription": "Разблокировать хранилище ПИН-кодом",
|
||||
"setupPin": "Установить ПИН-код",
|
||||
"enterNewPinDescription": "Введите ПИН-код, состоящий как минимум из 6 цифр",
|
||||
"confirmPin": "Подтвердите ПИН-код",
|
||||
"confirmPinDescription": "Повторите ПИН-код для подтверждения",
|
||||
"invalidPinFormat": "Неверный формат ПИН-кода",
|
||||
"pinMismatch": "ПИН-коды не совпадают",
|
||||
"incorrectPin": "Неверный ПИН-код. Осталось {{attemptsRemaining}} попытки.",
|
||||
"incorrectPinSingular": "Неверный ПИН-код. Осталась 1 попытка.",
|
||||
"enableSuccess": "Разблокировка по ПИН-коду успешно включена",
|
||||
"pinLocked": "Разблокировка по ПИН-коду отключена. Пожалуйста, используйте ваш мастер-пароль для разблокировки хранилища.",
|
||||
"pinSecurityWarning": "Разблокировка с помощью ПИН-кода в расширении для браузера может быть менее безопасной, чем использование вашего мастер-пароля, поскольку ПИН-коды обычно имеют более низкую энтропию и могут быть подобраны методом перебора (брутфорс), если ваше устройство будет скомпрометировано. Используйте этот способ только на устройствах, которым вы полностью доверяете."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
"passkey": "Passkey",
|
||||
"site": "Site",
|
||||
"displayName": "Name",
|
||||
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
|
||||
"passkeyMarkedForDeletion": "Passkey marked for deletion",
|
||||
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
|
||||
"passkey": "Ключ доступа",
|
||||
"site": "Сайт",
|
||||
"displayName": "Имя",
|
||||
"helpText": "Ключи доступа создаются на веб-сайте по запросу. Их нельзя редактировать вручную. Чтобы удалить этот ключ доступа, вы можете удалить его из этой учетной записи. Чтобы заменить этот ключ доступа или создать новый, посетите веб-сайт и следуйте его инструкциям.",
|
||||
"passkeyMarkedForDeletion": "Ключ доступа помечен на удаление",
|
||||
"passkeyWillBeDeleted": "Этот ключ доступа будет удален при сохранении этой учетной записи.",
|
||||
"bypass": {
|
||||
"title": "Use Browser Passkey",
|
||||
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
|
||||
"thisTimeOnly": "This time only",
|
||||
"alwaysForSite": "Always for this site"
|
||||
"title": "Использовать ключ доступа браузера",
|
||||
"description": "На какой срок вы хотите предоставить сайту {{origin}} доступ к ключам, сохраненным в браузере?",
|
||||
"thisTimeOnly": "Только в этот раз",
|
||||
"alwaysForSite": "Всегда для этого сайта"
|
||||
},
|
||||
"authenticate": {
|
||||
"title": "Sign in with Passkey",
|
||||
"signInFor": "Sign in with passkey for",
|
||||
"selectPasskey": "Select a passkey to sign in:",
|
||||
"noPasskeysFound": "No passkeys found for this site",
|
||||
"useBrowserPasskey": "Use Browser Passkey"
|
||||
"title": "Вход с помощью ключа доступа",
|
||||
"signInFor": "Вход с ключом доступа для",
|
||||
"selectPasskey": "Выберите ключ доступа для входа:",
|
||||
"noPasskeysFound": "Ключи доступа для этого сайта не найдены",
|
||||
"useBrowserPasskey": "Использовать ключ доступа из браузера"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Passkey",
|
||||
"createFor": "Create a new passkey for",
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
"replacingPasskey": "Replacing passkey: {{displayName}}",
|
||||
"confirmReplace": "Confirm Replace"
|
||||
"title": "Создание ключа доступа",
|
||||
"createFor": "Создайте новый ключ доступа для",
|
||||
"titleLabel": "Название",
|
||||
"titlePlaceholder": "Укажите название для этого ключа доступа",
|
||||
"createButton": "Создать ключ доступа",
|
||||
"useBrowserPasskey": "Использовать ключ доступа из браузера",
|
||||
"selectPasskeyToReplace": "Выберите ключ доступа для замены:",
|
||||
"createNewPasskey": "Создать новый ключ доступа",
|
||||
"replacingPasskey": "Замена ключа доступа: {{displayName}}",
|
||||
"confirmReplace": "Подтвердить замену"
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProvider": "Провайдер ключей доступа",
|
||||
"passkeyProviderOn": "Провайдер ключей доступа включен "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "Что нового",
|
||||
"whatsNewDescription": "Для поддержки следующих изменений требуется обновление:",
|
||||
"noDescriptionAvailable": "Описание для этой версии недоступно.",
|
||||
"okay": "ОК",
|
||||
"status": {
|
||||
"preparingUpgrade": "Подготовка обновления...",
|
||||
"vaultAlreadyUpToDate": "Хранилище уже обновлено",
|
||||
"startingDatabaseTransaction": "Запуск операции с базой данных...",
|
||||
"applyingDatabaseMigrations": "Применение перемещения базы данных...",
|
||||
"applyingMigration": "Применяя перемещение {{current}} из {{total}}...",
|
||||
"committingChanges": "Фиксация изменений..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Ошибка",
|
||||
"unableToGetVersionInfo": "Не удалось получить информацию о версии. Пожалуйста, попробуйте снова.",
|
||||
"selfHostedServer": "Автономный сервер",
|
||||
"selfHostedWarning": "Если вы используете автономный сервер, обязательно обновите свой автономный экземпляр, так как в противном случае вход в веб-клиент перестанет работать.",
|
||||
"cancel": "Отменить",
|
||||
"continueUpgrade": "Продолжить обновление",
|
||||
"upgradeFailed": "Ошибка обновления",
|
||||
"failedToApplyMigration": "Не удалось применить перенос ({{current}} из {{total}})"
|
||||
|
||||
@@ -6,23 +6,22 @@
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"loginButton": "Log in",
|
||||
"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",
|
||||
"unlockVault": "Unlock",
|
||||
"unlockWithPin": "Unlock with PIN",
|
||||
"enterPinToUnlock": "Enter your PIN to unlock your vault",
|
||||
"useMasterPassword": "Use Master Password",
|
||||
"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",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginWithMobile": "Log in using Mobile App",
|
||||
"unlockWithMobile": "Unlock using Mobile App",
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
|
||||
"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.",
|
||||
"sessionExpired": "Your session has expired. Please log in again."
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"mobileLoginRequestExpired": "Mobile login request timed out. Please reload the page and try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"notice": "Notice",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"or": "Or",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"showDetails": "Show details",
|
||||
"hideDetails": "Hide details",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
|
||||
"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.",
|
||||
"serverVersionTooOld": "The AliasVault server needs to be updated to a newer version in order to use this feature. Please contact the server admin if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"unknownErrorTryAgain": "An unknown error occurred. Please try again.",
|
||||
"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": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"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.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
@@ -164,7 +166,6 @@
|
||||
"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"
|
||||
@@ -176,27 +177,12 @@
|
||||
"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.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"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.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"lock": "Lock",
|
||||
"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",
|
||||
@@ -331,7 +316,6 @@
|
||||
"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",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred 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"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Vault Unlock Method",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "Master Password",
|
||||
"pin": "PIN Code",
|
||||
"pinDescription": "Unlock vault with PIN code",
|
||||
"setupPin": "Setup PIN Code",
|
||||
"enterNewPinDescription": "Enter a PIN code consisting of minimum 6 digits",
|
||||
"confirmPin": "Confirm PIN",
|
||||
"confirmPinDescription": "Enter your PIN again to confirm",
|
||||
"invalidPinFormat": "Invalid PIN format",
|
||||
"pinMismatch": "PINs do not match",
|
||||
"incorrectPin": "Incorrect PIN. {{attemptsRemaining}} attempts remaining.",
|
||||
"incorrectPinSingular": "Incorrect PIN. 1 attempt remaining.",
|
||||
"enableSuccess": "PIN unlock enabled successfully!",
|
||||
"pinLocked": "PIN unlock has been disabled. Please use your master password to unlock your vault.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProviderOn": "Passkey Provider on "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"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}})"
|
||||
|
||||
@@ -6,23 +6,22 @@
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"rememberMe": "Remember me",
|
||||
"loginButton": "Login",
|
||||
"loginButton": "Log in",
|
||||
"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",
|
||||
"unlockVault": "Unlock",
|
||||
"unlockWithPin": "Unlock with PIN",
|
||||
"enterPinToUnlock": "Enter your PIN to unlock your vault",
|
||||
"useMasterPassword": "Use Master Password",
|
||||
"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",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Connecting to",
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loggedIn": "Logged in",
|
||||
"loginWithMobile": "Log in using Mobile App",
|
||||
"unlockWithMobile": "Unlock using Mobile App",
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
|
||||
"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.",
|
||||
"sessionExpired": "Your session has expired. Please log in again."
|
||||
"sessionExpired": "Your session has expired. Please log in again.",
|
||||
"mobileLoginRequestExpired": "Mobile login request timed out. Please reload the page and try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Settings"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Loading...",
|
||||
"notice": "Notice",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"use": "Use",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"or": "Or",
|
||||
"close": "Close",
|
||||
"copied": "Copied!",
|
||||
"openInNewWindow": "Open in new window",
|
||||
"language": "Language",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"showPassword": "Show password",
|
||||
"hidePassword": "Hide password",
|
||||
"showDetails": "Show details",
|
||||
"hideDetails": "Hide details",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"loadingEmails": "Loading emails...",
|
||||
"loadingTotpCodes": "Loading TOTP codes...",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "This version of the AliasVault browser extension is not supported by the server anymore. Please update your browser extension to the latest version.",
|
||||
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
|
||||
"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.",
|
||||
"serverVersionTooOld": "The AliasVault server needs to be updated to a newer version in order to use this feature. Please contact the server admin if you need help.",
|
||||
"unknownError": "An unknown error occurred",
|
||||
"unknownErrorTryAgain": "An unknown error occurred. Please try again.",
|
||||
"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": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"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.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "or",
|
||||
"new": "New",
|
||||
"cancel": "Cancel",
|
||||
"search": "Search",
|
||||
"vaultLocked": "AliasVault is locked.",
|
||||
"creatingNewAlias": "Creating new alias...",
|
||||
"noMatchesFound": "No matches found",
|
||||
@@ -164,7 +166,6 @@
|
||||
"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"
|
||||
@@ -176,27 +177,12 @@
|
||||
"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.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
@@ -204,16 +190,6 @@
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"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.",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Enter full email address",
|
||||
"enterEmailPrefix": "Enter email prefix"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Emails",
|
||||
"deleteEmailTitle": "Delete Email",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Open web app",
|
||||
"loggedIn": "Logged in",
|
||||
"logout": "Logout",
|
||||
"lock": "Lock",
|
||||
"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",
|
||||
@@ -331,7 +316,6 @@
|
||||
"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",
|
||||
@@ -352,7 +336,6 @@
|
||||
"autoLock8Hours": "8 hours",
|
||||
"autoLock24Hours": "24 hours",
|
||||
"versionPrefix": "Version ",
|
||||
"preferences": "Preferences",
|
||||
"autofillSettings": "Autofill Settings",
|
||||
"clipboardSettings": "Clipboard Settings",
|
||||
"contextMenuSettings": "Context Menu Settings",
|
||||
@@ -372,12 +355,29 @@
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred 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"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Vault Unlock Method",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "Master Password",
|
||||
"pin": "PIN Code",
|
||||
"pinDescription": "Unlock vault with PIN code",
|
||||
"setupPin": "Setup PIN Code",
|
||||
"enterNewPinDescription": "Enter a PIN code consisting of minimum 6 digits",
|
||||
"confirmPin": "Confirm PIN",
|
||||
"confirmPinDescription": "Enter your PIN again to confirm",
|
||||
"invalidPinFormat": "Invalid PIN format",
|
||||
"pinMismatch": "PINs do not match",
|
||||
"incorrectPin": "Incorrect PIN. {{attemptsRemaining}} attempts remaining.",
|
||||
"incorrectPinSingular": "Incorrect PIN. 1 attempt remaining.",
|
||||
"enableSuccess": "PIN unlock enabled successfully!",
|
||||
"pinLocked": "PIN unlock has been disabled. Please use your master password to unlock your vault.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProviderOn": "Passkey Provider on "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"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}})"
|
||||
|
||||
@@ -13,16 +13,15 @@
|
||||
"authCode": "Код автентифікації",
|
||||
"authCodePlaceholder": "Введіть 6-значний код",
|
||||
"verify": "Перевірка",
|
||||
"cancel": "Скасувати",
|
||||
"twoFactorNote": "Примітка: якщо у вас немає доступу до вашого пристрою автентифікатора, ви можете скинути налаштування 2FA за допомогою коду відновлення, увійшовши через вебсайт.",
|
||||
"masterPassword": "Головний пароль",
|
||||
"unlockVault": "Розблокувати Vault",
|
||||
"unlockVault": "Розблокувати",
|
||||
"unlockWithPin": "Розблокувати за допомогою ПІН-коду",
|
||||
"enterPinToUnlock": "Введіть свій ПІН-код, щоб розблокувати сховище",
|
||||
"useMasterPassword": "Використовуйте головний пароль",
|
||||
"unlockTitle": "Розблокувати своє сховище",
|
||||
"unlockDescription": "Введіть свій головний пароль, щоб розблокувати сховище.",
|
||||
"logout": "Вийти",
|
||||
"logoutConfirm": "Ви впевнені, що хочете вийти?",
|
||||
"sessionExpired": "Ваш сеанс закінчився. Будь ласка, увійдіть знову.",
|
||||
"unlockSuccess": "Сховище успішно розблоковано!",
|
||||
"unlockSuccessTitle": "Ваше сховище успішно розблоковано",
|
||||
"unlockSuccessDescription": "Тепер ви можете використовувати автозаповнення форм входу у вашому браузері.",
|
||||
"closePopup": "Закрити цю підказку",
|
||||
@@ -30,15 +29,17 @@
|
||||
"connectingTo": "Підключення до",
|
||||
"switchAccounts": "Змінити обліковий запис?",
|
||||
"loggedIn": "Вхід виконано",
|
||||
"loginWithMobile": "Увійти за допомогою мобільного додатку",
|
||||
"unlockWithMobile": "Розблокувати за допомогою мобільного додатку",
|
||||
"scanQrCode": "Проскануйте цей QR-код за допомогою мобільного додатка AliasVault, щоб увійти та розблокувати сховище.",
|
||||
"errors": {
|
||||
"invalidCode": "Будь ласка, введіть дійсний 6-значний код автентифікації.",
|
||||
"serverError": "Не вдалося зв’язатися зі сервером AliasVault. Будь ласка, спробуйте пізніше або зверніться до служби підтримки, якщо проблема не зникне.",
|
||||
"noToken": "Не вдалося ввійти -- токен не знайдено",
|
||||
"migrationError": "Під час перевірки незавершених перенесень сталася помилка.",
|
||||
"wrongPassword": "Невірний пароль. Будь ласка, спробуйте ще раз.",
|
||||
"accountLocked": "Обліковий запис тимчасово заблоковано через занадто багато невдалих спроб.",
|
||||
"networkError": "Помилка мережі. Будь ласка, перевірте з’єднання та спробуйте ще раз.",
|
||||
"sessionExpired": "Your session has expired. Please log in again."
|
||||
"sessionExpired": "Час сеансу закінчився. Будь ласка, увійдіть знову.",
|
||||
"mobileLoginRequestExpired": "Час очікування запиту на вхід у мобільний додаток закінчився. Перезавантажте сторінку та спробуйте ще раз."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,23 +48,26 @@
|
||||
"settings": "Налаштування"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "Завантаження даних...",
|
||||
"notice": "Примітка",
|
||||
"error": "Помилка",
|
||||
"success": "Успішно",
|
||||
"cancel": "Скасувати",
|
||||
"back": "Back",
|
||||
"confirm": "Підтвердити",
|
||||
"back": "Назад",
|
||||
"next": "Далі",
|
||||
"use": "Використовувати",
|
||||
"delete": "Видалити",
|
||||
"or": "Or",
|
||||
"save": "Save",
|
||||
"or": "Або",
|
||||
"close": "Закрити",
|
||||
"copied": "Скопійовано!",
|
||||
"openInNewWindow": "Відкрити у новому вікні",
|
||||
"language": "Мова",
|
||||
"enabled": "Увімкнено",
|
||||
"disabled": "Вимкнено",
|
||||
"showPassword": "Показати пароль",
|
||||
"hidePassword": "Приховати пароль",
|
||||
"showDetails": "Показати подробиці",
|
||||
"hideDetails": "Приховати подробиці",
|
||||
"copyToClipboard": "Копіювати до буфера обміну",
|
||||
"loadingEmails": "Завантаження електронних адрес...",
|
||||
"loadingTotpCodes": "Завантаження кодів TOTP...",
|
||||
@@ -93,13 +97,13 @@
|
||||
"errors": {
|
||||
"serverNotAvailable": "Не вдалося зв’язатися зі сервером AliasVault. Будь ласка, спробуйте пізніше або зверніться до служби підтримки, якщо проблема не зникне.",
|
||||
"clientVersionNotSupported": "Ця версія розширення браузера AliasVault більше не підтримується сервером. Будь ласка, оновіть розширення браузера до останньої версії.",
|
||||
"browserExtensionOutdated": "This browser extension is outdated and cannot be used to access this vault. Please update this browser extension to continue.",
|
||||
"browserExtensionOutdated": "Це розширення браузера застаріло, і його не можна використовувати для доступу до цього сховища. Щоб продовжити, оновіть це розширення браузера.",
|
||||
"serverVersionNotSupported": "Щоб використовувати це розширення браузера, потрібно оновити сервер AliasVault до новішої версії. Зверніться до служби підтримки, якщо вам потрібна допомога.",
|
||||
"serverVersionTooOld": "Щоб скористатися цією функцією, потрібно оновити сервер AliasVault до нової версії. Якщо вам потрібна допомога, будь ласка, зверніться до адміністратора сервера.",
|
||||
"unknownError": "Сталася невідома помилка",
|
||||
"unknownErrorTryAgain": "Сталася невідома помилка. Будь ласка, спробуйте ще раз.",
|
||||
"vaultNotAvailable": "Сховище недоступне",
|
||||
"failedToRetrieveData": "Не вдалося отримати дані",
|
||||
"vaultIsLocked": "Сховище заблоковано",
|
||||
"failedToUploadVault": "Не вдалося завантажити сховище",
|
||||
"passwordChanged": "Ваш пароль змінився з моменту останнього входу. З міркувань безпеки, будь ласка, увійдіть ще раз."
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "Недійсний код відновлення. Будь ласка, спробуйте ще раз.",
|
||||
"REFRESH_TOKEN_REQUIRED": "Необхідне оновлення токена.",
|
||||
"INVALID_REFRESH_TOKEN": "Оновлення токена невдале.",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "Оновлення токена відкликано успішно.",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "Реєстрація нових облікових записів на цьому сервері наразі вимкнена. Зверніться до адміністратора.",
|
||||
"USERNAME_REQUIRED": "Ім'я користувача обов'язкове.",
|
||||
"USERNAME_ALREADY_IN_USE": "Ім'я користувача вже використовується.",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "або",
|
||||
"new": "Новий",
|
||||
"cancel": "Скасувати",
|
||||
"search": "Пошук",
|
||||
"vaultLocked": "AliasVault заблоковано.",
|
||||
"creatingNewAlias": "Створення нового псевдоніму...",
|
||||
"noMatchesFound": "Збігів не знайдено",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "Згенерувати новий пароль",
|
||||
"togglePasswordVisibility": "Перемикання видимості пароля",
|
||||
"passwordCopiedToClipboard": "Пароль скопійовано в буфер обміну",
|
||||
"enterEmailAndOrUsernameError": "Введіть електронну пошту та/або ім'я користувача",
|
||||
"openAliasVaultToUpgrade": "Відкрити AliasVault для покращення",
|
||||
"vaultUpgradeRequired": "Потрібне оновлення сховища.",
|
||||
"dismissPopup": "Закрити спливаюче вікно"
|
||||
@@ -176,53 +177,28 @@
|
||||
"deleteCredential": "Видалити облікові дані",
|
||||
"credentialDetails": "Відомості про облікові дані",
|
||||
"serviceName": "Назва сервісу",
|
||||
"serviceNamePlaceholder": "наприклад, Gmail, Facebook, Bank",
|
||||
"website": "Вебсайт",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "Ім'я користувача",
|
||||
"usernamePlaceholder": "Введіть ім'я користувача",
|
||||
"password": "Пароль",
|
||||
"passwordPlaceholder": "Введіть пароль",
|
||||
"generatePassword": "Згенерувати пароль",
|
||||
"copyPassword": "Копіювати пароль",
|
||||
"showPassword": "Показати пароль",
|
||||
"hidePassword": "Приховати пароль",
|
||||
"notes": "Нотатки",
|
||||
"notesPlaceholder": "Додаткові нотатки...",
|
||||
"totp": "Двофакторна аутентифікація",
|
||||
"totpCode": "Код TOTP",
|
||||
"copyTotp": "Копіювати TOTP",
|
||||
"totpSecret": "Секрет TOTP",
|
||||
"totpSecretPlaceholder": "Введіть секретний ключ TOTP",
|
||||
"noCredentials": "Облікових даних не знайдено",
|
||||
"noCredentialsDescription": "Додайте свої перші облікові дані, щоб розпочати",
|
||||
"searchPlaceholder": "Пошук облікових даних...",
|
||||
"welcomeTitle": "Ласкаво просимо до AliasVult!",
|
||||
"welcomeDescription": "Щоб скористатися розширенням браузера AliasVault: перейдіть на вебсайт і скористайтеся спливаючим вікном автозаповнення AliasVault, щоб створити нові облікові дані.",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
"noAttachmentsFound": "No credentials with attachments found",
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"noPasskeysFound": "Ключі доступу ще не створено. Ключі доступу створюються під час відвідування вебсайту, який пропонує ключі доступу як спосіб автентифікації.",
|
||||
"noAttachmentsFound": "Не знайдено облікових даних із вкладеннями",
|
||||
"noMatchingCredentials": "Не знайдено відповідних облікових даних",
|
||||
"createdAt": "Створено",
|
||||
"updatedAt": "Останнє оновлення",
|
||||
"autofill": "Автозаповнення",
|
||||
"fillForm": "Заповнити форму",
|
||||
"deleteConfirm": "Ви впевнені, що хочете видалити ці облікові дані?",
|
||||
"saveSuccess": "Облікові дані успішно збережено",
|
||||
"tags": "Теги",
|
||||
"addTag": "Додати тег",
|
||||
"removeTag": "Видалити тег",
|
||||
"folder": "Тека",
|
||||
"selectFolder": "Вибрати теку",
|
||||
"createFolder": "Створити теку",
|
||||
"saveCredential": "Зберегти облікові дані",
|
||||
"deleteCredentialTitle": "Видалити облікові дані",
|
||||
"deleteCredentialConfirm": "Ви впевнені, що хочете видалити ці облікові дані? Цю дію неможливо скасувати.",
|
||||
"filters": {
|
||||
"all": "(All) Credentials",
|
||||
"passkeys": "Passkeys",
|
||||
"aliases": "Aliases",
|
||||
"userpass": "Passwords",
|
||||
"attachments": "Attachments"
|
||||
"all": "(Усі) Облікові дані",
|
||||
"passkeys": "Ключі доступу",
|
||||
"aliases": "Псевдоніми",
|
||||
"userpass": "Паролі",
|
||||
"attachments": "Вкладення"
|
||||
},
|
||||
"randomAlias": "Випадковий псевдонім",
|
||||
"manual": "Посібник",
|
||||
@@ -240,7 +216,7 @@
|
||||
"avoidAmbiguousChars": "Уникайте неоднозначних символів (o, 0 тощо)",
|
||||
"generateNewPreview": "Згенерувати новий попередній перегляд",
|
||||
"generateRandomAlias": "Генерувати випадковий псевдонім",
|
||||
"clearAliasFields": "Clear Alias Fields",
|
||||
"clearAliasFields": "Очистити поля псевдоніма",
|
||||
"alias": "Псевдонім",
|
||||
"firstName": "Ім’я",
|
||||
"lastName": "Прізвище",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "Введіть повну електронну адресу",
|
||||
"enterEmailPrefix": "Введіть префікс електронної адреси"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "Add 2FA Code",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "Name (optional)",
|
||||
"secretKey": "Secret Key",
|
||||
"saveToViewCode": "Save to view code",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Invalid secret key format."
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "Електронні листи",
|
||||
"deleteEmailTitle": "Видалити електронного листа",
|
||||
@@ -304,12 +290,11 @@
|
||||
"openWebApp": "Відкрити веб додаток",
|
||||
"loggedIn": "Вхід виконано",
|
||||
"logout": "Вийти",
|
||||
"lock": "Заблокувати",
|
||||
"globalSettings": "Глобальні налаштування",
|
||||
"autofillPopup": "Спливаюче вікно автозаповнення",
|
||||
"activeOnAllSites": "Активно на всіх сайтах (якщо не вимкнено нижче)",
|
||||
"disabledOnAllSites": "Вимкнено на всіх сайтах",
|
||||
"enabled": "Увімкнено",
|
||||
"disabled": "Вимкнено",
|
||||
"rightClickContextMenu": "Контекстне меню правою кнопкою миші",
|
||||
"autofillMatching": "Автозаповнення відповідності",
|
||||
"autofillMatchingMode": "Режим автозаповнення відповідностей",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "Комбінації клавіш",
|
||||
"configureKeyboardShortcuts": "Налаштування комбінацій клавіш",
|
||||
"configure": "Налаштування",
|
||||
"security": "Безпека",
|
||||
"clipboardClearTimeout": "Очистити буфер обміну після копіювання",
|
||||
"clipboardClearTimeoutDescription": "Автоматично очищати буфер обміну після копіювання конфіденційних даних",
|
||||
"clipboardClearDisabled": "Ніколи не очищати",
|
||||
@@ -352,32 +336,48 @@
|
||||
"autoLock8Hours": "8 годин",
|
||||
"autoLock24Hours": "24 години",
|
||||
"versionPrefix": "Версія ",
|
||||
"preferences": "Налаштування",
|
||||
"autofillSettings": "Налаштування автозаповнення",
|
||||
"clipboardSettings": "Параметри буфера обміну",
|
||||
"contextMenuSettings": "Налаштування контекстного меню",
|
||||
"passkeySettings": "Passkey Settings",
|
||||
"passkeySettings": "Налаштування ключа доступу",
|
||||
"contextMenu": "Контекстне меню",
|
||||
"contextMenuEnabled": "Контекстне меню увімкнено",
|
||||
"contextMenuDisabled": "Контекстне меню вимкнено",
|
||||
"contextMenuDescription": "Натисніть правою кнопкою миші на поля введення, щоб отримати доступ до параметрів AliasVault",
|
||||
"selectLanguage": "Виберіть мову",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
|
||||
"customApiUrl": "API URL",
|
||||
"customClientUrl": "Client URL",
|
||||
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
|
||||
"clientUrlHint": "The web interface URL of your self-hosted instance",
|
||||
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"serverConfiguration": "Конфігурація сервера",
|
||||
"serverConfigurationDescription": "Налаштування URL-адреси сервера AliasVault для самостійно розміщених екземплярів",
|
||||
"customApiUrl": "URL-адреса API",
|
||||
"customClientUrl": "URL-адреса клієнта",
|
||||
"apiUrlHint": "URL-адреса кінцевої точки API (зазвичай URL-адреса клієнта + /api)",
|
||||
"clientUrlHint": "URL-адреса вебінтерфейсу вашого самостійного екземпляра",
|
||||
"autofillSettingsDescription": "Увімкнення або вимкнення спливаючого вікна автозаповнення на вебсторінках",
|
||||
"autofillEnabledDescription": "",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"validation": {
|
||||
"apiUrlRequired": "URL-адреса API обов'язкова",
|
||||
"apiUrlInvalid": "Будь ласка, введіть дійсну URL-адресу API",
|
||||
"clientUrlRequired": "URL-адреса клієнта обов'язкова",
|
||||
"clientUrlInvalid": "Будь ласка, введіть дійсну URL-адресу клієнта"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "Vault Unlock Method",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "Master Password",
|
||||
"pin": "PIN Code",
|
||||
"pinDescription": "Unlock vault with PIN code",
|
||||
"setupPin": "Setup PIN Code",
|
||||
"enterNewPinDescription": "Enter a PIN code consisting of minimum 6 digits",
|
||||
"confirmPin": "Confirm PIN",
|
||||
"confirmPinDescription": "Enter your PIN again to confirm",
|
||||
"invalidPinFormat": "Invalid PIN format",
|
||||
"pinMismatch": "PINs do not match",
|
||||
"incorrectPin": "Incorrect PIN. {{attemptsRemaining}} attempts remaining.",
|
||||
"incorrectPinSingular": "Incorrect PIN. 1 attempt remaining.",
|
||||
"enableSuccess": "PIN unlock enabled successfully!",
|
||||
"pinLocked": "PIN unlock has been disabled. Please use your master password to unlock your vault.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
@@ -406,7 +406,6 @@
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
@@ -415,9 +414,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProviderOn": "Passkey Provider on "
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "Що нового",
|
||||
"whatsNewDescription": "Для підтримки таких змін потрібне оновлення:",
|
||||
"noDescriptionAvailable": "Для цієї версії немає опису.",
|
||||
"okay": "Ок",
|
||||
"status": {
|
||||
"preparingUpgrade": "Підготовка оновлення...",
|
||||
"vaultAlreadyUpToDate": "Сховище вже оновлено",
|
||||
"startingDatabaseTransaction": "Початок транзакції бази даних...",
|
||||
"applyingDatabaseMigrations": "Застосування міграцій бази даних...",
|
||||
"applyingMigration": "Застосування міграції {{current}} з {{total}}...",
|
||||
"committingChanges": "Внесення змін..."
|
||||
},
|
||||
"alerts": {
|
||||
"error": "Помилка",
|
||||
"unableToGetVersionInfo": "Не вдалося отримати інформацію про версію. Спробуйте ще раз.",
|
||||
"selfHostedServer": "Сервер із самостійним розміщенням",
|
||||
"selfHostedWarning": "Якщо ви використовуєте власний сервер, обов’язково оновіть і свій власний екземпляр, інакше вхід до вебклієнта перестане працювати.",
|
||||
"cancel": "Скасувати",
|
||||
"continueUpgrade": "Продовжити оновлення",
|
||||
"upgradeFailed": "Помилка оновлення",
|
||||
"failedToApplyMigration": "Не вдалося застосувати міграцію ({{current}} з {{total}})"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"auth": {
|
||||
"loginTitle": "登录AliasVault",
|
||||
"loginTitle": "登录到 AliasVault",
|
||||
"username": "用户名或电子邮箱",
|
||||
"usernamePlaceholder": "姓名 / name@company.com",
|
||||
"password": "密码",
|
||||
@@ -10,35 +10,36 @@
|
||||
"noAccount": "还没有账户?",
|
||||
"createVault": "创建新密码库",
|
||||
"twoFactorTitle": "请输入认证器的动态验证码。",
|
||||
"authCode": "动态验证码",
|
||||
"authCodePlaceholder": "输入6位动态验证码",
|
||||
"authCode": "身份验证码",
|
||||
"authCodePlaceholder": "输入 6 位数代码",
|
||||
"verify": "验证",
|
||||
"cancel": "取消",
|
||||
"twoFactorNote": "注意:如果无法访问您的认证设备,您可以通过网站登录,使用恢复码重置双因素认证(2FA)。",
|
||||
"masterPassword": "主密码",
|
||||
"unlockVault": "解锁密码库",
|
||||
"unlockVault": "解锁",
|
||||
"unlockWithPin": "使用 PIN 解锁",
|
||||
"enterPinToUnlock": "输入您的 PIN 以解锁密码库",
|
||||
"useMasterPassword": "使用主密码",
|
||||
"unlockTitle": "解锁您的密码库",
|
||||
"unlockDescription": "输入您的主密码以解锁密码库。",
|
||||
"logout": "登出",
|
||||
"logout": "退出登录",
|
||||
"logoutConfirm": "确定要退出登录吗?",
|
||||
"sessionExpired": "您的会话已过期。请重新登录。",
|
||||
"unlockSuccess": "密码库解锁成功!",
|
||||
"unlockSuccessTitle": "您的密码库已成功解锁",
|
||||
"unlockSuccessDescription": "现在您可以在浏览器的登录表单中使用自动填充功能了。",
|
||||
"closePopup": "关闭此弹窗",
|
||||
"browseVault": "浏览密码库内容",
|
||||
"connectingTo": "正在连接到",
|
||||
"connectingTo": "正在连接",
|
||||
"switchAccounts": "切换账户?",
|
||||
"loggedIn": "已登录",
|
||||
"loginWithMobile": "使用移动应用登录",
|
||||
"unlockWithMobile": "使用移动应用解锁",
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
|
||||
"errors": {
|
||||
"invalidCode": "请输入有效的6位动态验证码。",
|
||||
"serverError": "无法连接到AliasVault服务器。请稍后重试,若问题依旧,请联系支持人员。",
|
||||
"noToken": "登录失败——未返回令牌",
|
||||
"migrationError": "检查待处理迁移时发生错误。",
|
||||
"wrongPassword": "密码不正确。请重试。",
|
||||
"accountLocked": "由于多次尝试失败,账户已暂时锁定。",
|
||||
"networkError": "网络错误。请检查您的连接后重试。",
|
||||
"sessionExpired": "您的会话已过期。请重新登录。"
|
||||
"sessionExpired": "您的会话已过期。请重新登录。",
|
||||
"mobileLoginRequestExpired": "Mobile login request timed out. Please reload the page and try again."
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -47,30 +48,33 @@
|
||||
"settings": "设置"
|
||||
},
|
||||
"common": {
|
||||
"appName": "AliasVault",
|
||||
"loading": "加载中…",
|
||||
"notice": "注意",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"cancel": "取消",
|
||||
"back": "Back",
|
||||
"confirm": "确认",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"use": "使用",
|
||||
"delete": "删除",
|
||||
"or": "Or",
|
||||
"save": "保存",
|
||||
"or": "或",
|
||||
"close": "关闭",
|
||||
"copied": "已复制!",
|
||||
"openInNewWindow": "在新窗口中打开",
|
||||
"language": "语言",
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用",
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"showDetails": "显示详情",
|
||||
"hideDetails": "隐藏详情",
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"loadingEmails": "加载邮件中…",
|
||||
"loadingTotpCodes": "加载TOTP验证码中…",
|
||||
"attachments": "附件",
|
||||
"loadingAttachments": "加载附件中…",
|
||||
"settings": "设置",
|
||||
"recentEmails": "最近邮件",
|
||||
"recentEmails": "近期电子邮件",
|
||||
"loginCredentials": "登录凭据",
|
||||
"twoFactorAuthentication": "双因素认证(2FA)",
|
||||
"alias": "别名",
|
||||
@@ -95,11 +99,11 @@
|
||||
"clientVersionNotSupported": "此版本的AliasVault浏览器扩展已不被服务器支持。请将浏览器扩展更新到最新版本。",
|
||||
"browserExtensionOutdated": "此浏览器扩展已过时,无法用于访问此密码库。请更新此浏览器扩展以继续。",
|
||||
"serverVersionNotSupported": "AliasVault服务器需要更新到新版本才能使用此浏览器扩展。如需帮助,请联系支持人员。",
|
||||
"serverVersionTooOld": "AliasVault 服务器需要更新到更高版本才能使用此功能。如需帮助,请联系服务器管理员。",
|
||||
"unknownError": "发生未知错误",
|
||||
"unknownErrorTryAgain": "发生未知错误,请重试。",
|
||||
"vaultNotAvailable": "密码库不可用",
|
||||
"failedToRetrieveData": "无法检索数据",
|
||||
"vaultIsLocked": "密码库已锁定",
|
||||
"failedToUploadVault": "上传密码库失败",
|
||||
"passwordChanged": "登录密码已更新,请重新登录以确保账户安全。"
|
||||
},
|
||||
"apiErrors": {
|
||||
@@ -111,7 +115,6 @@
|
||||
"INVALID_RECOVERY_CODE": "恢复码无效。请重试。",
|
||||
"REFRESH_TOKEN_REQUIRED": "刷新令牌为必填项。",
|
||||
"INVALID_REFRESH_TOKEN": "刷新令牌无效。",
|
||||
"REFRESH_TOKEN_REVOKED_SUCCESSFULLY": "刷新令牌已成功撤销。",
|
||||
"PUBLIC_REGISTRATION_DISABLED": "当前服务器已禁用新账户注册。请联系管理员。",
|
||||
"USERNAME_REQUIRED": "用户名为必填项。",
|
||||
"USERNAME_ALREADY_IN_USE": "用户名已被使用。",
|
||||
@@ -133,7 +136,6 @@
|
||||
"or": "或",
|
||||
"new": "新建",
|
||||
"cancel": "取消",
|
||||
"search": "搜索",
|
||||
"vaultLocked": "AliasVault已锁定。",
|
||||
"creatingNewAlias": "正在创建新别名…",
|
||||
"noMatchesFound": "未找到匹配项",
|
||||
@@ -164,7 +166,6 @@
|
||||
"generateNewPassword": "生成新密码",
|
||||
"togglePasswordVisibility": "切换密码可见性",
|
||||
"passwordCopiedToClipboard": "密码已复制到剪贴板",
|
||||
"enterEmailAndOrUsernameError": "请输入邮箱和/或用户名",
|
||||
"openAliasVaultToUpgrade": "打开AliasVault进行升级",
|
||||
"vaultUpgradeRequired": "需要升级密码库。",
|
||||
"dismissPopup": "关闭弹窗"
|
||||
@@ -176,53 +177,28 @@
|
||||
"deleteCredential": "删除凭据",
|
||||
"credentialDetails": "凭据详情",
|
||||
"serviceName": "服务名称",
|
||||
"serviceNamePlaceholder": "例如:Gmail、Facebook、银行",
|
||||
"website": "网站",
|
||||
"websitePlaceholder": "https://example.com",
|
||||
"username": "用户名",
|
||||
"usernamePlaceholder": "输入用户名",
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "输入密码",
|
||||
"generatePassword": "生成密码",
|
||||
"copyPassword": "复制密码",
|
||||
"showPassword": "显示密码",
|
||||
"hidePassword": "隐藏密码",
|
||||
"notes": "备注",
|
||||
"notesPlaceholder": "附加备注…",
|
||||
"totp": "双因素认证(2FA)",
|
||||
"totpCode": "TOTP验证码",
|
||||
"copyTotp": "复制 TOTP",
|
||||
"totpSecret": "TOTP密钥",
|
||||
"totpSecretPlaceholder": "输入TOTP密钥",
|
||||
"noCredentials": "未找到凭据",
|
||||
"noCredentialsDescription": "添加您的第一个凭据开始使用",
|
||||
"searchPlaceholder": "搜索凭据…",
|
||||
"welcomeTitle": "欢迎使用AliasVault!",
|
||||
"welcomeDescription": "要使用AliasVault浏览器扩展:导航到某个网站,使用AliasVault自动填充弹窗创建新凭据。",
|
||||
"noPasskeysFound": "No passkeys have been created yet. Passkeys are created by visiting a website that offers passkeys as an authentication method.",
|
||||
"noAttachmentsFound": "No credentials with attachments found",
|
||||
"noMatchingCredentials": "No matching credentials found",
|
||||
"noPasskeysFound": "尚未创建通行密钥。访问以通行密钥为认证方式的网站才能创建通行密钥。",
|
||||
"noAttachmentsFound": "未找到带有附件的凭据",
|
||||
"noMatchingCredentials": "未找到匹配的凭据",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "最后更新时间",
|
||||
"autofill": "自动填充",
|
||||
"fillForm": "填充表单",
|
||||
"deleteConfirm": "确定要删除此凭据吗?",
|
||||
"saveSuccess": "凭据保存成功",
|
||||
"tags": "标签",
|
||||
"addTag": "添加标签",
|
||||
"removeTag": "移除标签",
|
||||
"folder": "文件夹",
|
||||
"selectFolder": "选择文件夹",
|
||||
"createFolder": "创建文件夹",
|
||||
"saveCredential": "保存凭据",
|
||||
"deleteCredentialTitle": "删除凭据",
|
||||
"deleteCredentialConfirm": "确定要删除此凭据吗?此操作无法撤销。",
|
||||
"filters": {
|
||||
"all": "(All) Credentials",
|
||||
"passkeys": "Passkeys",
|
||||
"aliases": "Aliases",
|
||||
"userpass": "Passwords",
|
||||
"attachments": "Attachments"
|
||||
"all": "(所有)凭据",
|
||||
"passkeys": "通行密钥",
|
||||
"aliases": "别名",
|
||||
"userpass": "密码",
|
||||
"attachments": "附件"
|
||||
},
|
||||
"randomAlias": "随机别名",
|
||||
"manual": "手动",
|
||||
@@ -265,6 +241,16 @@
|
||||
"enterFullEmail": "输入验证邮箱地址",
|
||||
"enterEmailPrefix": "输入邮箱前缀"
|
||||
},
|
||||
"totp": {
|
||||
"addCode": "添加两步验证码",
|
||||
"instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.",
|
||||
"nameOptional": "名称(可选)",
|
||||
"secretKey": "密钥",
|
||||
"saveToViewCode": "保存以查看验证码",
|
||||
"errors": {
|
||||
"invalidSecretKey": "密钥格式无效。"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"title": "邮件",
|
||||
"deleteEmailTitle": "删除邮件",
|
||||
@@ -303,13 +289,12 @@
|
||||
"openInNewWindow": "在新窗口中打开",
|
||||
"openWebApp": "打开网页应用",
|
||||
"loggedIn": "已登录",
|
||||
"logout": "登出",
|
||||
"logout": "退出登录",
|
||||
"lock": "锁定",
|
||||
"globalSettings": "全局设置",
|
||||
"autofillPopup": "自动填充弹窗",
|
||||
"activeOnAllSites": "在所有网站上激活(除非在下方禁用)",
|
||||
"disabledOnAllSites": "在所有网站上禁用",
|
||||
"enabled": "启用",
|
||||
"disabled": "禁用",
|
||||
"rightClickContextMenu": "右键上下文菜单",
|
||||
"autofillMatching": "自动填充匹配",
|
||||
"autofillMatchingMode": "自动填充匹配模式",
|
||||
@@ -331,7 +316,6 @@
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"configureKeyboardShortcuts": "配置键盘快捷键",
|
||||
"configure": "配置",
|
||||
"security": "安全",
|
||||
"clipboardClearTimeout": "复制后清除剪贴板",
|
||||
"clipboardClearTimeoutDescription": "复制敏感数据后自动清除剪贴板",
|
||||
"clipboardClearDisabled": "从不清除",
|
||||
@@ -352,72 +336,85 @@
|
||||
"autoLock8Hours": "8 小时",
|
||||
"autoLock24Hours": "24 小时",
|
||||
"versionPrefix": "版本 ",
|
||||
"preferences": "首选项",
|
||||
"autofillSettings": "自动填充设置",
|
||||
"clipboardSettings": "剪贴板设置",
|
||||
"contextMenuSettings": "上下文菜单设置",
|
||||
"passkeySettings": "Passkey Settings",
|
||||
"passkeySettings": "通行密钥设置",
|
||||
"contextMenu": "上下文菜单",
|
||||
"contextMenuEnabled": "上下文菜单已启用",
|
||||
"contextMenuDisabled": "上下文菜单已停用",
|
||||
"contextMenuDescription": "右键点击输入字段即可访问 AliasVault 选项",
|
||||
"selectLanguage": "选择语言",
|
||||
"serverConfiguration": "Server Configuration",
|
||||
"serverConfiguration": "服务器配置",
|
||||
"serverConfigurationDescription": "Configure the AliasVault server URL for self-hosted instances",
|
||||
"customApiUrl": "API URL",
|
||||
"customClientUrl": "Client URL",
|
||||
"customClientUrl": "客户端 URL",
|
||||
"apiUrlHint": "The API endpoint URL (usually client URL + /api)",
|
||||
"clientUrlHint": "The web interface URL of your self-hosted instance",
|
||||
"autofillSettingsDescription": "Enable or disable the autofill popup on web pages",
|
||||
"autofillEnabledDescription": "Autofill suggestions will appear on login forms",
|
||||
"autofillDisabledDescription": "Autofill suggestions are disabled globally",
|
||||
"languageSettings": "Language",
|
||||
"languageSettingsDescription": "Choose your preferred language",
|
||||
"languageSettings": "语言",
|
||||
"validation": {
|
||||
"apiUrlRequired": "API URL 为必填项",
|
||||
"apiUrlInvalid": "请输入有效的 API URL",
|
||||
"clientUrlRequired": "客户端 URL 为必填项",
|
||||
"clientUrlInvalid": "请输入有效的客户端 URL"
|
||||
},
|
||||
"unlockMethod": {
|
||||
"title": "密码库解锁方式",
|
||||
"introText": "Choose how you want to unlock your vault. You can use your master password (always available) or set up a PIN code for faster access. After 3 failed PIN attempts, you'll need to use your master password.",
|
||||
"password": "主密码",
|
||||
"pin": "PIN 码",
|
||||
"pinDescription": "使用 PIN 码解锁密码库",
|
||||
"setupPin": "设置 PIN 码",
|
||||
"enterNewPinDescription": "Enter a PIN code consisting of minimum 6 digits",
|
||||
"confirmPin": "确认 PIN",
|
||||
"confirmPinDescription": "再次输入您的 PIN 以确认",
|
||||
"invalidPinFormat": "PIN 格式无效",
|
||||
"pinMismatch": "PIN 不一致",
|
||||
"incorrectPin": "Incorrect PIN. {{attemptsRemaining}} attempts remaining.",
|
||||
"incorrectPinSingular": "Incorrect PIN. 1 attempt remaining.",
|
||||
"enableSuccess": "PIN unlock enabled successfully!",
|
||||
"pinLocked": "PIN unlock has been disabled. Please use your master password to unlock your vault.",
|
||||
"pinSecurityWarning": "PIN unlock in the browser extension can be less secure than your master password, as PINs typically have lower entropy and may be brute-forced if your device is compromised. Use it only on devices you fully trust."
|
||||
}
|
||||
},
|
||||
"passkeys": {
|
||||
"passkey": "Passkey",
|
||||
"site": "Site",
|
||||
"displayName": "Name",
|
||||
"passkey": "通行密钥",
|
||||
"site": "网站",
|
||||
"displayName": "名称",
|
||||
"helpText": "Passkeys are created on the website when prompted. They cannot be manually edited. To remove this passkey, you can delete it from this credential. To replace this passkey or create a new one, visit the website and follow its prompts.",
|
||||
"passkeyMarkedForDeletion": "Passkey marked for deletion",
|
||||
"passkeyMarkedForDeletion": "通行密钥已标记为删除",
|
||||
"passkeyWillBeDeleted": "This passkey will be deleted when you save this credential.",
|
||||
"bypass": {
|
||||
"title": "Use Browser Passkey",
|
||||
"title": "使用浏览器通行密钥",
|
||||
"description": "How long would you like to use the browser's passkey provider for {{origin}}?",
|
||||
"thisTimeOnly": "This time only",
|
||||
"alwaysForSite": "Always for this site"
|
||||
"thisTimeOnly": "仅一次",
|
||||
"alwaysForSite": "始终为此网站"
|
||||
},
|
||||
"authenticate": {
|
||||
"title": "Sign in with Passkey",
|
||||
"signInFor": "Sign in with passkey for",
|
||||
"selectPasskey": "Select a passkey to sign in:",
|
||||
"noPasskeysFound": "No passkeys found for this site",
|
||||
"useBrowserPasskey": "Use Browser Passkey"
|
||||
"title": "使用通行密钥登录",
|
||||
"signInFor": "使用通行密钥登录或",
|
||||
"selectPasskey": "选择要登录的通行密钥:",
|
||||
"noPasskeysFound": "未找到此网站的通行密钥",
|
||||
"useBrowserPasskey": "使用浏览器通行密钥"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Passkey",
|
||||
"createFor": "Create a new passkey for",
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a name for this passkey",
|
||||
"createButton": "Create Passkey",
|
||||
"creatingButton": "Creating...",
|
||||
"useBrowserPasskey": "Use Browser Passkey",
|
||||
"selectPasskeyToReplace": "Select a passkey to replace:",
|
||||
"createNewPasskey": "Create New Passkey",
|
||||
"replacingPasskey": "Replacing passkey: {{displayName}}",
|
||||
"confirmReplace": "Confirm Replace"
|
||||
"title": "创建通行密钥",
|
||||
"createFor": "创建新通行密钥用于",
|
||||
"titleLabel": "标题",
|
||||
"titlePlaceholder": "输入此通行密钥的名称",
|
||||
"createButton": "创建通行密钥",
|
||||
"useBrowserPasskey": "使用浏览器通行密钥",
|
||||
"selectPasskeyToReplace": "选择要替换的通行密钥:",
|
||||
"createNewPasskey": "创建新通行密钥",
|
||||
"replacingPasskey": "替换通行密钥:{{displayName}}",
|
||||
"confirmReplace": "确认替换"
|
||||
},
|
||||
"settings": {
|
||||
"passkeyProvider": "Passkey Provider",
|
||||
"passkeyProviderOn": "Passkey Provider on ",
|
||||
"enable": "Enable AliasVault as passkey provider",
|
||||
"description": "When enabled, AliasVault will handle passkey requests from websites. When a website requests a passkey, the AliasVault popup will be shown instead of the native browser or OS passkey interface."
|
||||
"passkeyProvider": "通行密钥提供程序",
|
||||
"passkeyProviderOn": "通行密钥提供程序于"
|
||||
}
|
||||
},
|
||||
"upgrade": {
|
||||
@@ -432,21 +429,10 @@
|
||||
"whatsNew": "新功能",
|
||||
"whatsNewDescription": "需要升级才能支持以下更改:",
|
||||
"noDescriptionAvailable": "此版本无可用说明。",
|
||||
"okay": "确定",
|
||||
"status": {
|
||||
"preparingUpgrade": "准备升级中…",
|
||||
"vaultAlreadyUpToDate": "当前密码库数据已是最新",
|
||||
"startingDatabaseTransaction": "开始数据库事务…",
|
||||
"applyingDatabaseMigrations": "应用数据库迁移…",
|
||||
"applyingMigration": "应用迁移 {{current}} / {{total}}…",
|
||||
"committingChanges": "提交更改中…"
|
||||
},
|
||||
"alerts": {
|
||||
"error": "错误",
|
||||
"unableToGetVersionInfo": "无法获取版本信息。请重试。",
|
||||
"selfHostedServer": "自托管服务器",
|
||||
"selfHostedWarning": "如果您使用的是自托管服务器,请确保同时更新您的自托管实例,否则将无法登录网页客户端。",
|
||||
"cancel": "取消",
|
||||
"continueUpgrade": "继续升级",
|
||||
"upgradeFailed": "升级失败",
|
||||
"failedToApplyMigration": "应用迁移失败({{current}} / {{total}})"
|
||||
|
||||
@@ -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.24.0';
|
||||
public static readonly VERSION = '0.26.0-alpha';
|
||||
|
||||
/**
|
||||
* The API version to send to the server (base semver without stage suffixes).
|
||||
|
||||
411
apps/browser-extension/src/utils/PinUnlockService.ts
Normal file
411
apps/browser-extension/src/utils/PinUnlockService.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
import argon2 from 'argon2-browser/dist/argon2-bundled.min.js';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* PinUnlockService - Handles PIN-based vault unlock
|
||||
*
|
||||
* This service allows users to set a 6-8 digit PIN to unlock their vault instead
|
||||
* of entering their full master password. The vault encryption key is encrypted
|
||||
* with a key derived from the PIN and stored locally.
|
||||
*
|
||||
* Security features:
|
||||
* - 4 failed attempts maximum before requiring full password
|
||||
* - PIN must be 6-8 digits
|
||||
* - Encryption key derived using Argon2id (memory-hard, GPU-resistant)
|
||||
* - Extension ID pepper adds friction for naive attacks
|
||||
* - Failed attempts counter stored separately
|
||||
* - Encrypted data automatically deleted after max failed attempts
|
||||
*
|
||||
* Security model
|
||||
* - Random salt: Stored locally (prevents rainbow tables)
|
||||
* - Extension ID pepper: Derived from browser.runtime.id
|
||||
* - Argon2id memory cost: 64 MB makes each attempt expensive
|
||||
* - Attempt limiting: 4 attempts max before PIN is disabled
|
||||
*
|
||||
* Recommendation: Use PIN unlock only on trusted devices. For high-security scenarios, always
|
||||
* use full master password unlock.
|
||||
*/
|
||||
|
||||
const PIN_ENABLED_KEY = 'local:aliasvault_pin_enabled';
|
||||
const PIN_ENCRYPTED_KEY_KEY = 'local:aliasvault_pin_encrypted_key';
|
||||
const PIN_SALT_KEY = 'local:aliasvault_pin_salt';
|
||||
const PIN_LENGTH_KEY = 'local:aliasvault_pin_length';
|
||||
const PIN_FAILED_ATTEMPTS_KEY = 'local:aliasvault_pin_failed_attempts';
|
||||
const MAX_PIN_ATTEMPTS = 4;
|
||||
|
||||
/**
|
||||
* Error thrown when PIN is locked after too many failed attempts.
|
||||
* Translation key: settings.unlockMethod.pinLocked
|
||||
*/
|
||||
export class PinLockedError extends Error {
|
||||
/**
|
||||
* Creates a new instance of PinLockedError.
|
||||
*/
|
||||
public constructor() {
|
||||
super('PIN locked after too many failed attempts');
|
||||
this.name = 'PinLockedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when PIN format is invalid.
|
||||
* Translation key: settings.unlockMethod.invalidPinFormat
|
||||
*/
|
||||
export class InvalidPinFormatError extends Error {
|
||||
/**
|
||||
* Creates a new instance of InvalidPinFormatError.
|
||||
*/
|
||||
public constructor() {
|
||||
super('Invalid PIN format');
|
||||
this.name = 'InvalidPinFormatError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when PIN is incorrect.
|
||||
* Includes remaining attempts count.
|
||||
* Translation key: settings.unlockMethod.incorrectPin
|
||||
*/
|
||||
export class IncorrectPinError extends Error {
|
||||
public readonly attemptsRemaining: number;
|
||||
|
||||
/**
|
||||
* Creates a new instance of IncorrectPinError.
|
||||
* @param attemptsRemaining - Number of attempts remaining
|
||||
*/
|
||||
public constructor(attemptsRemaining: number) {
|
||||
super(`Incorrect PIN. ${attemptsRemaining} attempts remaining.`);
|
||||
this.name = 'IncorrectPinError';
|
||||
this.attemptsRemaining = attemptsRemaining;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when encryption key is not available for PIN setup.
|
||||
*/
|
||||
export class EncryptionKeyNotAvailableError extends Error {
|
||||
/**
|
||||
* Creates a new instance of EncryptionKeyNotAvailableError.
|
||||
*/
|
||||
public constructor() {
|
||||
super('Encryption key not available');
|
||||
this.name = 'EncryptionKeyNotAvailableError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if PIN unlock is enabled
|
||||
*/
|
||||
export async function isPinEnabled(): Promise<boolean> {
|
||||
try {
|
||||
const result = await storage.getItem(PIN_ENABLED_KEY) as boolean | null;
|
||||
return result === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of the configured PIN
|
||||
*/
|
||||
export async function getPinLength(): Promise<number | null> {
|
||||
try {
|
||||
const result = await storage.getItem(PIN_LENGTH_KEY) as number | null;
|
||||
return result || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate PIN format (6-8 digits)
|
||||
*/
|
||||
export function isValidPin(pin: string): boolean {
|
||||
const pinRegex = /^\d{6,8}$/;
|
||||
return pinRegex.test(pin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get failed attempts count
|
||||
*/
|
||||
export async function getFailedAttempts(): Promise<number> {
|
||||
try {
|
||||
const result = await storage.getItem(PIN_FAILED_ATTEMPTS_KEY) as number | null;
|
||||
return result || 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if PIN attempts are exhausted
|
||||
*/
|
||||
export async function isPinLocked(): Promise<boolean> {
|
||||
const attempts = await getFailedAttempts();
|
||||
return attempts >= MAX_PIN_ATTEMPTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup PIN unlock
|
||||
* Encrypts the vault encryption key with the PIN and stores it
|
||||
*
|
||||
* @param pin - The PIN to set (6-8 digits)
|
||||
* @param vaultEncryptionKey - The base64-encoded vault encryption key to protect
|
||||
*/
|
||||
export async function setupPin(pin: string, vaultEncryptionKey: string): Promise<void> {
|
||||
if (!isValidPin(pin)) {
|
||||
throw new InvalidPinFormatError();
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate random salt
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const saltBase64 = arrayBufferToBase64(salt.buffer);
|
||||
|
||||
// Derive key from PIN using Argon2id
|
||||
const combinedSalt = await assembleSaltWithPepper(salt);
|
||||
const pinKey = await derivePinKey(pin, combinedSalt);
|
||||
|
||||
// Encrypt the vault encryption key
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encryptedKey = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
pinKey,
|
||||
new TextEncoder().encode(vaultEncryptionKey)
|
||||
);
|
||||
|
||||
// Combine IV + encrypted data
|
||||
const combined = new Uint8Array(iv.length + encryptedKey.byteLength);
|
||||
combined.set(iv, 0);
|
||||
combined.set(new Uint8Array(encryptedKey), iv.length);
|
||||
const encryptedKeyBase64 = arrayBufferToBase64(combined.buffer);
|
||||
|
||||
/* Store encrypted key, salt, PIN length, and enable flag */
|
||||
await Promise.all([
|
||||
storage.setItem(PIN_ENABLED_KEY, true),
|
||||
storage.setItem(PIN_ENCRYPTED_KEY_KEY, encryptedKeyBase64),
|
||||
storage.setItem(PIN_SALT_KEY, saltBase64),
|
||||
storage.setItem(PIN_LENGTH_KEY, pin.length),
|
||||
storage.setItem(PIN_FAILED_ATTEMPTS_KEY, 0)
|
||||
]);
|
||||
|
||||
} catch (error: unknown) {
|
||||
/* Re-throw custom errors as-is */
|
||||
if (error instanceof InvalidPinFormatError) {
|
||||
throw error;
|
||||
}
|
||||
/* Log internal errors and throw generic error for user */
|
||||
console.error('[PinUnlockService] Failed to setup PIN:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock with PIN
|
||||
* Returns the decrypted vault encryption key
|
||||
*
|
||||
* @param pin - The PIN to use for unlocking
|
||||
* @returns The decrypted vault encryption key (base64)
|
||||
*/
|
||||
export async function unlockWithPin(pin: string): Promise<string> {
|
||||
if (!isValidPin(pin)) {
|
||||
throw new InvalidPinFormatError();
|
||||
}
|
||||
|
||||
/* Check if locked due to too many attempts */
|
||||
if (await isPinLocked()) {
|
||||
throw new PinLockedError();
|
||||
}
|
||||
|
||||
try {
|
||||
/* Get stored data */
|
||||
const [encryptedKeyBase64, saltBase64] = await Promise.all([
|
||||
storage.getItem(PIN_ENCRYPTED_KEY_KEY) as Promise<string | null>,
|
||||
storage.getItem(PIN_SALT_KEY) as Promise<string | null>
|
||||
]);
|
||||
|
||||
if (!encryptedKeyBase64 || !saltBase64) {
|
||||
throw new PinLockedError();
|
||||
}
|
||||
|
||||
// Decode encrypted package
|
||||
const combined = new Uint8Array(base64ToArrayBuffer(encryptedKeyBase64));
|
||||
const iv = combined.slice(0, 12);
|
||||
const encryptedData = combined.slice(12);
|
||||
|
||||
// Derive key from PIN with extension ID pepper
|
||||
const salt = new Uint8Array(base64ToArrayBuffer(saltBase64));
|
||||
const combinedSalt = await assembleSaltWithPepper(salt);
|
||||
const pinKey = await derivePinKey(pin, combinedSalt);
|
||||
|
||||
// Decrypt the vault encryption key
|
||||
const decryptedData = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
pinKey,
|
||||
encryptedData
|
||||
);
|
||||
|
||||
const vaultEncryptionKey = new TextDecoder().decode(decryptedData);
|
||||
|
||||
/* Reset failed attempts on success */
|
||||
await storage.setItem(PIN_FAILED_ATTEMPTS_KEY, 0);
|
||||
|
||||
return vaultEncryptionKey;
|
||||
} catch {
|
||||
/* Increment failed attempts */
|
||||
const currentAttempts = await getFailedAttempts();
|
||||
const newAttempts = currentAttempts + 1;
|
||||
await storage.setItem(PIN_FAILED_ATTEMPTS_KEY, newAttempts);
|
||||
|
||||
/*
|
||||
* If max attempts reached, disable PIN and clear ALL stored data for security.
|
||||
* This prevents offline brute-force attacks on the encrypted key.
|
||||
*/
|
||||
if (newAttempts >= MAX_PIN_ATTEMPTS) {
|
||||
await removeAndDisablePin();
|
||||
throw new PinLockedError();
|
||||
}
|
||||
|
||||
throw new IncorrectPinError(MAX_PIN_ATTEMPTS - newAttempts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable PIN unlock and remove all stored (encrypted) data.
|
||||
*/
|
||||
export async function removeAndDisablePin(): Promise<void> {
|
||||
try {
|
||||
await Promise.all([
|
||||
storage.removeItem(PIN_ENABLED_KEY),
|
||||
storage.removeItem(PIN_ENCRYPTED_KEY_KEY),
|
||||
storage.removeItem(PIN_SALT_KEY),
|
||||
storage.removeItem(PIN_LENGTH_KEY),
|
||||
storage.removeItem(PIN_FAILED_ATTEMPTS_KEY)
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('[PinUnlockService] Failed to disable PIN:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset failed attempts counter
|
||||
* Called after successful password unlock
|
||||
*/
|
||||
export async function resetFailedAttempts(): Promise<void> {
|
||||
try {
|
||||
await storage.setItem(PIN_FAILED_ATTEMPTS_KEY, 0);
|
||||
} catch (error) {
|
||||
console.error('[PinUnlockService] Failed to reset failed attempts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extension ID pepper component
|
||||
*
|
||||
* This provides a device-bound value that is NOT stored in chrome.storage.
|
||||
* The extension ID is unique per installation and accessible via browser.runtime.id.
|
||||
*
|
||||
* Security benefits:
|
||||
* - Not stored in chrome.storage directly (however still stored elsewhere on filesystem)
|
||||
* - Adds friction for attackers who only copy storage directory
|
||||
* - Unique per extension installation
|
||||
*
|
||||
* @returns SHA-256 hash of extension ID as Uint8Array
|
||||
*/
|
||||
async function getExtensionPepper(): Promise<Uint8Array> {
|
||||
const extensionId = browser.runtime.id;
|
||||
const pepperSource = new TextEncoder().encode(extensionId);
|
||||
const pepperHash = await crypto.subtle.digest('SHA-256', pepperSource);
|
||||
return new Uint8Array(pepperHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine random salt with extension ID pepper
|
||||
*
|
||||
* Creates a composite salt that includes both:
|
||||
* 1. Random salt (stored locally, prevents rainbow tables)
|
||||
* 2. Extension ID pepper (not stored, prevents offline brute-force)
|
||||
*
|
||||
* @param randomSalt - The random salt stored in chrome.storage
|
||||
* @returns Combined salt for Argon2id key derivation
|
||||
*/
|
||||
async function assembleSaltWithPepper(randomSalt: Uint8Array): Promise<Uint8Array> {
|
||||
const pepper = await getExtensionPepper();
|
||||
|
||||
// Combine: random_salt || extension_id_pepper
|
||||
const combinedSalt = new Uint8Array(randomSalt.length + pepper.length);
|
||||
combinedSalt.set(randomSalt, 0);
|
||||
combinedSalt.set(pepper, randomSalt.length);
|
||||
|
||||
return combinedSalt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive encryption key from PIN using Argon2id
|
||||
*
|
||||
* Uses Argon2id with high memory cost (64 MB) to make brute-force attacks
|
||||
* significantly more expensive. This is especially important for PINs which
|
||||
* have lower entropy than passwords.
|
||||
*
|
||||
* The salt parameter should be the COMBINED salt (random salt + extension pepper)
|
||||
* created by assembleSaltWithPepper().
|
||||
*
|
||||
* Parameters chosen for security:
|
||||
* - Memory: 65536 KB (64 MB) - makes GPU attacks much harder
|
||||
* - Iterations: 3 - standard for Argon2id
|
||||
* - Parallelism: 1 - suitable for browser environment
|
||||
* - Output: 32 bytes for AES-256-GCM
|
||||
*/
|
||||
async function derivePinKey(pin: string, salt: Uint8Array): Promise<CryptoKey> {
|
||||
// Convert salt to base64 string (required by argon2-browser)
|
||||
const saltBase64 = arrayBufferToBase64(salt.buffer as ArrayBuffer);
|
||||
|
||||
// Derive key using Argon2id
|
||||
const hash = await argon2.hash({
|
||||
pass: pin,
|
||||
salt: saltBase64,
|
||||
time: 3,
|
||||
mem: 65536,
|
||||
parallelism: 1,
|
||||
hashLen: 32,
|
||||
type: 2,
|
||||
});
|
||||
|
||||
// Import the derived key into WebCrypto API
|
||||
const pinKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
hash.hash,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
return pinKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ArrayBuffer to base64 string
|
||||
*/
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert base64 string to ArrayBuffer
|
||||
*/
|
||||
function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersion
|
||||
|
||||
import { t } from '@/i18n/StandaloneI18n';
|
||||
|
||||
import { storage } from '#imports';
|
||||
|
||||
/**
|
||||
* Placeholder base64 image for credentials without a logo.
|
||||
*/
|
||||
@@ -381,18 +383,24 @@ export class SqliteClient {
|
||||
* Get the default email domain from the database.
|
||||
* @param privateEmailDomains - Array of private email domains
|
||||
* @param publicEmailDomains - Array of public email domains
|
||||
* @param hiddenPrivateEmailDomains - Array of hidden private email domains (optional)
|
||||
* @returns The default email domain or null if no valid domain is found
|
||||
*/
|
||||
public getDefaultEmailDomain(privateEmailDomains: string[], publicEmailDomains: string[]): string | null {
|
||||
public async getDefaultEmailDomain(): Promise<string | null> {
|
||||
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[] ?? [];
|
||||
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[] ?? [];
|
||||
const hiddenPrivateEmailDomains = await storage.getItem('session:hiddenPrivateEmailDomains') as string[] ?? [];
|
||||
|
||||
const defaultEmailDomain = this.getSetting('DefaultEmailDomain');
|
||||
|
||||
/**
|
||||
* Check if a domain is valid.
|
||||
* Check if a domain is valid (not disabled, not hidden, and exists in domain lists).
|
||||
*/
|
||||
const isValidDomain = (domain: string): boolean => {
|
||||
return Boolean(domain &&
|
||||
domain !== 'DISABLED.TLD' &&
|
||||
domain !== '' &&
|
||||
!hiddenPrivateEmailDomains.includes(domain) &&
|
||||
(privateEmailDomains.includes(domain) || publicEmailDomains.includes(domain)));
|
||||
};
|
||||
|
||||
@@ -401,7 +409,7 @@ export class SqliteClient {
|
||||
return defaultEmailDomain;
|
||||
}
|
||||
|
||||
// If default domain is not valid, fall back to first available private domain.
|
||||
// If default domain is not valid, fall back to first available private domain (excluding hidden ones).
|
||||
const firstPrivate = privateEmailDomains.find(isValidDomain);
|
||||
if (firstPrivate) {
|
||||
return firstPrivate;
|
||||
@@ -419,9 +427,36 @@ export class SqliteClient {
|
||||
|
||||
/**
|
||||
* Get the default identity language from the database.
|
||||
* Returns the stored override value if set, otherwise returns empty string to indicate no explicit preference.
|
||||
* Use getEffectiveIdentityLanguage() to get the language with smart defaults based on UI language.
|
||||
*/
|
||||
public getDefaultIdentityLanguage(): string {
|
||||
return this.getSetting('DefaultIdentityLanguage', 'en');
|
||||
return this.getSetting('DefaultIdentityLanguage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective identity generator language to use.
|
||||
* If user has explicitly set a language preference, use that.
|
||||
* Otherwise, intelligently match the UI language to an available identity generator language.
|
||||
* Falls back to "en" if no match is found.
|
||||
*/
|
||||
public async getEffectiveIdentityLanguage(): Promise<string> {
|
||||
const explicitLanguage = this.getDefaultIdentityLanguage();
|
||||
|
||||
// If user has explicitly set a language preference, use it
|
||||
if (explicitLanguage) {
|
||||
return explicitLanguage;
|
||||
}
|
||||
|
||||
// Otherwise, try to match UI language to an identity generator language
|
||||
const { mapUiLanguageToIdentityLanguage } = await import('@/utils/dist/shared/identity-generator');
|
||||
const { default: i18n } = await import('@/i18n/i18n');
|
||||
|
||||
const uiLanguage = i18n.language;
|
||||
const mappedLanguage = mapUiLanguageToIdentityLanguage(uiLanguage);
|
||||
|
||||
// Return the mapped language, or fall back to "en" if no match found
|
||||
return mappedLanguage ?? 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,6 +466,13 @@ export class SqliteClient {
|
||||
return this.getSetting('DefaultIdentityGender', 'random');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default identity age range from the database.
|
||||
*/
|
||||
public getDefaultIdentityAgeRange(): string {
|
||||
return this.getSetting('DefaultIdentityAgeRange', 'random');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the password settings from the database.
|
||||
*/
|
||||
@@ -464,7 +506,7 @@ export class SqliteClient {
|
||||
* @param attachments The attachments to insert
|
||||
* @returns The ID of the created credential
|
||||
*/
|
||||
public async createCredential(credential: Credential, attachments: Attachment[]): Promise<string> {
|
||||
public async createCredential(credential: Credential, attachments: Attachment[], totpCodes: TotpCode[] = []): Promise<string> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
@@ -575,6 +617,30 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Insert TOTP codes
|
||||
if (totpCodes) {
|
||||
for (const totpCode of totpCodes) {
|
||||
// Skip deleted codes
|
||||
if (totpCode.IsDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const totpCodeQuery = `
|
||||
INSERT INTO TotpCodes (Id, Name, SecretKey, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
this.executeUpdate(totpCodeQuery, [
|
||||
totpCode.Id || crypto.randomUUID().toUpperCase(),
|
||||
totpCode.Name,
|
||||
totpCode.SecretKey,
|
||||
credentialId,
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
await this.commitTransaction();
|
||||
return credentialId;
|
||||
|
||||
@@ -815,7 +881,7 @@ export class SqliteClient {
|
||||
* @param attachments The attachments to update
|
||||
* @returns The number of rows modified
|
||||
*/
|
||||
public async updateCredentialById(credential: Credential, originalAttachmentIds: string[], attachments: Attachment[]): Promise<number> {
|
||||
public async updateCredentialById(credential: Credential, originalAttachmentIds: string[], attachments: Attachment[], originalTotpCodeIds: string[] = [], totpCodes: TotpCode[] = []): Promise<number> {
|
||||
if (!this.db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
@@ -996,6 +1062,76 @@ export class SqliteClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Handle TOTP codes
|
||||
if (totpCodes) {
|
||||
// Get current TOTP code IDs (excluding deleted ones)
|
||||
const currentTotpCodeIds = totpCodes
|
||||
.filter(tc => !tc.IsDeleted)
|
||||
.map(tc => tc.Id);
|
||||
|
||||
// Mark TOTP codes as deleted that were removed
|
||||
const totpCodesToDelete = originalTotpCodeIds.filter(id => !currentTotpCodeIds.includes(id));
|
||||
for (const totpCodeId of totpCodesToDelete) {
|
||||
const deleteQuery = `
|
||||
UPDATE TotpCodes
|
||||
SET IsDeleted = 1,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
this.executeUpdate(deleteQuery, [currentDateTime, totpCodeId]);
|
||||
}
|
||||
|
||||
// Handle TOTP codes marked for deletion in the array
|
||||
const markedForDeletion = totpCodes.filter(tc => tc.IsDeleted && originalTotpCodeIds.includes(tc.Id));
|
||||
for (const totpCode of markedForDeletion) {
|
||||
const deleteQuery = `
|
||||
UPDATE TotpCodes
|
||||
SET IsDeleted = 1,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
this.executeUpdate(deleteQuery, [currentDateTime, totpCode.Id]);
|
||||
}
|
||||
|
||||
// Process each TOTP code
|
||||
for (const totpCode of totpCodes) {
|
||||
// Skip deleted codes
|
||||
if (totpCode.IsDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isExistingTotpCode = originalTotpCodeIds.includes(totpCode.Id);
|
||||
|
||||
if (!isExistingTotpCode) {
|
||||
// Insert new TOTP code
|
||||
const insertQuery = `
|
||||
INSERT INTO TotpCodes (Id, Name, SecretKey, CredentialId, CreatedAt, UpdatedAt, IsDeleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
||||
this.executeUpdate(insertQuery, [
|
||||
totpCode.Id || crypto.randomUUID().toUpperCase(),
|
||||
totpCode.Name,
|
||||
totpCode.SecretKey,
|
||||
credential.Id,
|
||||
currentDateTime,
|
||||
currentDateTime,
|
||||
0
|
||||
]);
|
||||
} else {
|
||||
// Update existing TOTP code
|
||||
const updateQuery = `
|
||||
UPDATE TotpCodes
|
||||
SET Name = ?,
|
||||
SecretKey = ?,
|
||||
UpdatedAt = ?
|
||||
WHERE Id = ?`;
|
||||
this.executeUpdate(updateQuery, [
|
||||
totpCode.Name,
|
||||
totpCode.SecretKey,
|
||||
currentDateTime,
|
||||
totpCode.Id
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.commitTransaction();
|
||||
return 1;
|
||||
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
import { CombinedStopWords } from '@/utils/formDetector/FieldPatterns';
|
||||
|
||||
/**
|
||||
* Credential filtering for browser extension autofill.
|
||||
* This implementation follows the unified filtering algorithm specification defined in
|
||||
* docs/CREDENTIAL_FILTERING_SPEC.md for cross-platform consistency with Android and iOS.
|
||||
*
|
||||
* Algorithm Structure (Priority Order with Early Returns):
|
||||
* 1. PRIORITY 1: App Package Name Exact Match (included for consistency, not used in browser)
|
||||
* 2. PRIORITY 2: URL Domain Matching (exact, subdomain, root domain)
|
||||
* 3. PRIORITY 3: Service Name Fallback (only for credentials without URLs - anti-phishing)
|
||||
* 4. PRIORITY 4: Text/Page Title Matching (non-URL search)
|
||||
*/
|
||||
|
||||
export enum AutofillMatchingMode {
|
||||
DEFAULT = 'default',
|
||||
URL_EXACT = 'url_exact',
|
||||
URL_SUBDOMAIN = 'url_subdomain'
|
||||
}
|
||||
|
||||
type CredentialWithPriority = Credential & {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common top-level domains (TLDs) used for app package name detection.
|
||||
* When a search string starts with one of these TLDs followed by a dot (e.g., "com.coolblue.app"),
|
||||
* it's identified as a reversed domain name (app package name) rather than a regular URL.
|
||||
* Note: This is included for cross-platform test consistency but not actively used in browser context.
|
||||
*/
|
||||
const COMMON_TLDS = new Set([
|
||||
// Generic TLDs
|
||||
'com', 'net', 'org', 'edu', 'gov', 'mil', 'int',
|
||||
// Country code TLDs
|
||||
'nl', 'de', 'uk', 'fr', 'it', 'es', 'pl', 'be', 'ch', 'at', 'se', 'no', 'dk', 'fi',
|
||||
'pt', 'gr', 'cz', 'hu', 'ro', 'bg', 'hr', 'sk', 'si', 'lt', 'lv', 'ee', 'ie', 'lu',
|
||||
'us', 'ca', 'mx', 'br', 'ar', 'cl', 'co', 've', 'pe', 'ec',
|
||||
'au', 'nz', 'jp', 'cn', 'in', 'kr', 'tw', 'hk', 'sg', 'my', 'th', 'id', 'ph', 'vn',
|
||||
'za', 'eg', 'ng', 'ke', 'ug', 'tz', 'ma',
|
||||
'ru', 'ua', 'by', 'kz', 'il', 'tr', 'sa', 'ae', 'qa', 'kw',
|
||||
// New gTLDs (common ones)
|
||||
'app', 'dev', 'io', 'ai', 'tech', 'shop', 'store', 'online', 'site', 'website',
|
||||
'blog', 'news', 'media', 'tv', 'video', 'music', 'pro', 'info', 'biz', 'name'
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check if a string is likely an app package name (reversed domain).
|
||||
* Package names start with TLD followed by dot (e.g., "com.example", "nl.app").
|
||||
* @param text - Text to check
|
||||
* @returns True if it looks like an app package name
|
||||
*/
|
||||
function isAppPackageName(text: string): boolean {
|
||||
// Must contain a dot
|
||||
if (!text.includes('.')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must not have protocol
|
||||
if (text.startsWith('http://') || text.startsWith('https://')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract first part before first dot
|
||||
const firstPart = text.split('.')[0].toLowerCase();
|
||||
|
||||
// Check if first part is a common TLD - indicates reversed domain (package name)
|
||||
return COMMON_TLDS.has(firstPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domain from URL, handling both full URLs and partial domains
|
||||
* @param url - URL or domain string
|
||||
* @returns Normalized domain without protocol or www, or empty string if not a valid URL/domain
|
||||
*/
|
||||
export function extractDomain(url: string): string {
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let domain = url.toLowerCase().trim();
|
||||
|
||||
// Check if it has a protocol
|
||||
const hasProtocol = domain.startsWith('http://') || domain.startsWith('https://');
|
||||
|
||||
/*
|
||||
* If no protocol and starts with TLD + dot, it's likely an app package name
|
||||
* Return empty string to indicate that domain extraction has failed for this string
|
||||
*/
|
||||
if (!hasProtocol && isAppPackageName(domain)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove protocol if present
|
||||
domain = domain.replace(/^https?:\/\//, '');
|
||||
|
||||
// Remove www. prefix
|
||||
domain = domain.replace(/^www\./, '');
|
||||
|
||||
// Remove path, query, and fragment
|
||||
domain = domain.split('/')[0];
|
||||
domain = domain.split('?')[0];
|
||||
domain = domain.split('#')[0];
|
||||
|
||||
// Basic domain validation - must contain at least one dot and valid characters
|
||||
if (!domain.includes('.') || !/^[a-z0-9.-]+$/.test(domain)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Ensure valid domain structure
|
||||
if (domain.startsWith('.') || domain.endsWith('.') || domain.includes('..')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract root domain from a domain string.
|
||||
* E.g., "sub.example.com" -> "example.com"
|
||||
* E.g., "sub.example.com.au" -> "example.com.au"
|
||||
* E.g., "sub.example.co.uk" -> "example.co.uk"
|
||||
*/
|
||||
export function extractRootDomain(domain: string): string {
|
||||
const parts = domain.split('.');
|
||||
if (parts.length < 2) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
// Common two-level public TLDs
|
||||
const twoLevelTlds = new Set([
|
||||
// Australia
|
||||
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
|
||||
// United Kingdom
|
||||
'co.uk', 'org.uk', 'net.uk', 'ac.uk', 'gov.uk', 'plc.uk', 'ltd.uk', 'me.uk',
|
||||
// Canada
|
||||
'co.ca', 'net.ca', 'org.ca', 'gc.ca', 'ab.ca', 'bc.ca', 'mb.ca', 'nb.ca', 'nf.ca', 'nl.ca', 'ns.ca', 'nt.ca', 'nu.ca',
|
||||
'on.ca', 'pe.ca', 'qc.ca', 'sk.ca', 'yk.ca',
|
||||
// India
|
||||
'co.in', 'net.in', 'org.in', 'edu.in', 'gov.in', 'ac.in', 'res.in', 'gen.in', 'firm.in', 'ind.in',
|
||||
// Japan
|
||||
'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'ad.jp', 'ed.jp', 'go.jp', 'gr.jp', 'lg.jp',
|
||||
// South Africa
|
||||
'co.za', 'net.za', 'org.za', 'edu.za', 'gov.za', 'ac.za', 'web.za',
|
||||
// New Zealand
|
||||
'co.nz', 'net.nz', 'org.nz', 'edu.nz', 'govt.nz', 'ac.nz', 'geek.nz', 'gen.nz', 'kiwi.nz', 'maori.nz', 'mil.nz', 'school.nz',
|
||||
// Brazil
|
||||
'com.br', 'net.br', 'org.br', 'edu.br', 'gov.br', 'mil.br', 'art.br', 'etc.br', 'adv.br', 'arq.br', 'bio.br', 'cim.br',
|
||||
'cng.br', 'cnt.br', 'ecn.br', 'eng.br', 'esp.br', 'eti.br', 'far.br', 'fnd.br', 'fot.br', 'fst.br', 'g12.br', 'geo.br',
|
||||
'ggf.br', 'jor.br', 'lel.br', 'mat.br', 'med.br', 'mus.br', 'not.br', 'ntr.br', 'odo.br', 'ppg.br', 'pro.br', 'psc.br',
|
||||
'psi.br', 'qsl.br', 'rec.br', 'slg.br', 'srv.br', 'tmp.br', 'trd.br', 'tur.br', 'tv.br', 'vet.br', 'zlg.br',
|
||||
// Russia
|
||||
'com.ru', 'net.ru', 'org.ru', 'edu.ru', 'gov.ru', 'int.ru', 'mil.ru', 'spb.ru', 'msk.ru',
|
||||
// China
|
||||
'com.cn', 'net.cn', 'org.cn', 'edu.cn', 'gov.cn', 'mil.cn', 'ac.cn', 'ah.cn', 'bj.cn', 'cq.cn', 'fj.cn', 'gd.cn', 'gs.cn',
|
||||
'gz.cn', 'gx.cn', 'ha.cn', 'hb.cn', 'he.cn', 'hi.cn', 'hk.cn', 'hl.cn', 'hn.cn', 'jl.cn', 'js.cn', 'jx.cn', 'ln.cn', 'mo.cn',
|
||||
'nm.cn', 'nx.cn', 'qh.cn', 'sc.cn', 'sd.cn', 'sh.cn', 'sn.cn', 'sx.cn', 'tj.cn', 'tw.cn', 'xj.cn', 'xz.cn', 'yn.cn', 'zj.cn',
|
||||
// Mexico
|
||||
'com.mx', 'net.mx', 'org.mx', 'edu.mx', 'gob.mx',
|
||||
// Argentina
|
||||
'com.ar', 'net.ar', 'org.ar', 'edu.ar', 'gov.ar', 'mil.ar', 'int.ar',
|
||||
// Chile
|
||||
'com.cl', 'net.cl', 'org.cl', 'edu.cl', 'gov.cl', 'mil.cl',
|
||||
// Colombia
|
||||
'com.co', 'net.co', 'org.co', 'edu.co', 'gov.co', 'mil.co', 'nom.co',
|
||||
// Venezuela
|
||||
'com.ve', 'net.ve', 'org.ve', 'edu.ve', 'gov.ve', 'mil.ve', 'web.ve',
|
||||
// Peru
|
||||
'com.pe', 'net.pe', 'org.pe', 'edu.pe', 'gob.pe', 'mil.pe', 'nom.pe',
|
||||
// Ecuador
|
||||
'com.ec', 'net.ec', 'org.ec', 'edu.ec', 'gov.ec', 'mil.ec', 'med.ec', 'fin.ec', 'pro.ec', 'info.ec',
|
||||
// Europe
|
||||
'co.at', 'or.at', 'ac.at', 'gv.at', 'priv.at',
|
||||
'co.be', 'ac.be',
|
||||
'co.dk', 'ac.dk',
|
||||
'co.il', 'net.il', 'org.il', 'ac.il', 'gov.il', 'idf.il', 'k12.il', 'muni.il',
|
||||
'co.no', 'ac.no', 'priv.no',
|
||||
'co.pl', 'net.pl', 'org.pl', 'edu.pl', 'gov.pl', 'mil.pl', 'nom.pl', 'com.pl',
|
||||
'co.th', 'net.th', 'org.th', 'edu.th', 'gov.th', 'mil.th', 'ac.th', 'in.th',
|
||||
'co.kr', 'net.kr', 'org.kr', 'edu.kr', 'gov.kr', 'mil.kr', 'ac.kr', 'go.kr', 'ne.kr', 'or.kr', 'pe.kr', 're.kr', 'seoul.kr',
|
||||
'kyonggi.kr',
|
||||
// Others
|
||||
'co.id', 'net.id', 'org.id', 'edu.id', 'gov.id', 'mil.id', 'web.id', 'ac.id', 'sch.id',
|
||||
'co.ma', 'net.ma', 'org.ma', 'edu.ma', 'gov.ma', 'ac.ma', 'press.ma',
|
||||
'co.ke', 'net.ke', 'org.ke', 'edu.ke', 'gov.ke', 'ac.ke', 'go.ke', 'info.ke', 'me.ke', 'mobi.ke', 'sc.ke',
|
||||
'co.ug', 'net.ug', 'org.ug', 'edu.ug', 'gov.ug', 'ac.ug', 'sc.ug', 'go.ug', 'ne.ug', 'or.ug',
|
||||
'co.tz', 'net.tz', 'org.tz', 'edu.tz', 'gov.tz', 'ac.tz', 'go.tz', 'hotel.tz', 'info.tz', 'me.tz', 'mil.tz', 'mobi.tz',
|
||||
'ne.tz', 'or.tz', 'sc.tz', 'tv.tz',
|
||||
]);
|
||||
|
||||
// Check if the last two parts form a known two-level TLD
|
||||
if (parts.length >= 3) {
|
||||
const lastTwoParts = parts.slice(-2).join('.');
|
||||
if (twoLevelTlds.has(lastTwoParts)) {
|
||||
// Take the last three parts for two-level TLDs
|
||||
return parts.slice(-3).join('.');
|
||||
}
|
||||
}
|
||||
|
||||
// Default to last two parts for regular TLDs
|
||||
return parts.length >= 2 ? parts.slice(-2).join('.') : domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two domains match, supporting partial matches
|
||||
* Note: Both parameters should be pre-extracted domains (without protocol, www, path, etc.)
|
||||
* @param domain1 - First domain (pre-extracted)
|
||||
* @param domain2 - Second domain (pre-extracted)
|
||||
* @returns True if domains match (including partial matches)
|
||||
*/
|
||||
function domainsMatch(domain1: string, domain2: string): boolean {
|
||||
if (!domain1 || !domain2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if (domain1 === domain2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if one domain contains the other (for subdomain matching)
|
||||
if (domain1.includes(domain2) || domain2.includes(domain1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check root domain match
|
||||
const d1Root = extractRootDomain(domain1);
|
||||
const d2Root = extractRootDomain(domain2);
|
||||
|
||||
return d1Root === d2Root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract meaningful words from text, removing punctuation and filtering stop words
|
||||
* @param text - Text to extract words from
|
||||
* @returns Array of filtered words
|
||||
*/
|
||||
function extractWords(text: string): string[] {
|
||||
if (!text || text.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return text.toLowerCase()
|
||||
// Replace common separators and punctuation with spaces (including dots)
|
||||
.replace(/[|,;:\-–—/\\()[\]{}'"`~!@#$%^&*+=<>?.]/g, ' ')
|
||||
// Split on whitespace and filter
|
||||
.split(/\s+/)
|
||||
.filter(word =>
|
||||
word.length > 3 &&
|
||||
!CombinedStopWords.has(word)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter credentials based on current URL and page context with anti-phishing protection.
|
||||
*
|
||||
* This method follows a strict priority-based algorithm with early returns:
|
||||
* 1. PRIORITY 1: App Package Name Exact Match (highest priority, included for consistency)
|
||||
* 2. PRIORITY 2: URL Domain Matching
|
||||
* 3. PRIORITY 3: Service Name Fallback (anti-phishing protection)
|
||||
* 4. PRIORITY 4: Text/Page Title Matching (lowest priority)
|
||||
*
|
||||
* @param credentials - List of credentials to filter
|
||||
* @param currentUrl - Current page URL
|
||||
* @param pageTitle - Current page title
|
||||
* @param matchingMode - Matching mode (controls subdomain and fallback behavior)
|
||||
* @returns Filtered list of credentials (max 3)
|
||||
*
|
||||
* **Security Note**: Priority 3 only searches credentials with no service URL defined.
|
||||
* This prevents phishing attacks where a malicious site might match credentials
|
||||
* intended for a legitimate site.
|
||||
*/
|
||||
export function filterCredentials(credentials: Credential[], currentUrl: string, pageTitle: string, matchingMode: AutofillMatchingMode = AutofillMatchingMode.DEFAULT): Credential[] {
|
||||
// Early return for empty URL
|
||||
if (!currentUrl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* PRIORITY 1: App Package Name Exact Match
|
||||
* Check if current URL is an app package name (e.g., com.coolblue.app)
|
||||
* Note: Not used in browser context but included for cross-platform test consistency
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
const isPackageName = isAppPackageName(currentUrl);
|
||||
if (isPackageName) {
|
||||
// Perform exact string match on ServiceUrl field
|
||||
const packageMatches = credentials.filter(cred =>
|
||||
cred.ServiceUrl && cred.ServiceUrl.length > 0 && currentUrl === cred.ServiceUrl
|
||||
);
|
||||
|
||||
// EARLY RETURN if matches found
|
||||
if (packageMatches.length > 0) {
|
||||
return packageMatches.slice(0, 3);
|
||||
}
|
||||
/*
|
||||
* If no matches found, skip URL matching and go directly to text matching (Priority 4)
|
||||
* Package names shouldn't be treated as URLs
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* PRIORITY 2: URL Domain Matching
|
||||
* Try to extract domain from current URL (skip if package name)
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
if (!isPackageName) {
|
||||
const currentDomain = extractDomain(currentUrl);
|
||||
|
||||
if (currentDomain) {
|
||||
const filtered: CredentialWithPriority[] = [];
|
||||
|
||||
// Determine matching features based on mode
|
||||
const enableExactMatch = matchingMode !== undefined;
|
||||
const enableSubdomainMatch = matchingMode === AutofillMatchingMode.DEFAULT || matchingMode === AutofillMatchingMode.URL_SUBDOMAIN;
|
||||
|
||||
// Process credentials with service URLs
|
||||
for (const cred of credentials) {
|
||||
if (!cred.ServiceUrl || cred.ServiceUrl.length === 0) {
|
||||
continue; // Handle these in Priority 3
|
||||
}
|
||||
|
||||
const credDomain = extractDomain(cred.ServiceUrl);
|
||||
|
||||
// Check for exact match (priority 1)
|
||||
if (enableExactMatch && currentDomain === credDomain) {
|
||||
filtered.push({ ...cred, priority: 1 });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for subdomain/partial match (priority 2)
|
||||
if (enableSubdomainMatch && domainsMatch(currentDomain, credDomain)) {
|
||||
filtered.push({ ...cred, priority: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
// EARLY RETURN if matches found
|
||||
if (filtered.length > 0) {
|
||||
const uniqueCredentials = Array.from(
|
||||
new Map(
|
||||
filtered
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.map(cred => [cred.Id, cred])
|
||||
).values()
|
||||
);
|
||||
return uniqueCredentials.slice(0, 3);
|
||||
}
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════
|
||||
* PRIORITY 3: Page Title / Service Name Fallback (Anti-Phishing Protection)
|
||||
* No domain matches found - search in service names using page title
|
||||
* CRITICAL: Only search credentials with NO service URL defined
|
||||
* ═══════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
if (pageTitle) {
|
||||
const titleWords = extractWords(pageTitle);
|
||||
|
||||
if (titleWords.length > 0) {
|
||||
const nameMatches: Credential[] = [];
|
||||
|
||||
for (const cred of credentials) {
|
||||
// SECURITY: Skip credentials that have a URL defined
|
||||
if (cred.ServiceUrl && cred.ServiceUrl.length > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check page title match with service name
|
||||
if (cred.ServiceName) {
|
||||
const credNameWords = extractWords(cred.ServiceName);
|
||||
|
||||
/*
|
||||
* Match only complete words, not substrings
|
||||
* For example: "Express" should match "My Express Account" but not "AliExpress"
|
||||
*/
|
||||
const hasTitleMatch = titleWords.some(titleWord =>
|
||||
credNameWords.some(credWord => titleWord === credWord)
|
||||
);
|
||||
|
||||
if (hasTitleMatch) {
|
||||
nameMatches.push(cred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return matches from Priority 3 if any found
|
||||
if (nameMatches.length > 0) {
|
||||
return nameMatches.slice(0, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matches found in Priority 2 or Priority 3
|
||||
return [];
|
||||
}
|
||||
} // End of Priority 2 (!isPackageName)
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
* PRIORITY 4: Text Matching
|
||||
* Used when: 1) Package name didn't match in Priority 1, OR 2) URL extraction failed
|
||||
* Performs word-based matching on service names
|
||||
* ═══════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
const searchWords = extractWords(currentUrl);
|
||||
|
||||
if (searchWords.length > 0) {
|
||||
return credentials.filter(cred => {
|
||||
const serviceNameWords = cred.ServiceName ? extractWords(cred.ServiceName) : [];
|
||||
|
||||
// Check if any search word matches any service name word exactly
|
||||
return searchWords.some(searchWord =>
|
||||
serviceNameWords.includes(searchWord)
|
||||
);
|
||||
}).slice(0, 3);
|
||||
}
|
||||
|
||||
// No matches found
|
||||
return [];
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
import { filterCredentials } from '@/utils/credentialMatcher/CredentialMatcher';
|
||||
import type { Credential } from '@/utils/dist/shared/models/vault';
|
||||
|
||||
import { filterCredentials } from '../Filter';
|
||||
|
||||
describe('Filter - Credential URL Matching', () => {
|
||||
describe('CredentialMatcher - Credential URL Matching', () => {
|
||||
let testCredentials: Credential[];
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -292,28 +291,63 @@ describe('Filter - Credential URL Matching', () => {
|
||||
expect(matches[0].ServiceName).toBe('Reddit');
|
||||
});
|
||||
|
||||
/**
|
||||
* [#20] - Test reversed domain (Android package name) doesn't match on TLD
|
||||
* Note: Android package name filtering is not applicable to browser extensions.
|
||||
* This test is included for consistency with Android and iOS test suites but is skipped.
|
||||
*/
|
||||
it.skip('should not match credentials based on TLD when filtering reversed domains', () => {
|
||||
/**
|
||||
* Android package name detection is not implemented in browser extensions
|
||||
* since they only deal with web URLs, not Android app contexts.
|
||||
// [#20] - Test reversed domain (app package name) doesn't match on TLD
|
||||
it('should not match credentials based on TLD when filtering reversed domains', () => {
|
||||
/*
|
||||
* Test that dumpert.nl credential doesn't match nl.marktplaats.android package
|
||||
* They both contain "nl" in the name but shouldn't match since "nl" is just a TLD
|
||||
*/
|
||||
const reversedDomainCredentials = [
|
||||
createTestCredential('Dumpert.nl', '', 'user@dumpert.nl'),
|
||||
createTestCredential('Marktplaats.nl', '', 'user@marktplaats.nl'),
|
||||
];
|
||||
|
||||
const matches = filterCredentials(
|
||||
reversedDomainCredentials,
|
||||
'nl.marktplaats.android',
|
||||
''
|
||||
);
|
||||
|
||||
// Should only match Marktplaats, not Dumpert (even though both have "nl")
|
||||
expect(matches).toHaveLength(1);
|
||||
expect(matches[0].ServiceName).toBe('Marktplaats.nl');
|
||||
});
|
||||
|
||||
/**
|
||||
* [#21] - Test Android package names are properly detected and handled
|
||||
* Note: Android package name filtering is not applicable to browser extensions.
|
||||
* This test is included for consistency with Android and iOS test suites but is skipped.
|
||||
*/
|
||||
it.skip('should properly handle Android package names in filtering', () => {
|
||||
/**
|
||||
* Android package name detection is not implemented in browser extensions
|
||||
* since they only deal with web URLs, not Android app contexts.
|
||||
*/
|
||||
// [#21] - Test app package names are properly detected and handled
|
||||
it('should properly handle app package names in filtering', () => {
|
||||
const packageCredentials = [
|
||||
createTestCredential('Google App', 'com.google.android.googlequicksearchbox', 'user@google.com'),
|
||||
createTestCredential('Facebook', 'com.facebook.katana', 'user@facebook.com'),
|
||||
createTestCredential('WhatsApp', 'com.whatsapp', 'user@whatsapp.com'),
|
||||
createTestCredential('Generic Site', 'example.com', 'user@example.com'),
|
||||
];
|
||||
|
||||
// Test com.google.android package matches
|
||||
const googleMatches = filterCredentials(
|
||||
packageCredentials,
|
||||
'com.google.android.googlequicksearchbox',
|
||||
''
|
||||
);
|
||||
expect(googleMatches).toHaveLength(1);
|
||||
expect(googleMatches[0].ServiceName).toBe('Google App');
|
||||
|
||||
// Test com.facebook package matches
|
||||
const facebookMatches = filterCredentials(
|
||||
packageCredentials,
|
||||
'com.facebook.katana',
|
||||
''
|
||||
);
|
||||
expect(facebookMatches).toHaveLength(1);
|
||||
expect(facebookMatches[0].ServiceName).toBe('Facebook');
|
||||
|
||||
// Test that web domain doesn't match package name
|
||||
const webMatches = filterCredentials(
|
||||
packageCredentials,
|
||||
'https://example.com',
|
||||
''
|
||||
);
|
||||
expect(webMatches).toHaveLength(1);
|
||||
expect(webMatches[0].ServiceName).toBe('Generic Site');
|
||||
});
|
||||
|
||||
// [#22] - Test multi-part TLDs like .com.au don't match incorrectly
|
||||
@@ -16,10 +16,32 @@ type Identity = {
|
||||
nickName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for birthdate generation.
|
||||
*/
|
||||
interface IBirthdateOptions {
|
||||
/**
|
||||
* The target year for the birthdate (e.g., 1990).
|
||||
*/
|
||||
targetYear: number;
|
||||
/**
|
||||
* The random deviation in years (e.g., 5 means ±5 years from targetYear).
|
||||
* If 0, a random date within the target year will be chosen.
|
||||
*/
|
||||
yearDeviation: number;
|
||||
}
|
||||
interface IIdentityGenerator {
|
||||
generateRandomIdentity(gender?: string | 'random'): Identity;
|
||||
generateRandomIdentity(gender?: string | 'random', birthdateOptions?: IBirthdateOptions | null): Identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictionary of firstnames organized by decade range.
|
||||
*/
|
||||
interface IDecadeFirstnames {
|
||||
startYear: number;
|
||||
endYear: number;
|
||||
names: string[];
|
||||
}
|
||||
/**
|
||||
* Base identity generator.
|
||||
*/
|
||||
@@ -36,13 +58,29 @@ declare abstract class IdentityGenerator implements IIdentityGenerator {
|
||||
protected abstract getFirstNamesFemaleJson(): string[];
|
||||
protected abstract getLastNamesJson(): string[];
|
||||
/**
|
||||
* Generate a random date of birth.
|
||||
* Get decade-based male first names. Override this to provide age-specific names.
|
||||
* If not overridden, returns an empty array and falls back to generic names.
|
||||
*/
|
||||
protected generateRandomDateOfBirth(): Date;
|
||||
protected getFirstNamesMaleByDecade(): IDecadeFirstnames[];
|
||||
/**
|
||||
* Get decade-based female first names. Override this to provide age-specific names.
|
||||
* If not overridden, returns an empty array and falls back to generic names.
|
||||
*/
|
||||
protected getFirstNamesFemaleByDecade(): IDecadeFirstnames[];
|
||||
/**
|
||||
* Generate a random date of birth.
|
||||
* @param birthdateOptions Optional birthdate configuration
|
||||
*/
|
||||
protected generateRandomDateOfBirth(birthdateOptions?: IBirthdateOptions | null): Date;
|
||||
/**
|
||||
* Select appropriate firstnames based on birthdate.
|
||||
* Falls back to generic names if no decade-specific data is available.
|
||||
*/
|
||||
protected selectFirstnamesForBirthdate(birthdate: Date, isMale: boolean): string[];
|
||||
/**
|
||||
* Generate a random identity.
|
||||
*/
|
||||
generateRandomIdentity(gender?: string | 'random'): Identity;
|
||||
generateRandomIdentity(gender?: string | 'random', birthdateOptions?: IBirthdateOptions | null): Identity;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,6 +119,35 @@ declare class IdentityGeneratorNl extends IdentityGenerator {
|
||||
protected getLastNamesJson(): string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity generator for German language using German dictionaries with decade-based firstname support.
|
||||
* This implementation demonstrates how to use age-appropriate names based on birthdate.
|
||||
*/
|
||||
declare class IdentityGeneratorDe extends IdentityGenerator {
|
||||
/**
|
||||
* Get the male first names (generic fallback - empty as we use decade-based).
|
||||
*/
|
||||
protected getFirstNamesMaleJson(): string[];
|
||||
/**
|
||||
* Get the female first names (generic fallback - empty as we use decade-based).
|
||||
*/
|
||||
protected getFirstNamesFemaleJson(): string[];
|
||||
/**
|
||||
* Get the last names.
|
||||
*/
|
||||
protected getLastNamesJson(): string[];
|
||||
/**
|
||||
* Get decade-based male first names.
|
||||
* Each range covers a specific decade with names popular during that period.
|
||||
*/
|
||||
protected getFirstNamesMaleByDecade(): IDecadeFirstnames[];
|
||||
/**
|
||||
* Get decade-based female first names.
|
||||
* Each range covers a specific decade with names popular during that period.
|
||||
*/
|
||||
protected getFirstNamesFemaleByDecade(): IDecadeFirstnames[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper utilities for identity generation that can be used by multiple client applications.
|
||||
*/
|
||||
@@ -134,9 +201,79 @@ declare class UsernameEmailGenerator {
|
||||
private getSecureRandom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an age range option for identity generation.
|
||||
*/
|
||||
interface IAgeRangeOption {
|
||||
/**
|
||||
* The value to store (e.g., "21-25", "random")
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* The display label (e.g., "21-25", "Random")
|
||||
*/
|
||||
label: string;
|
||||
}
|
||||
/**
|
||||
* Gets all available age range options for identity generation.
|
||||
* @returns Array of age range options
|
||||
*/
|
||||
declare function getAvailableAgeRanges(): IAgeRangeOption[];
|
||||
/**
|
||||
* Converts an age range string (e.g., "21-25", "30-35", or "random") to birthdate options.
|
||||
* @param ageRange - The age range string
|
||||
* @returns An object containing targetYear and yearDeviation, or null if random
|
||||
*/
|
||||
declare function convertAgeRangeToBirthdateOptions(ageRange: string): IBirthdateOptions | null;
|
||||
|
||||
/**
|
||||
* Represents a language option for identity generation.
|
||||
*/
|
||||
interface ILanguageOption {
|
||||
/**
|
||||
* The language code (e.g., "en", "nl", "de")
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* The display label in the native language (e.g., "English", "Nederlands", "Deutsch")
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The flag emoji for the language (e.g., "🇬🇧", "🇳🇱", "🇩🇪")
|
||||
*/
|
||||
flag: string;
|
||||
/**
|
||||
* Alternative language codes that map to this identity generator language.
|
||||
* Used for matching UI locale codes to identity generator languages.
|
||||
* For example, "en-US", "en-GB", "en-CA" all map to "en"
|
||||
*/
|
||||
alternativeCodes?: string[];
|
||||
}
|
||||
/**
|
||||
* Gets all available languages for identity generation.
|
||||
* Display labels are in the native language, with optional flag emoji that clients can choose to display.
|
||||
* @returns Array of language options
|
||||
*/
|
||||
declare function getAvailableLanguages(): ILanguageOption[];
|
||||
/**
|
||||
* Maps a UI language code to an identity generator language code.
|
||||
* If no explicit match is found, returns null to indicate no preference.
|
||||
*
|
||||
* @param uiLanguageCode - The UI language code (e.g., "en", "en-US", "nl-NL", "de-DE", "fr")
|
||||
* @returns The matching identity generator language code or null if no match
|
||||
*
|
||||
* @example
|
||||
* mapUiLanguageToIdentityLanguage("en-US") // returns "en"
|
||||
* mapUiLanguageToIdentityLanguage("nl") // returns "nl"
|
||||
* mapUiLanguageToIdentityLanguage("de-CH") // returns "de"
|
||||
* mapUiLanguageToIdentityLanguage("fr") // returns null (no French identity generator)
|
||||
*/
|
||||
declare function mapUiLanguageToIdentityLanguage(uiLanguageCode: string | null | undefined): string | null;
|
||||
|
||||
/**
|
||||
* Creates a new identity generator based on the language.
|
||||
* @param language - The language to use for generating the identity (e.g. "en", "nl").
|
||||
* Falls back to English if the requested language is not supported.
|
||||
* @param language - The language to use for generating the identity (e.g. "en", "nl", "de").
|
||||
* @returns A new identity generator instance.
|
||||
*/
|
||||
declare const CreateIdentityGenerator: (language: string) => IdentityGenerator;
|
||||
@@ -148,4 +285,4 @@ declare const CreateIdentityGenerator: (language: string) => IdentityGenerator;
|
||||
*/
|
||||
declare const CreateUsernameEmailGenerator: () => UsernameEmailGenerator;
|
||||
|
||||
export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type Identity, IdentityGenerator, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator };
|
||||
export { CreateIdentityGenerator, CreateUsernameEmailGenerator, Gender, type IAgeRangeOption, type IBirthdateOptions, type IDecadeFirstnames, type IIdentityGenerator, type ILanguageOption, type Identity, IdentityGenerator, IdentityGeneratorDe, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator, convertAgeRangeToBirthdateOptions, getAvailableAgeRanges, getAvailableLanguages, mapUiLanguageToIdentityLanguage };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
type VaultMetadata = {
|
||||
publicEmailDomains: string[];
|
||||
privateEmailDomains: string[];
|
||||
hiddenPrivateEmailDomains: string[];
|
||||
vaultRevisionNumber: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,8 @@ type TotpCode = {
|
||||
SecretKey: string;
|
||||
/** The credential ID this TOTP code belongs to */
|
||||
CredentialId: string;
|
||||
/** Whether the TOTP code has been deleted (soft delete) */
|
||||
IsDeleted?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,18 +28,18 @@ type ApiErrorResponse = {
|
||||
* Vault type.
|
||||
*/
|
||||
type Vault = {
|
||||
blob: string;
|
||||
createdAt: string;
|
||||
credentialsCount: number;
|
||||
currentRevisionNumber: number;
|
||||
emailAddressList: string[];
|
||||
privateEmailDomainList: string[];
|
||||
publicEmailDomainList: string[];
|
||||
encryptionPublicKey: string;
|
||||
updatedAt: string;
|
||||
username: string;
|
||||
blob: string;
|
||||
version: string;
|
||||
client: string;
|
||||
currentRevisionNumber: number;
|
||||
credentialsCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
encryptionPublicKey?: string;
|
||||
emailAddressList?: string[];
|
||||
privateEmailDomainList?: string[];
|
||||
hiddenPrivateEmailDomainList?: string[];
|
||||
publicEmailDomainList?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,15 +103,19 @@ type ValidateLoginRequest2Fa = {
|
||||
clientPublicEphemeral: string;
|
||||
clientSessionProof: string;
|
||||
};
|
||||
/**
|
||||
* Token model type.
|
||||
*/
|
||||
type TokenModel = {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
/**
|
||||
* Validate login response type.
|
||||
*/
|
||||
type ValidateLoginResponse = {
|
||||
requiresTwoFactor: boolean;
|
||||
token?: {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
token?: TokenModel;
|
||||
serverSessionProof: string;
|
||||
};
|
||||
|
||||
@@ -350,6 +354,10 @@ declare enum AuthEventType {
|
||||
* Represents a user logout event.
|
||||
*/
|
||||
Logout = 3,
|
||||
/**
|
||||
* Represents a mobile login attempt (login via QR code from mobile app).
|
||||
*/
|
||||
MobileLogin = 4,
|
||||
/**
|
||||
* Represents JWT access token refresh event issued by client to API.
|
||||
*/
|
||||
@@ -380,4 +388,35 @@ declare enum AuthEventType {
|
||||
AccountDeletion = 99
|
||||
}
|
||||
|
||||
export { type ApiErrorResponse, AuthEventType, type AuthLogModel, type BadRequestResponse, type DeleteAccountInitiateRequest, type DeleteAccountInitiateResponse, type DeleteAccountRequest, type Email, type EmailAttachment, type FaviconExtractModel, type LoginRequest, type LoginResponse, type MailboxBulkRequest, type MailboxBulkResponse, type MailboxEmail, type PasswordChangeInitiateResponse, type RefreshToken, type StatusResponse, type ValidateLoginRequest, type ValidateLoginRequest2Fa, type ValidateLoginResponse, type Vault, type VaultPasswordChangeRequest, type VaultPostResponse, type VaultResponse };
|
||||
/**
|
||||
* Mobile login initiate request type.
|
||||
*/
|
||||
type MobileLoginInitiateRequest = {
|
||||
clientPublicKey: string;
|
||||
};
|
||||
/**
|
||||
* Mobile login initiate response type.
|
||||
*/
|
||||
type MobileLoginInitiateResponse = {
|
||||
requestId: string;
|
||||
};
|
||||
/**
|
||||
* Mobile login submit request type.
|
||||
*/
|
||||
type MobileLoginSubmitRequest = {
|
||||
requestId: string;
|
||||
encryptedDecryptionKey: string;
|
||||
};
|
||||
/**
|
||||
* Mobile login poll response type.
|
||||
*/
|
||||
type MobileLoginPollResponse = {
|
||||
fulfilled: boolean;
|
||||
encryptedSymmetricKey: string | null;
|
||||
encryptedToken: string | null;
|
||||
encryptedRefreshToken: string | null;
|
||||
encryptedDecryptionKey: string | null;
|
||||
encryptedUsername: string | null;
|
||||
};
|
||||
|
||||
export { type ApiErrorResponse, AuthEventType, type AuthLogModel, type BadRequestResponse, type DeleteAccountInitiateRequest, type DeleteAccountInitiateResponse, type DeleteAccountRequest, type Email, type EmailAttachment, type FaviconExtractModel, type LoginRequest, type LoginResponse, type MailboxBulkRequest, type MailboxBulkResponse, type MailboxEmail, type MobileLoginInitiateRequest, type MobileLoginInitiateResponse, type MobileLoginPollResponse, type MobileLoginSubmitRequest, type PasswordChangeInitiateResponse, type RefreshToken, type StatusResponse, type TokenModel, type ValidateLoginRequest, type ValidateLoginRequest2Fa, type ValidateLoginResponse, type Vault, type VaultPasswordChangeRequest, type VaultPostResponse, type VaultResponse };
|
||||
|
||||
@@ -7,6 +7,7 @@ var AuthEventType = /* @__PURE__ */ ((AuthEventType2) => {
|
||||
AuthEventType2[AuthEventType2["Login"] = 1] = "Login";
|
||||
AuthEventType2[AuthEventType2["TwoFactorAuthentication"] = 2] = "TwoFactorAuthentication";
|
||||
AuthEventType2[AuthEventType2["Logout"] = 3] = "Logout";
|
||||
AuthEventType2[AuthEventType2["MobileLogin"] = 4] = "MobileLogin";
|
||||
AuthEventType2[AuthEventType2["TokenRefresh"] = 10] = "TokenRefresh";
|
||||
AuthEventType2[AuthEventType2["PasswordReset"] = 20] = "PasswordReset";
|
||||
AuthEventType2[AuthEventType2["PasswordChange"] = 21] = "PasswordChange";
|
||||
|
||||
@@ -36,6 +36,20 @@ declare class PasswordGenerator {
|
||||
private readonly uppercaseChars;
|
||||
private readonly numberChars;
|
||||
private readonly specialChars;
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
private readonly ambiguousChars;
|
||||
private length;
|
||||
private useLowercase;
|
||||
|
||||
@@ -39,7 +39,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -13,7 +13,21 @@ var PasswordGenerator = class {
|
||||
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
this.numberChars = "0123456789";
|
||||
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
this.ambiguousChars = "Il1O0o";
|
||||
/**
|
||||
* Ambiguous characters that look similar and are easy to confuse when typing:
|
||||
* - I, l, 1, | (pipe) - all look like vertical lines
|
||||
* - O, 0, o - all look like circles
|
||||
* - Z, 2 - similar appearance
|
||||
* - S, 5 - similar appearance
|
||||
* - B, 8 - similar appearance
|
||||
* - G, 6 - similar appearance
|
||||
* - Brackets, braces, parentheses: [], {}, ()
|
||||
* - Quotes: ', ", `
|
||||
* - Punctuation pairs: ;:, .,
|
||||
* - Dashes: -, _
|
||||
* - Angle brackets: <>
|
||||
*/
|
||||
this.ambiguousChars = "Il1O0oZzSsBbGg2568|[]{}()<>;:,.`'\"_-";
|
||||
this.length = 18;
|
||||
this.useLowercase = true;
|
||||
this.useUppercase = true;
|
||||
|
||||
@@ -27,17 +27,16 @@ export class FormFiller {
|
||||
* @param credential The credential to fill the form with.
|
||||
*/
|
||||
public async fillFields(credential: Credential): Promise<void> {
|
||||
// Perform security validation before filling any fields
|
||||
if (!await this.validateFormSecurity()) {
|
||||
console.warn('[AliasVault Security] Autofill blocked due to security validation failure');
|
||||
return;
|
||||
}
|
||||
// Perform security validation to identify safe fields
|
||||
const securityResults = await this.validateFormSecurity();
|
||||
|
||||
// Fill basic fields and password fields in parallel
|
||||
await Promise.all([
|
||||
this.fillBasicFields(credential),
|
||||
this.fillPasswordFields(credential)
|
||||
]);
|
||||
/*
|
||||
* Fill fields sequentially to avoid race conditions and conflicts.
|
||||
* Some websites have event handlers that can interfere with parallel filling.
|
||||
* Only fill fields that passed security validation.
|
||||
*/
|
||||
await this.fillBasicFields(credential, securityResults);
|
||||
await this.fillPasswordFields(credential, securityResults);
|
||||
|
||||
this.fillBirthdateFields(credential);
|
||||
this.fillGenderFields(credential);
|
||||
@@ -50,12 +49,18 @@ export class FormFiller {
|
||||
* - Form field obstruction via overlays
|
||||
* - Suspicious element positioning
|
||||
* - Multiple forms with identical fields (potential decoy attacks)
|
||||
*
|
||||
* @returns A map of field elements to their security validation result (true = safe, false = unsafe)
|
||||
*/
|
||||
private async validateFormSecurity(): Promise<boolean> {
|
||||
private async validateFormSecurity(): Promise<Map<HTMLElement, boolean>> {
|
||||
const results = new Map<HTMLElement, boolean>();
|
||||
|
||||
try {
|
||||
// Skip security validation in test environments where browser APIs may not be available
|
||||
if (typeof window === 'undefined' || typeof MouseEvent === 'undefined') {
|
||||
return true;
|
||||
// In test environments, mark all fields as safe
|
||||
this.getAllFormFields().forEach(field => results.set(field, true));
|
||||
return results;
|
||||
}
|
||||
|
||||
// 1. Check page-wide security using ClickValidator (detects body/HTML opacity tricks)
|
||||
@@ -67,30 +72,40 @@ export class FormFiller {
|
||||
});
|
||||
// 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;
|
||||
const pageWideSecure = await this.clickValidator.validateClick(dummyEvent);
|
||||
if (!pageWideSecure) {
|
||||
console.warn('[AliasVault Security] Page-wide attack detected - blocking all autofill');
|
||||
// Mark all fields as unsafe
|
||||
this.getAllFormFields().forEach(field => results.set(field, false));
|
||||
return results;
|
||||
}
|
||||
|
||||
// 2. Check form field obstruction and positioning
|
||||
// 2. Check for suspicious form duplication (decoy attack)
|
||||
const hasDecoyForms = this.detectDecoyForms();
|
||||
if (hasDecoyForms) {
|
||||
console.warn('[AliasVault Security] Multiple suspicious forms detected - blocking all autofill');
|
||||
// Mark all fields as unsafe
|
||||
this.getAllFormFields().forEach(field => results.set(field, false));
|
||||
return results;
|
||||
}
|
||||
|
||||
// 3. Check individual form field obstruction and positioning
|
||||
const formFields = this.getAllFormFields();
|
||||
for (const field of formFields) {
|
||||
if (!this.validateFieldSecurity(field)) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Field obstruction detected', field);
|
||||
return false;
|
||||
const isFieldSecure = this.validateFieldSecurity(field);
|
||||
results.set(field, isFieldSecure);
|
||||
|
||||
if (!isFieldSecure) {
|
||||
console.warn('[AliasVault Security] Field failed security check (will be skipped):', field);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check for suspicious form duplication (decoy attack)
|
||||
if (this.detectDecoyForms()) {
|
||||
console.warn('[AliasVault Security] Form autofill blocked: Multiple suspicious forms detected');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('[AliasVault Security] Form security validation error:', error);
|
||||
return false; // Fail safely - block autofill if validation fails
|
||||
// Fail safely - mark all fields as unsafe if validation fails
|
||||
this.getAllFormFields().forEach(field => results.set(field, false));
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,37 +269,50 @@ export class FormFiller {
|
||||
* @param value The value to set
|
||||
*/
|
||||
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
|
||||
/*
|
||||
* Check for shadow DOM first - if found, only set value on the shadow input
|
||||
* to avoid duplicate value setting which can cause conflicts.
|
||||
*/
|
||||
if (element.shadowRoot) {
|
||||
const shadowInput = element.shadowRoot.querySelector('input, textarea') as HTMLInputElement;
|
||||
if (shadowInput) {
|
||||
shadowInput.value = value;
|
||||
// Trigger events on the shadow input as well
|
||||
this.triggerInputEvents(shadowInput, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
childInput.value = value;
|
||||
this.triggerInputEvents(childInput, false);
|
||||
/*
|
||||
* Check for child input (non-shadow DOM) only if element is not already an input.
|
||||
* This handles custom wrapper elements.
|
||||
*/
|
||||
if (element.tagName.toLowerCase() !== 'input' && element.tagName.toLowerCase() !== 'select' && element.tagName.toLowerCase() !== 'textarea') {
|
||||
const childInput = element.querySelector('input, textarea, select') as HTMLInputElement | HTMLSelectElement;
|
||||
if (childInput) {
|
||||
childInput.value = value;
|
||||
this.triggerInputEvents(childInput, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Default case: set value directly on the element.
|
||||
* This handles standard HTML input/select/textarea elements.
|
||||
*/
|
||||
element.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the basic fields of the form.
|
||||
* @param credential The credential to fill the form with.
|
||||
* @param securityResults Security validation results for each field.
|
||||
*/
|
||||
private async fillBasicFields(credential: Credential): Promise<void> {
|
||||
if (this.form.usernameField && credential.Username) {
|
||||
private async fillBasicFields(credential: Credential, securityResults: Map<HTMLElement, boolean>): Promise<void> {
|
||||
if (this.form.usernameField && credential.Username && securityResults.get(this.form.usernameField) !== false) {
|
||||
await this.fillTextFieldWithTyping(this.form.usernameField, credential.Username);
|
||||
}
|
||||
|
||||
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined)) {
|
||||
if (this.form.emailField && (credential.Alias?.Email !== undefined || credential.Username !== undefined) && securityResults.get(this.form.emailField) !== false) {
|
||||
if (credential.Alias?.Email) {
|
||||
this.setElementValue(this.form.emailField, credential.Alias.Email);
|
||||
this.triggerInputEvents(this.form.emailField);
|
||||
@@ -304,7 +332,7 @@ export class FormFiller {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.form.emailConfirmField && credential.Alias?.Email) {
|
||||
if (this.form.emailConfirmField && credential.Alias?.Email && securityResults.get(this.form.emailConfirmField) !== false) {
|
||||
this.setElementValue(this.form.emailConfirmField, credential.Alias.Email);
|
||||
this.triggerInputEvents(this.form.emailConfirmField);
|
||||
}
|
||||
@@ -333,7 +361,10 @@ export class FormFiller {
|
||||
* @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)
|
||||
/*
|
||||
* Find the actual input element (could be in shadow DOM).
|
||||
* This ensures we only fill one element, avoiding duplicate fills.
|
||||
*/
|
||||
let actualInput = field;
|
||||
|
||||
// Check for shadow DOM input
|
||||
@@ -372,19 +403,20 @@ export class FormFiller {
|
||||
* 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.
|
||||
* @param securityResults Security validation results for each field.
|
||||
*/
|
||||
private async fillPasswordFields(credential: Credential): Promise<void> {
|
||||
private async fillPasswordFields(credential: Credential, securityResults: Map<HTMLElement, boolean>): Promise<void> {
|
||||
if (!credential.Password) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill main password field first
|
||||
if (this.form.passwordField) {
|
||||
// Fill main password field first (only if it passed security check)
|
||||
if (this.form.passwordField && securityResults.get(this.form.passwordField) !== false) {
|
||||
await this.fillPasswordField(this.form.passwordField, credential.Password);
|
||||
}
|
||||
|
||||
// Then fill password confirm field after main field is complete
|
||||
if (this.form.passwordConfirmField) {
|
||||
// Then fill password confirm field after main field is complete (only if it passed security check)
|
||||
if (this.form.passwordConfirmField && securityResults.get(this.form.passwordConfirmField) !== false) {
|
||||
await this.fillPasswordField(this.form.passwordConfirmField, credential.Password);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +430,10 @@ export class FormFiller {
|
||||
* @param password The password to fill the field with.
|
||||
*/
|
||||
private async fillPasswordField(field: HTMLInputElement, password: string): Promise<void> {
|
||||
// Find the actual input element (could be in shadow DOM)
|
||||
/*
|
||||
* Find the actual input element (could be in shadow DOM).
|
||||
* This ensures we only fill one element, avoiding duplicate fills.
|
||||
*/
|
||||
let actualInput = field;
|
||||
|
||||
// Check for shadow DOM input
|
||||
|
||||
@@ -33,7 +33,7 @@ export class PasskeyAuthenticator {
|
||||
*/
|
||||
private constructor() {}
|
||||
|
||||
/** AliasVault AAGUID: a11a5vau-9f32-4b8c-8c5d-2f7d13e8c942 */
|
||||
/** AliasVault AAGUID: a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942 */
|
||||
private static readonly AAGUID = new Uint8Array([
|
||||
0xa1, 0x1a, 0x5f, 0xaa, 0x9f, 0x32, 0x4b, 0x8c,
|
||||
0x8c, 0x5d, 0x2f, 0x7d, 0x13, 0xe8, 0xc9, 0x42
|
||||
|
||||
@@ -208,11 +208,10 @@ describe('PasskeyAuthenticator', () => {
|
||||
);
|
||||
|
||||
/*
|
||||
* AliasVault AAGUID: a11a5vau-9f32-4b8c-8c5d-2f7d13e8c942
|
||||
* AliasVault AAGUID: a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942
|
||||
* Convert the string representation to bytes (replace 'v' with 'f' and 'u' with 'a')
|
||||
*/
|
||||
const aaguidString = 'a11a5vau-9f32-4b8c-8c5d-2f7d13e8c942';
|
||||
const aaguidHex = aaguidString.replace(/-/g, '').replace(/v/g, 'f').replace(/u/g, 'a');
|
||||
const aaguidHex = 'a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942'.replace(/-/g, '');
|
||||
|
||||
// Verify the hex conversion matches expected bytes
|
||||
const expectedAAGUID = new Uint8Array(16);
|
||||
|
||||
@@ -72,6 +72,7 @@ export type WebAuthnGetRequest = {
|
||||
allowCredentials?: Array<{ id: string }>;
|
||||
};
|
||||
origin: string;
|
||||
isAutomaticRequest?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Result of a successful mobile login containing decrypted authentication data.
|
||||
*/
|
||||
export type MobileLoginResult = {
|
||||
/**
|
||||
* The username.
|
||||
*/
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* The JWT access token.
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* The refresh token.
|
||||
*/
|
||||
refreshToken: string;
|
||||
|
||||
/**
|
||||
* The vault decryption key (base64 encoded).
|
||||
*/
|
||||
decryptionKey: string;
|
||||
|
||||
/**
|
||||
* The user's salt for key derivation.
|
||||
*/
|
||||
salt: string;
|
||||
|
||||
/**
|
||||
* The encryption type (e.g., "Argon2id").
|
||||
*/
|
||||
encryptionType: string;
|
||||
|
||||
/**
|
||||
* The encryption settings JSON string.
|
||||
*/
|
||||
encryptionSettings: string;
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export type StoreVaultRequest = {
|
||||
vaultBlob: string;
|
||||
publicEmailDomainList?: string[];
|
||||
privateEmailDomainList?: string[];
|
||||
hiddenPrivateEmailDomainList?: string[];
|
||||
vaultRevisionNumber?: number;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ export type VaultResponse = {
|
||||
vault?: string,
|
||||
publicEmailDomains?: string[],
|
||||
privateEmailDomains?: string[],
|
||||
hiddenPrivateEmailDomains?: string[],
|
||||
vaultRevisionNumber?: number
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export default defineConfig({
|
||||
return {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.24.0",
|
||||
version: "0.26.0",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
11
apps/mobile-app/.vscode/settings.json
vendored
Normal file
11
apps/mobile-app/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"i18n",
|
||||
"i18n/locales",
|
||||
"ios/Pods/RCT-Folly/folly/lang",
|
||||
"ios/Pods/boost/boost/predef/language",
|
||||
"ios/Pods/Headers/Private/RCT-Folly/folly/lang",
|
||||
"ios/Pods/Headers/Public/RCT-Folly/folly/lang"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
@@ -93,8 +93,8 @@ android {
|
||||
applicationId 'net.aliasvault.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2400902
|
||||
versionName "0.24.0"
|
||||
versionCode 2600100
|
||||
versionName "0.26.0-alpha"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
@@ -202,6 +202,9 @@ dependencies {
|
||||
// Add modern SQLite library with VACUUM INTO and backup API support
|
||||
implementation("com.github.requery:sqlite-android:3.49.0")
|
||||
|
||||
// Add ZXing library for QR code scanning (F-Droid compatible)
|
||||
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
|
||||
|
||||
// Test dependencies
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.0.0'
|
||||
|
||||
@@ -50,6 +50,21 @@
|
||||
android:theme="@style/PasskeyRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:fitsSystemWindows="true" />
|
||||
|
||||
<!-- PIN Unlock Activity -->
|
||||
<activity
|
||||
android:name=".pinunlock.PinUnlockActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/PasskeyRegistrationTheme"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<!-- QR Scanner Activity -->
|
||||
<activity
|
||||
android:name=".qrscanner.QRScannerActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/zxing_CaptureTheme"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package net.aliasvault.app
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.WindowInsetsController
|
||||
@@ -36,7 +37,6 @@ class MainActivity : ReactActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Reapply system bar configuration when app resumes
|
||||
// This ensures our settings persist even if other code tries to override them
|
||||
configureSystemBars()
|
||||
}
|
||||
|
||||
@@ -101,4 +101,126 @@ class MainActivity : ReactActivity() {
|
||||
) {},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle activity results - specifically for PIN unlock and PIN setup.
|
||||
*/
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// Handle PIN unlock results directly
|
||||
if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_UNLOCK_REQUEST_CODE) {
|
||||
handlePinUnlockResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_SETUP_REQUEST_CODE) {
|
||||
handlePinSetupResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.QR_SCANNER_REQUEST_CODE) {
|
||||
handleQRScannerResult(resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle PIN unlock result directly without going through React context.
|
||||
* This avoids race conditions with React context initialization.
|
||||
* @param resultCode The result code from the PIN unlock activity.
|
||||
* @param data The intent data containing the encryption key.
|
||||
*/
|
||||
private fun handlePinUnlockResult(resultCode: Int, data: Intent?) {
|
||||
val promise = net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise
|
||||
net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise = null
|
||||
|
||||
if (promise == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val vaultStore = net.aliasvault.app.vaultstore.VaultStore.getInstance(
|
||||
net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider(this) { null },
|
||||
net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider(this),
|
||||
)
|
||||
|
||||
when (resultCode) {
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_SUCCESS -> {
|
||||
val encryptionKeyBase64 = data?.getStringExtra(
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.EXTRA_ENCRYPTION_KEY,
|
||||
)
|
||||
|
||||
if (encryptionKeyBase64 == null) {
|
||||
promise.reject("UNLOCK_ERROR", "Failed to get encryption key from PIN unlock", null)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
vaultStore.storeEncryptionKey(encryptionKeyBase64)
|
||||
vaultStore.unlockVault()
|
||||
promise.resolve(true)
|
||||
} catch (e: Exception) {
|
||||
promise.reject("UNLOCK_ERROR", "Failed to unlock vault: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_CANCELLED -> {
|
||||
promise.reject("USER_CANCELLED", "User cancelled PIN unlock", null)
|
||||
}
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_PIN_DISABLED -> {
|
||||
promise.reject("PIN_DISABLED", "PIN was disabled", null)
|
||||
}
|
||||
else -> {
|
||||
promise.reject("UNKNOWN_ERROR", "Unknown error in PIN unlock", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle PIN setup result.
|
||||
* @param resultCode The result code from the PIN setup activity.
|
||||
* @param data The intent data (not used for setup, setup happens internally).
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handlePinSetupResult(resultCode: Int, data: Intent?) {
|
||||
val promise = net.aliasvault.app.nativevaultmanager.NativeVaultManager.pinSetupPromise
|
||||
net.aliasvault.app.nativevaultmanager.NativeVaultManager.pinSetupPromise = null
|
||||
|
||||
if (promise == null) {
|
||||
return
|
||||
}
|
||||
|
||||
when (resultCode) {
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_SUCCESS -> {
|
||||
// PIN setup successful
|
||||
promise.resolve(null)
|
||||
}
|
||||
net.aliasvault.app.pinunlock.PinUnlockActivity.RESULT_CANCELLED -> {
|
||||
// User cancelled PIN setup
|
||||
promise.reject("USER_CANCELLED", "User cancelled PIN setup", null)
|
||||
}
|
||||
else -> {
|
||||
promise.reject("SETUP_ERROR", "PIN setup failed", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle QR scanner result.
|
||||
* @param resultCode The result code from the QR scanner activity.
|
||||
* @param data The intent data containing the scanned QR code.
|
||||
*/
|
||||
private fun handleQRScannerResult(resultCode: Int, data: Intent?) {
|
||||
val promise = net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise
|
||||
net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise = null
|
||||
|
||||
if (promise == null) {
|
||||
return
|
||||
}
|
||||
|
||||
when (resultCode) {
|
||||
RESULT_OK -> {
|
||||
val scannedData = data?.getStringExtra("SCAN_RESULT")
|
||||
promise.resolve(scannedData)
|
||||
}
|
||||
RESULT_CANCELED -> {
|
||||
promise.resolve(null)
|
||||
}
|
||||
else -> {
|
||||
promise.resolve(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ class AutofillService : AutofillService() {
|
||||
val encodedUrl = appInfo?.let { java.net.URLEncoder.encode(it, "UTF-8") } ?: ""
|
||||
|
||||
// Create deep link URL
|
||||
val deepLinkUrl = "net.aliasvault.app://credentials/add-edit-page?serviceUrl=$encodedUrl"
|
||||
val deepLinkUrl = "aliasvault://credentials/add-edit-page?serviceUrl=$encodedUrl"
|
||||
|
||||
// Add a click listener to open AliasVault app with deep link
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
@@ -477,7 +477,7 @@ class AutofillService : AutofillService() {
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
|
||||
// Create deep link URL
|
||||
val deepLinkUrl = "net.aliasvault.app://reinitialize"
|
||||
val deepLinkUrl = "aliasvault://reinitialize"
|
||||
|
||||
// Add a click listener to open AliasVault app with deep link
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
@@ -526,7 +526,7 @@ class AutofillService : AutofillService() {
|
||||
val encodedUrl = appInfo?.let { java.net.URLEncoder.encode(it, "UTF-8") } ?: ""
|
||||
|
||||
// Create deep link URL to credentials page with service URL
|
||||
val deepLinkUrl = "net.aliasvault.app://credentials?serviceUrl=$encodedUrl"
|
||||
val deepLinkUrl = "aliasvault://credentials?serviceUrl=$encodedUrl"
|
||||
|
||||
// Add a click listener to open AliasVault app with deep link
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
@@ -569,7 +569,7 @@ class AutofillService : AutofillService() {
|
||||
// Create deep link URL to open the credentials page
|
||||
val appInfo = fieldFinder.getAppInfo()
|
||||
val encodedUrl = appInfo?.let { java.net.URLEncoder.encode(it, "UTF-8") } ?: ""
|
||||
val deepLinkUrl = "net.aliasvault.app://credentials?serviceUrl=$encodedUrl"
|
||||
val deepLinkUrl = "aliasvault://credentials?serviceUrl=$encodedUrl"
|
||||
|
||||
// Add a click listener to open AliasVault app with deep link
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
|
||||
@@ -4,13 +4,22 @@ import net.aliasvault.app.vaultstore.models.Credential
|
||||
|
||||
/**
|
||||
* Helper class to match credentials against app/website information for autofill.
|
||||
* This implementation matches the iOS filtering logic exactly for cross-platform consistency.
|
||||
* This implementation follows the unified filtering algorithm specification defined in
|
||||
* docs/CREDENTIAL_FILTERING_SPEC.md for cross-platform consistency with iOS and Browser Extension.
|
||||
*
|
||||
* Algorithm Structure (Priority Order with Early Returns):
|
||||
* 1. PRIORITY 1: App Package Name Exact Match (e.g., com.coolblue.app)
|
||||
* 2. PRIORITY 2: URL Domain Matching (exact, subdomain, root domain)
|
||||
* 3. PRIORITY 3: Service Name Fallback (only for credentials without URLs - anti-phishing)
|
||||
* 4. PRIORITY 4: Text/Word Matching (non-URL search)
|
||||
*/
|
||||
object CredentialMatcher {
|
||||
|
||||
/**
|
||||
* Common top-level domains (TLDs) that should be excluded from matching.
|
||||
* This prevents false matches when dealing with reversed domain names (App package names).
|
||||
* Common top-level domains (TLDs) used for app package name detection.
|
||||
* When a search string starts with one of these TLDs followed by a dot (e.g., "com.coolblue.app"),
|
||||
* it's identified as a reversed domain name (app package name) rather than a regular URL.
|
||||
* This prevents false matches and enables proper package name handling.
|
||||
*/
|
||||
private val commonTlds = setOf(
|
||||
// Generic TLDs
|
||||
@@ -230,101 +239,117 @@ object CredentialMatcher {
|
||||
|
||||
/**
|
||||
* Filter credentials based on search text with anti-phishing protection.
|
||||
*
|
||||
* This method follows a strict priority-based algorithm with early returns:
|
||||
* 1. PRIORITY 1: App Package Name Exact Match (highest priority)
|
||||
* 2. PRIORITY 2: URL Domain Matching
|
||||
* 3. PRIORITY 3: Service Name Fallback (anti-phishing protection)
|
||||
* 4. PRIORITY 4: Text/Word Matching (lowest priority)
|
||||
*
|
||||
* @param credentials List of credentials to filter
|
||||
* @param searchText Search term (app info, URL, etc.)
|
||||
* @param searchText Search term (app package name, URL, or text)
|
||||
* @return Filtered list of credentials
|
||||
*
|
||||
* **Security Note**: When searching with a URL, text search fallback only applies to
|
||||
* credentials with no service URL defined. This prevents phishing attacks where a
|
||||
* malicious site might match credentials intended for the legitimate site.
|
||||
* **Security Note**: Priority 3 only searches credentials with no service URL defined.
|
||||
* This prevents phishing attacks where a malicious site might match credentials
|
||||
* intended for a legitimate site.
|
||||
*/
|
||||
fun filterCredentialsByAppInfo(
|
||||
credentials: List<Credential>,
|
||||
searchText: String,
|
||||
): List<Credential> {
|
||||
// Early return for empty search
|
||||
if (searchText.isEmpty()) {
|
||||
return credentials
|
||||
}
|
||||
|
||||
val matches = mutableSetOf<Credential>()
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIORITY 1: App Package Name Exact Match
|
||||
// Check if search text is an app package name (e.g., com.coolblue.app)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
if (isAppPackageName(searchText)) {
|
||||
// Perform exact string match on ServiceUrl field
|
||||
val packageMatches = credentials.filter { credential ->
|
||||
val serviceUrl = credential.service.url
|
||||
!serviceUrl.isNullOrEmpty() && searchText == serviceUrl
|
||||
}
|
||||
|
||||
// EARLY RETURN if matches found
|
||||
if (packageMatches.isNotEmpty()) {
|
||||
return packageMatches
|
||||
}
|
||||
// If no matches found, continue to next priority
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIORITY 2: URL Domain Matching
|
||||
// Try to extract domain from search text
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
val searchDomain = extractDomain(searchText)
|
||||
|
||||
// Try to parse as App package name first.
|
||||
if (isAppPackageName(searchText)) {
|
||||
// Is most likely app package name, do a simple exact match search on URL field
|
||||
credentials.forEach { credential ->
|
||||
val serviceUrl = credential.service.url
|
||||
if (!serviceUrl.isNullOrEmpty()) {
|
||||
if (searchText == serviceUrl) {
|
||||
matches.add(credential)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If app package name results in matches, return them immediately.
|
||||
if (matches.isNotEmpty()) {
|
||||
return matches.toList()
|
||||
}
|
||||
|
||||
// Try URL second
|
||||
if (searchDomain.isNotEmpty()) {
|
||||
// Check for domain matches with priority
|
||||
credentials.forEach { credential ->
|
||||
// Valid domain extracted - perform domain matching
|
||||
val domainMatches = credentials.filter { credential ->
|
||||
val serviceUrl = credential.service.url
|
||||
if (!serviceUrl.isNullOrEmpty()) {
|
||||
if (domainsMatch(searchText, serviceUrl)) {
|
||||
matches.add(credential)
|
||||
}
|
||||
}
|
||||
!serviceUrl.isNullOrEmpty() && domainsMatch(searchText, serviceUrl)
|
||||
}
|
||||
|
||||
// SECURITY: If no domain matches found, only search text in credentials with NO service URL
|
||||
// This prevents phishing attacks by ensuring URL-based credentials only match their domains
|
||||
if (matches.isEmpty()) {
|
||||
val domainParts = searchDomain.split(".")
|
||||
val domainWithoutExtension = domainParts.firstOrNull()?.lowercase() ?: searchDomain.lowercase()
|
||||
|
||||
val nameMatches = credentials.filter { credential ->
|
||||
if (!credential.service.url.isNullOrEmpty()) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
val serviceNameMatch = credential.service.name?.lowercase()?.contains(domainWithoutExtension) ?: false
|
||||
val notesMatch = credential.notes?.lowercase()?.contains(domainWithoutExtension) ?: false
|
||||
serviceNameMatch || notesMatch
|
||||
}
|
||||
matches.addAll(nameMatches)
|
||||
// EARLY RETURN if matches found
|
||||
if (domainMatches.isNotEmpty()) {
|
||||
return domainMatches
|
||||
}
|
||||
|
||||
return matches.toList()
|
||||
} else {
|
||||
// Non-URL fallback: Extract words from search text for better matching
|
||||
val searchWords = extractWords(searchText)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// PRIORITY 3: Service Name Fallback (Anti-Phishing Protection)
|
||||
// No domain matches found - search in service names
|
||||
// CRITICAL: Only search credentials with NO service URL defined
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
val domainParts = searchDomain.split(".")
|
||||
val domainWithoutExtension = domainParts.firstOrNull()?.lowercase() ?: searchDomain.lowercase()
|
||||
|
||||
if (searchWords.isEmpty()) {
|
||||
// If no meaningful words after extraction, fall back to simple contains
|
||||
val lowercasedSearch = searchText.lowercase()
|
||||
return credentials.filter { credential ->
|
||||
(credential.service.name?.lowercase()?.contains(lowercasedSearch) ?: false) ||
|
||||
(credential.username?.lowercase()?.contains(lowercasedSearch) ?: false) ||
|
||||
(credential.notes?.lowercase()?.contains(lowercasedSearch) ?: false)
|
||||
val nameMatches = credentials.filter { credential ->
|
||||
// SECURITY: Skip credentials that have a URL defined
|
||||
if (!credential.service.url.isNullOrEmpty()) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
// Search in ServiceName and Notes using substring contains
|
||||
val serviceNameMatch = credential.service.name?.lowercase()?.contains(domainWithoutExtension) ?: false
|
||||
val notesMatch = credential.notes?.lowercase()?.contains(domainWithoutExtension) ?: false
|
||||
serviceNameMatch || notesMatch
|
||||
}
|
||||
|
||||
// Match using extracted words
|
||||
// Return matches from Priority 3 (don't continue to Priority 4)
|
||||
return nameMatches
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIORITY 4: Text/Word Matching
|
||||
// Search text is not a URL or package name - perform text-based matching
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
val searchWords = extractWords(searchText)
|
||||
|
||||
if (searchWords.isEmpty()) {
|
||||
// If no meaningful words after extraction, fall back to simple substring contains
|
||||
val lowercasedSearch = searchText.lowercase()
|
||||
return credentials.filter { credential ->
|
||||
val serviceNameWords = credential.service.name?.let { extractWords(it) } ?: emptyList()
|
||||
val usernameWords = credential.username?.let { extractWords(it) } ?: emptyList()
|
||||
val notesWords = credential.notes?.let { extractWords(it) } ?: emptyList()
|
||||
(credential.service.name?.lowercase()?.contains(lowercasedSearch) ?: false) ||
|
||||
(credential.username?.lowercase()?.contains(lowercasedSearch) ?: false) ||
|
||||
(credential.notes?.lowercase()?.contains(lowercasedSearch) ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any search word matches any credential word exactly
|
||||
searchWords.any { searchWord ->
|
||||
serviceNameWords.contains(searchWord) ||
|
||||
usernameWords.contains(searchWord) ||
|
||||
notesWords.contains(searchWord)
|
||||
}
|
||||
// Match using extracted words - exact word matching only
|
||||
return credentials.filter { credential ->
|
||||
val serviceNameWords = credential.service.name?.let { extractWords(it) } ?: emptyList()
|
||||
val usernameWords = credential.username?.let { extractWords(it) } ?: emptyList()
|
||||
val notesWords = credential.notes?.let { extractWords(it) } ?: emptyList()
|
||||
|
||||
// Check if any search word matches any credential word exactly
|
||||
searchWords.any { searchWord ->
|
||||
serviceNameWords.contains(searchWord) ||
|
||||
usernameWords.contains(searchWord) ||
|
||||
notesWords.contains(searchWord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import net.aliasvault.app.R
|
||||
import net.aliasvault.app.utils.Helpers
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreOperationCallback
|
||||
import net.aliasvault.app.vaultstore.passkey.PasskeyAuthenticator
|
||||
import net.aliasvault.app.vaultstore.passkey.PasskeyHelper
|
||||
import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider
|
||||
@@ -44,6 +43,7 @@ class PasskeyAuthenticationActivity : FragmentActivity() {
|
||||
}
|
||||
|
||||
private lateinit var vaultStore: VaultStore
|
||||
private lateinit var unlockCoordinator: UnlockCoordinator
|
||||
private var providerRequest: ProviderGetCredentialRequest? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -66,47 +66,30 @@ class PasskeyAuthenticationActivity : FragmentActivity() {
|
||||
VaultStore.getInstance(keystoreProvider, storageProvider)
|
||||
}
|
||||
|
||||
// Show loading screen while biometric prompt is displayed
|
||||
// Show loading screen while unlock is in progress
|
||||
setContentView(R.layout.activity_loading)
|
||||
|
||||
// Check if biometric authentication is available before attempting unlock
|
||||
if (!vaultStore.isBiometricAuthEnabled()) {
|
||||
Log.e(TAG, "Biometric authentication is not enabled or not available")
|
||||
showError(getString(R.string.error_biometric_required))
|
||||
return
|
||||
}
|
||||
|
||||
// Show biometric prompt to unlock vault (same pattern as registration)
|
||||
val keystoreProvider = AndroidKeystoreProvider(applicationContext) { this }
|
||||
keystoreProvider.retrieveKeyExternal(
|
||||
this,
|
||||
object : KeystoreOperationCallback {
|
||||
override fun onSuccess(result: String) {
|
||||
try {
|
||||
// Biometric authentication successful, unlock vault
|
||||
vaultStore.initEncryptionKey(result)
|
||||
vaultStore.unlockVault()
|
||||
|
||||
// Now process the authentication request
|
||||
runOnUiThread {
|
||||
processAuthenticationRequest()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to unlock vault after biometric auth", e)
|
||||
runOnUiThread {
|
||||
showUnlockError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Exception) {
|
||||
Log.e(TAG, "Failed to retrieve encryption key", e)
|
||||
runOnUiThread {
|
||||
showKeychainError(e)
|
||||
}
|
||||
}
|
||||
// Initialize unlock coordinator
|
||||
unlockCoordinator = UnlockCoordinator(
|
||||
activity = this,
|
||||
vaultStore = vaultStore,
|
||||
onUnlocked = {
|
||||
// Vault unlocked successfully - process authentication request
|
||||
processAuthenticationRequest()
|
||||
},
|
||||
onCancelled = {
|
||||
// User cancelled unlock
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
},
|
||||
onError = { errorMessage ->
|
||||
// Error during unlock
|
||||
showError(errorMessage)
|
||||
},
|
||||
)
|
||||
|
||||
// Start the unlock flow
|
||||
unlockCoordinator.startUnlockFlow()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onCreate", e)
|
||||
// Make sure we have the layout set before showing error
|
||||
@@ -117,9 +100,18 @@ class PasskeyAuthenticationActivity : FragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// Delegate PIN unlock result to coordinator
|
||||
if (requestCode == UnlockCoordinator.REQUEST_CODE_PIN_UNLOCK) {
|
||||
unlockCoordinator.handlePinUnlockResult(resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the passkey authentication request and generate assertion.
|
||||
* Called after biometric authentication succeeds and vault is unlocked.
|
||||
* Called after authentication (biometric or PIN) succeeds and vault is unlocked.
|
||||
*/
|
||||
private fun processAuthenticationRequest() {
|
||||
val providerRequest = this.providerRequest ?: run {
|
||||
@@ -337,33 +329,6 @@ class PasskeyAuthenticationActivity : FragmentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message when unlocking vault fails.
|
||||
*/
|
||||
private fun showUnlockError(e: Exception) {
|
||||
val errorMessage = when {
|
||||
e.message?.contains("No encryption key found", ignoreCase = true) == true ->
|
||||
getString(R.string.error_unlock_vault_first)
|
||||
e.message?.contains("Database setup error", ignoreCase = true) == true ->
|
||||
getString(R.string.error_vault_decrypt_failed)
|
||||
else -> getString(R.string.error_vault_unlock_failed)
|
||||
}
|
||||
showError(errorMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message when retrieving key from keychain fails.
|
||||
*/
|
||||
private fun showKeychainError(e: Exception) {
|
||||
val errorMessage = when {
|
||||
e.message?.contains("user canceled", ignoreCase = true) == true ||
|
||||
e.message?.contains("authentication failed", ignoreCase = true) == true ->
|
||||
getString(R.string.error_biometric_cancelled)
|
||||
else -> getString(R.string.error_encryption_key_failed)
|
||||
}
|
||||
showError(errorMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message in the loading view and display a close button.
|
||||
* Hides the loading indicator and shows the error state.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.aliasvault.app.credentialprovider
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
@@ -13,7 +14,6 @@ import net.aliasvault.app.credentialprovider.models.PasskeyRegistrationViewModel
|
||||
import net.aliasvault.app.utils.Helpers
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.KeystoreOperationCallback
|
||||
import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider
|
||||
import org.json.JSONObject
|
||||
|
||||
@@ -36,6 +36,7 @@ class PasskeyRegistrationActivity : FragmentActivity() {
|
||||
|
||||
private val viewModel: PasskeyRegistrationViewModel by viewModels()
|
||||
private lateinit var vaultStore: VaultStore
|
||||
private lateinit var unlockCoordinator: UnlockCoordinator
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -101,57 +102,46 @@ class PasskeyRegistrationActivity : FragmentActivity() {
|
||||
null
|
||||
}
|
||||
|
||||
// Show loading screen first
|
||||
// Show loading screen while unlock is in progress
|
||||
setContentView(R.layout.activity_loading)
|
||||
|
||||
// Check if biometric authentication is available before attempting unlock
|
||||
if (!vaultStore.isBiometricAuthEnabled()) {
|
||||
Log.e(TAG, "Biometric authentication is not enabled or not available")
|
||||
showError(getString(R.string.error_biometric_required))
|
||||
return
|
||||
}
|
||||
|
||||
// Add biometric prompt here to get decryption key and act as user verification as well
|
||||
// If biometric prompt is successful, we can proceed with the passkey registration
|
||||
// Create new keystore provider instance to avoid using the existing one
|
||||
val keystoreProvider = AndroidKeystoreProvider(applicationContext) { this }
|
||||
keystoreProvider.retrieveKeyExternal(
|
||||
this,
|
||||
object : KeystoreOperationCallback {
|
||||
override fun onSuccess(result: String) {
|
||||
try {
|
||||
Log.d(TAG, "Got decrypt key: ${result.length} bytes")
|
||||
// Biometric authentication successful, now proceed with passkey registration
|
||||
// (Re)unlock the vault now that the decryption key is available
|
||||
vaultStore.initEncryptionKey(result)
|
||||
vaultStore.unlockVault()
|
||||
runOnUiThread {
|
||||
proceedWithPasskeyRegistration(savedInstanceState)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to unlock vault after biometric auth", e)
|
||||
runOnUiThread {
|
||||
showUnlockError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(e: Exception) {
|
||||
Log.e(TAG, "Failed to retrieve encryption key", e)
|
||||
runOnUiThread {
|
||||
showKeychainError(e)
|
||||
}
|
||||
}
|
||||
// Initialize unlock coordinator
|
||||
unlockCoordinator = UnlockCoordinator(
|
||||
activity = this,
|
||||
vaultStore = vaultStore,
|
||||
onUnlocked = {
|
||||
// Vault unlocked successfully - proceed with passkey registration
|
||||
proceedWithPasskeyRegistration(savedInstanceState)
|
||||
},
|
||||
onCancelled = {
|
||||
// User cancelled unlock
|
||||
finish()
|
||||
},
|
||||
onError = { errorMessage ->
|
||||
// Error during unlock
|
||||
showError(errorMessage)
|
||||
},
|
||||
)
|
||||
|
||||
// Start the unlock flow
|
||||
unlockCoordinator.startUnlockFlow()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in onCreate", e)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// Delegate PIN unlock result to coordinator
|
||||
if (requestCode == UnlockCoordinator.REQUEST_CODE_PIN_UNLOCK) {
|
||||
unlockCoordinator.handlePinUnlockResult(resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceed with passkey registration after biometric authentication.
|
||||
* Proceed with passkey registration after authentication (biometric or PIN).
|
||||
*/
|
||||
private fun proceedWithPasskeyRegistration(savedInstanceState: Bundle?) {
|
||||
try {
|
||||
@@ -206,33 +196,6 @@ class PasskeyRegistrationActivity : FragmentActivity() {
|
||||
.commit()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message when unlocking vault fails.
|
||||
*/
|
||||
private fun showUnlockError(e: Exception) {
|
||||
val errorMessage = when {
|
||||
e.message?.contains("No encryption key found", ignoreCase = true) == true ->
|
||||
getString(R.string.error_unlock_vault_first)
|
||||
e.message?.contains("Database setup error", ignoreCase = true) == true ->
|
||||
getString(R.string.error_vault_decrypt_failed)
|
||||
else -> getString(R.string.error_vault_unlock_failed)
|
||||
}
|
||||
showError(errorMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message when retrieving key from keychain fails.
|
||||
*/
|
||||
private fun showKeychainError(e: Exception) {
|
||||
val errorMessage = when {
|
||||
e.message?.contains("user canceled", ignoreCase = true) == true ||
|
||||
e.message?.contains("authentication failed", ignoreCase = true) == true ->
|
||||
getString(R.string.error_biometric_cancelled)
|
||||
else -> getString(R.string.error_encryption_key_failed)
|
||||
}
|
||||
showError(errorMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message in the loading view and display a close button.
|
||||
* Hides the loading indicator and shows the error state.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user