diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000000..ff3f1ffc4c7 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +> 0.5%, last 3 major versions, Firefox ESR, not dead diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 47ebaf5189c..6809d950c4e 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -65,7 +65,7 @@ jobs: os: [ { base: "linux", distro: "ubuntu-22.04" }, - { base: "mac", distro: "macos-11" } + { base: "mac", distro: "macos-13" } ] license_type: [ diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index e73f882bb40..bebf7b5646c 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -444,10 +444,7 @@ jobs: macos-build: name: MacOS Build - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} @@ -605,10 +602,7 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - browser-build - macos-build @@ -814,10 +808,7 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - browser-build - macos-build @@ -1014,11 +1005,7 @@ jobs: macos-package-dev: name: MacOS Package Dev Release Asset - if: false # We need to look into how code signing works for dev - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - browser-build - macos-build @@ -1188,14 +1175,15 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Zip masdev asset - working-directory: ./dist/mas-dev-universal - run: zip -r Bitwarden-${{ env.PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app + run: | + cd dist/mas-dev-universal + zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip - path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip + path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip if-no-files-found: error diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 46f4ffad57d..0013234faa3 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -393,10 +393,7 @@ jobs: macos-build: name: MacOS Build - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} @@ -457,7 +454,7 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 - + az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 @@ -525,10 +522,7 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - setup - macos-build @@ -596,7 +590,7 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 - + az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 @@ -665,7 +659,7 @@ jobs: - name: Download artifact from hotfix-rc if: github.ref == 'refs/heads/hotfix-rc' - uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2 + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-browser.yml workflow_conclusion: success @@ -674,7 +668,7 @@ jobs: - name: Download artifact from rc if: github.ref == 'refs/heads/rc' - uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2 + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-browser.yml workflow_conclusion: success @@ -683,7 +677,7 @@ jobs: - name: Download artifacts from main if: ${{ github.ref != 'refs/heads/rc' && github.ref != 'refs/heads/hotfix-rc' }} - uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2 + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-browser.yml workflow_conclusion: success @@ -738,10 +732,7 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, - # as the newer versions will case the native modules to be incompatible with older macOS systems - # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules - runs-on: macos-11 + runs-on: macos-13 needs: - setup - macos-build @@ -804,7 +795,7 @@ jobs: az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/bitwarden-desktop-key | jq -r .value | base64 -d > $HOME/certificates/bitwarden-desktop-key.p12 - + az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/appstore-app-cert | jq -r .value | base64 -d > $HOME/certificates/appstore-app-cert.p12 @@ -873,7 +864,7 @@ jobs: - name: Download artifact from hotfix-rc if: github.ref == 'refs/heads/hotfix-rc' - uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2 + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-browser.yml workflow_conclusion: success @@ -882,7 +873,7 @@ jobs: - name: Download artifact from rc if: github.ref == 'refs/heads/rc' - uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2 + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-browser.yml workflow_conclusion: success @@ -891,7 +882,7 @@ jobs: - name: Download artifact from main if: ${{ github.ref != 'refs/heads/rc' && github.ref != 'refs/heads/hotfix-rc' }} - uses: dawidd6/action-download-artifact@71072fbb1229e1317f1a8de6b04206afb461bd67 # v3.1.2 + uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-browser.yml workflow_conclusion: success diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c650d8a622..12649b91ea9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,10 +48,12 @@ jobs: # Tests in apps/ are typechecked when their app is built, so we just do it here for libs/ # See https://bitwarden.atlassian.net/browse/EC-497 - name: Run typechecking - run: npm run test:types --coverage + run: npm run test:types - name: Run tests - run: npm run test --coverage + # maxWorkers is a workaround for a memory leak that crashes tests in CI: + # https://github.com/facebook/jest/issues/9430#issuecomment-1149882002 + run: npm test -- --coverage --maxWorkers=3 - name: Report test results uses: dorny/test-reporter@eaa763f6ffc21c7a37837f56cd5f9737f27fc6c8 # v1.8.0 diff --git a/apps/browser/package.json b/apps/browser/package.json index 25912c4832f..a295a0f5bfe 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.5.1", + "version": "2024.6.0", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index c4a07015412..c4251626401 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "إلغاء القفل" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "إظهار خيارات قائمة السياق" }, @@ -799,12 +802,39 @@ "message": "داكن مُشمس", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "تصدير الخزنة" }, "fileFormat": { "message": "صيغة الملف" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "تحذير", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "خطأ" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 8b3632da1ba..129aac00ee2 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Kilidi aç" }, + "additionalOptions": { + "message": "Əlavə seçimlər" + }, "enableContextMenuItem": { "message": "Konteks menyu seçimlərini göstər" }, @@ -799,12 +802,39 @@ "message": "Günəşli tünd", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Buradan xaricə köçür" + }, "exportVault": { "message": "Anbarı xaricə köçür" }, "fileFormat": { "message": "Fayl formatı" }, + "fileEncryptedExportWarningDesc": { + "message": "Bu faylın xaricə köçürülməsi, parolla qorunacaq və şifrəsini açmaq üçün fayl parolu tələb olunacaq." + }, + "filePassword": { + "message": "Fayl parolu" + }, + "exportPasswordDescription": { + "message": "Bu parol, bu faylı daxilə və xaricə köçürmək üçün istifadə olunacaq" + }, + "accountRestrictedOptionDescription": { + "message": "Xaricə köçürməni şifrələmək və daxilə köçürməni yalnız mövcud Bitwarden hesabı ilə məhdudlaşdırmaq üçün hesabınızın istifadəçi adı və Ana Parolundan əldə edilən hesab şifrələmə açarınızı istifadə edin." + }, + "passwordProtectedOptionDescription": { + "message": "Xaricə köçürməni şifrələmək üçün bir fayl parolu təyin edin və şifrəni açma parolunu istifadə edərək bunu istənilən Bitwarden hesabına köçürün." + }, + "exportTypeHeading": { + "message": "Xaricə köçürmə növü" + }, + "accountRestricted": { + "message": "Hesab məhdudlaşdırıldı" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Fayl parolu\" və \"Fayl parolunu təsdiqlə\" uyuşmur." + }, "warning": { "message": "XƏBƏRDARLIQ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Təşkilat anbarını xaricə köçürmə" + }, + "exportingOrganizationVaultDesc": { + "message": "Yalnız $ORGANIZATION$ ilə əlaqələndirilmiş təşkilat anbarı ixrac ediləcək. Fərdi anbardakı və digər təşkilat elementlər daxil edilmir.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Xəta" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 5409816accf..6ee3fffdfa5 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Разблакіраваць" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Паказваць параметры кантэкстнага меню" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Экспартаваць сховішча" }, "fileFormat": { "message": "Фармат файла" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "ПАПЯРЭДЖАННЕ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Памылка" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index e6f6cfc14d8..337467ddfe9 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Отключване" }, + "additionalOptions": { + "message": "Допълнителни настройки" + }, "enableContextMenuItem": { "message": "Показване на опции в контекстното меню" }, @@ -799,12 +802,39 @@ "message": "Преекспонирано тъмен", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Изнасяне от" + }, "exportVault": { "message": "Изнасяне на трезора" }, "fileFormat": { "message": "Формат на файла" }, + "fileEncryptedExportWarningDesc": { + "message": "Изнесеният файл ще бъде защитен с парола, която ще бъде необходима за дешифриране на файла." + }, + "filePassword": { + "message": "Парола на файла" + }, + "exportPasswordDescription": { + "message": "Парола ще се използва при изнасянето и при внасянето на този файл" + }, + "accountRestrictedOptionDescription": { + "message": "Използвайте ключа си за шифриране, който се получава чрез комбиниране на потребителското име на регистрацията Ви и главната парола. С него изнасянето ще се шифрира и внасянето ще бъда възможно само в текущата регистрация в Битуорден." + }, + "passwordProtectedOptionDescription": { + "message": "Задайте парола за файла, за да шифровате изнесените данни. Ще можете да внесете данните във всяка регистрация в Битуорден използвайки паролата за дешифриране." + }, + "exportTypeHeading": { + "message": "Вид изнасяне" + }, + "accountRestricted": { + "message": "Регистрацията е ограничена" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "Дънните в полетата „Парола на файла“ и „Потвърждаване на паролата на файла“ не съвпадат." + }, "warning": { "message": "ВНИМАНИЕ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Изнасяне на трезора на организацията" + }, + "exportingOrganizationVaultDesc": { + "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$. Записите в отделните лични трезори и тези в други организации няма да бъдат включени.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Грешка" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 1635de5bb8b..541bebd427b 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "ভল্ট রফতানি" }, "fileFormat": { "message": "ফাইলের ধরণ" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "সতর্কতা", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index db55f4784e4..cd82d91cefc 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 01f66bffd64..fe2a25e6595 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Desbloqueja" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Mostra les opcions del menú contextual" }, @@ -799,12 +802,39 @@ "message": "Solaritzat fosc", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exporta caixa forta" }, "fileFormat": { "message": "Format de fitxer" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "ADVERTIMENT", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 2939bba1468..3b9507fdf21 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Odemknout" }, + "additionalOptions": { + "message": "Další volby" + }, "enableContextMenuItem": { "message": "Zobrazit volby v kontextovém menu" }, @@ -799,12 +802,39 @@ "message": "Tmavý (solarizovaný)", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Exportovat z" + }, "exportVault": { "message": "Exportovat trezor" }, "fileFormat": { "message": "Formát souboru" }, + "fileEncryptedExportWarningDesc": { + "message": "Tento soubor exportu bude chráněn heslem a k dešifrování bude vyžadovat heslo souboru." + }, + "filePassword": { + "message": "Heslo souboru" + }, + "exportPasswordDescription": { + "message": "Toto heslo bude použito pro export a import tohoto souboru" + }, + "accountRestrictedOptionDescription": { + "message": "Pro zašifrování exportu a omezení importu pouze na aktuální účet Bitwardenu použijte šifrovací klíč Vašeho účtu odvozený z uživatelského jména a hlavního hesla." + }, + "passwordProtectedOptionDescription": { + "message": "Nastavte heslo pro šifrování exportu a importujte ho do libovolného účtu Bitwardenu pomocí hesla pro dešifrování." + }, + "exportTypeHeading": { + "message": "Typ exportu" + }, + "accountRestricted": { + "message": "Účet je omezený" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Heslo souboru\" a \"Potvrzení hesla souboru\" se neshodují." + }, "warning": { "message": "VAROVÁNÍ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exportování trezoru organizace" + }, + "exportingOrganizationVaultDesc": { + "message": "Exportován bude jen trezor organizace přidružený k položce $ORGANIZATION$. Osobní položky trezoru a položky z jiných organizací nebudou zahrnuty.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Chyba" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 58f7bc95446..6bfa7de69f3 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Datgloi" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Allforio'r gell" }, "fileFormat": { "message": "Fformat y ffeil" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "RHYBUDD", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Gwall" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 8f27f95be02..32266ceb31f 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Oplås" }, + "additionalOptions": { + "message": "Yderligere indstillinger" + }, "enableContextMenuItem": { "message": "Vis indstillinger i kontekstmenuen" }, @@ -799,12 +802,39 @@ "message": "Solariseret mørk", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Eksportér fra" + }, "exportVault": { "message": "Eksportér boks" }, "fileFormat": { "message": "Filformat" }, + "fileEncryptedExportWarningDesc": { + "message": "Denne fileksport vil være adgangskodebeskyttet og kræve filadgangskoden at dekryptere." + }, + "filePassword": { + "message": "Filadgangskode" + }, + "exportPasswordDescription": { + "message": "Denne adgangskode vil blive brugt ved eksport og import af denne fil" + }, + "accountRestrictedOptionDescription": { + "message": "Brug kontokrypteringsnøglen, dannet af kontobrugernavn og Hovedadgangskode, for at kryptere eksporten og hindre import til andre end den aktuelle Bitwarden-konto." + }, + "passwordProtectedOptionDescription": { + "message": "Opsæt en adgangskode til både at kryptere eksporten samt dekryptere denne ved import til enhver Bitwarden-konto." + }, + "exportTypeHeading": { + "message": "Eksporttype" + }, + "accountRestricted": { + "message": "Konto begrænset" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“Filadgangskode” og “Bekræft filadgangskode“ matcher ikke." + }, "warning": { "message": "ADVARSEL", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Eksport af organisationsboks" + }, + "exportingOrganizationVaultDesc": { + "message": "Kun organisationsboksen tilknyttet $ORGANIZATION$ eksporteres. Emner i individuelle bokse eller andre organisationer medtages ikke.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Fejl" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8a7c3e0067d..bec6702ae28 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Entsperren" }, + "additionalOptions": { + "message": "Weitere Optionen" + }, "enableContextMenuItem": { "message": "Kontextmenüoptionen anzeigen" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export aus" + }, "exportVault": { "message": "Tresor exportieren" }, "fileFormat": { "message": "Dateiformat" }, + "fileEncryptedExportWarningDesc": { + "message": "Dieser Datei-Export ist passwortgeschützt und erfordert das Dateipasswort zum Entschlüsseln." + }, + "filePassword": { + "message": "Dateipasswort" + }, + "exportPasswordDescription": { + "message": "Dieses Passwort wird zum Exportieren und Importieren dieser Datei verwendet" + }, + "accountRestrictedOptionDescription": { + "message": "Verwende den Verschlüsselungsschlüssel deines Kontos, abgeleitet vom Benutzernamen und Master-Passwort, um den Export zu verschlüsseln und den Import auf das aktuelle Bitwarden-Konto zu beschränken." + }, + "passwordProtectedOptionDescription": { + "message": "Lege ein Dateipasswort fest, um den Export zu verschlüsseln und importiere ihn in ein beliebiges Bitwarden-Konto, wobei das Passwort zum Entschlüsseln genutzt wird." + }, + "exportTypeHeading": { + "message": "Exporttyp" + }, + "accountRestricted": { + "message": "Konto eingeschränkt" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "„Dateipasswort“ und „Dateipasswort bestätigen“ stimmen nicht überein." + }, "warning": { "message": "ACHTUNG", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Tresor der Organisation wird exportiert" + }, + "exportingOrganizationVaultDesc": { + "message": "Nur der mit $ORGANIZATION$ verbundene Organisationstresor wird exportiert. Einträge in persönlichen Tresoren oder anderen Organisationen werden nicht berücksichtigt.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Fehler" }, diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 365f8475a07..da376f6c6a0 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Ξεκλείδωμα" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Εμφάνιση επιλογών μενού περιβάλλοντος" }, @@ -799,12 +802,39 @@ "message": "Solarized Σκούρο", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Εξαγωγή Vault" }, "fileFormat": { "message": "Μορφή αρχείου" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Σφάλμα" }, diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 426f570d643..231637d7af3 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -185,7 +185,7 @@ "message": "Continue to browser extension store?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." }, "changeMasterPasswordOnWebConfirmation": { "message": "You can change your master password on the Bitwarden web app." @@ -224,7 +224,7 @@ }, "continueToAuthenticatorPageDesc": { "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" - }, + }, "bitwardenSecretsManager": { "message": "Bitwarden Secrets Manager" }, @@ -389,6 +389,9 @@ "favorite": { "message": "Favorite" }, + "unfavorite": { + "message": "Unfavorite" + }, "notes": { "message": "Notes" }, @@ -410,6 +413,9 @@ "launch": { "message": "Launch" }, + "launchWebsite": { + "message": "Launch website" + }, "website": { "message": "Website" }, @@ -599,6 +605,9 @@ "loggedOut": { "message": "Logged out" }, + "loggedOutDesc": { + "message": "You have been logged out of your account." + }, "loginExpired": { "message": "Your login session has expired." }, @@ -819,7 +828,7 @@ }, "exportPasswordDescription": { "message": "This password will be used to export and import this file" - }, + }, "accountRestrictedOptionDescription": { "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." }, @@ -1107,6 +1116,15 @@ "selfHostedEnvironmentFooter": { "message": "Specify the base URL of your on-premises hosted Bitwarden installation." }, + "selfHostedBaseUrlHint": { + "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + }, + "selfHostedCustomEnvHeader" :{ + "message": "For advanced configuration, you can specify the base URL of each service independently." + }, + "selfHostedEnvFormInvalid" :{ + "message": "You must add either the base Server URL or at least one custom environment." + }, "customEnvironment": { "message": "Custom environment" }, @@ -1419,6 +1437,15 @@ "collections": { "message": "Collections" }, + "nCollections": { + "message": "$COUNT$ collections", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "favorites": { "message": "Favorites" }, @@ -1648,6 +1675,9 @@ "autoFillAndSave": { "message": "Auto-fill and save" }, + "fillAndSave": { + "message": "Fill and save" + }, "autoFillSuccessAndSavedUri": { "message": "Item auto-filled and URI saved" }, @@ -1744,6 +1774,12 @@ "ok": { "message": "Ok" }, + "errorRefreshingAccessToken":{ + "message": "Access Token Refresh Error" + }, + "errorRefreshingAccessTokenDesc":{ + "message": "No refresh token or API keys found. Please try logging out and logging back in." + }, "desktopSyncVerificationTitle": { "message": "Desktop sync verification" }, @@ -3263,7 +3299,7 @@ "clearFiltersOrTryAnother": { "message": "Clear filters or try another search term" }, - "copyInfo": { + "copyInfoLabel": { "message": "Copy info, $ITEMNAME$", "description": "Aria label for a button that opens a menu with options to copy information from an item.", "placeholders": { @@ -3273,7 +3309,37 @@ } } }, - "moreOptions": { + "copyInfoTitle": { + "message": "Copy info - $ITEMNAME$", + "description": "Title for a button that opens a menu with options to copy information from an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + } + } + }, + "copyNoteLabel": { + "message": "Copy Note, $ITEMNAME$", + "description": "Aria label for a button copies a note to the clipboard.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Note Item" + } + } + }, + "copyNoteTitle": { + "message": "Copy Note - $ITEMNAME$", + "description": "Title for a button copies a note to the clipboard.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Note Item" + } + } + }, + "moreOptionsLabel": { "message": "More options, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { @@ -3283,6 +3349,38 @@ } } }, + "moreOptionsTitle": { + "message": "More options - $ITEMNAME$", + "description": "Title for a button that opens a menu with more options for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + } + } + }, + "viewItemTitle": { + "message": "View item - $ITEMNAME$", + "description": "Title for a link that opens a view for an item.", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret Item" + } + } + }, + "assignCollections": { + "message": "Assign collections" + }, + "copyEmail": { + "message": "Copy email" + }, + "copyPhone": { + "message": "Copy phone" + }, + "copyAddress": { + "message": "Copy address" + }, "adminConsole": { "message": "Admin Console" }, @@ -3333,5 +3431,14 @@ "example": "Work" } } + }, + "itemsWithNoFolder": { + "message": "Items with no folder" + }, + "organizationIsDeactivated": { + "message": "Organization is deactivated" + }, + "contactYourOrgAdmin": { + "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index ffde447604b..0ff4085900c 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organisation vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organisations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 3ceedc40deb..bb1ae5f7318 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarised Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organisation vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organisations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 3550fa68c03..1134f941513 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Desbloquear" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Mostrar las opciones de menú contextuales" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Exportar desde" + }, "exportVault": { "message": "Exportar caja fuerte" }, "fileFormat": { "message": "Formato de archivo" }, + "fileEncryptedExportWarningDesc": { + "message": "Esta exportación de archivo estará protegida por contraseña y requerirá la contraseña del archivo para descifrarla." + }, + "filePassword": { + "message": "Contraseña del archivo" + }, + "exportPasswordDescription": { + "message": "Esta contraseña se utilizará para exportar e importar este archivo" + }, + "accountRestrictedOptionDescription": { + "message": "Utiliza la clave de cifrado de tu cuenta, derivada del nombre de usuario y la contraseña maestra de tu cuenta, para cifrar la exportación y restringir la importación solo a la cuenta actual de Bitwarden." + }, + "passwordProtectedOptionDescription": { + "message": "Establece una contraseña de archivo para cifrar la exportación e importarlo a cualquier cuenta de Bitwarden utilizando la contraseña para el descifrado." + }, + "exportTypeHeading": { + "message": "Tipo de exportación" + }, + "accountRestricted": { + "message": "Cuenta restringida" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Contraseña de archivo\" y \"Confirmar contraseña de archivo\" no coinciden." + }, "warning": { "message": "ADVERTENCIA", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exportando caja fuerte de la organización" + }, + "exportingOrganizationVaultDesc": { + "message": "Solo se exportará la caja fuerte de la organización asociada a $ORGANIZATION$. Los elementos en las cajas fuertes individuales o de otras organizaciones no serán incluidos.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 8fb5274c52f..03267c79d7b 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Lukusta lahti" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Kuva parema kliki menüü valikud" }, @@ -799,12 +802,39 @@ "message": "Solarized tume", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Ekspordi hoidla" }, "fileFormat": { "message": "Failivorming" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "HOIATUS", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Viga" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 60339f16b14..3d0e37f4147 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Erakutsi laster-menuko aukerak" }, @@ -799,12 +802,39 @@ "message": "Solarized iluna", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Esportatu kutxa gotorra" }, "fileFormat": { "message": "Fitxategiaren formatua" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "KONTUZ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Akatsa" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index a493a1c24bf..5b55561a6a8 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "باز کردن قفل" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "نمایش گزینه‌های منوی زمینه" }, @@ -799,12 +802,39 @@ "message": "تاریک خورشیدی", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "برون ریزی گاوصندوق" }, "fileFormat": { "message": "فرمت پرونده" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "اخطار", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "خطا" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 05d2d3ed952..42251e9e42b 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Avaa" }, + "additionalOptions": { + "message": "Lisäasetukset" + }, "enableContextMenuItem": { "message": "Näytä sisältövalikon valinnat" }, @@ -799,12 +802,39 @@ "message": "Solarized, tumma", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Vie lähteestä" + }, "exportVault": { "message": "Vie holvi" }, "fileFormat": { "message": "Tiedostomuoto" }, + "fileEncryptedExportWarningDesc": { + "message": "Tämä vientitiedosto suojataan salasanalla, joka on syötettävä ja salauksen purkamiseksi." + }, + "filePassword": { + "message": "Tiedoston salasana" + }, + "exportPasswordDescription": { + "message": "Tätä salasanaa käytetään tämän tiedoston viennissä ja tuonnissa" + }, + "accountRestrictedOptionDescription": { + "message": "Salaa vienti ja rajoita tuonti vain nykyiselle Bitwarden-tilille tilisi käyttäjätunnukseen ja pääsalasanaan pohjautuvalla salausavaimella." + }, + "passwordProtectedOptionDescription": { + "message": "Salaa vientitiedosto salasanalla, joka mahdollistaa sen tuonnin mille tahansa Bitwarden-tilille." + }, + "exportTypeHeading": { + "message": "Viennin tyyppi" + }, + "accountRestricted": { + "message": "Rajoitettu tilille" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Tiedoston salasana\" ja \"Vahvista tiedoston salasana\" eivät täsmää." + }, "warning": { "message": "VAROITUS", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Organisaation holvin vienti" + }, + "exportingOrganizationVaultDesc": { + "message": "Vain organisaatioon $ORGANIZATION$ liitetyn holvin kohteet viedään. Yksityisen holvin ja muiden organisaatioiden kohteita ei sisällytetä.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Virhe" }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 061da97446b..86f8209e1cd 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "I-unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Ipakita ang mga opsyon ng menu ng konteksto" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "I-export vault" }, "fileFormat": { "message": "Format ng file" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "BABALA", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Mali" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index c062390f228..53df48d0004 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Déverrouiller" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Afficher les options du menu contextuel" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exporter le coffre" }, "fileFormat": { "message": "Format de fichier" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "AVERTISSEMENT", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Erreur" }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b92e05269eb..dd29c8a071c 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Desbloquear" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarizado escuro", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exportar caixa forte" }, "fileFormat": { "message": "Formato de ficheiro" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "ADVERTENCIA", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Erro" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index fd49f7e55e8..c191df36bf8 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "יצוא כספת" }, "fileFormat": { "message": "פורמט קובץ" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "אזהרה", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "שגיאה" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 48c55b30a83..d6ac94bec32 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "संदर्भ मेनू विकल्प दिखाएं" }, @@ -799,12 +802,39 @@ "message": "सौरीकृत अंधेरा", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export Vault" }, "fileFormat": { "message": "File Format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "चेतावनी", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "एरर" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index ac452af85ee..8b9f96fa5c4 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Otključaj" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Prikaži opcije kotekstualnog izbornika" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Izvezi trezor" }, "fileFormat": { "message": "Format datoteke" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "UPOZORENJE", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Pogreška" }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 8b937375a63..8ff50ea4f08 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Feloldás" }, + "additionalOptions": { + "message": "Kiegészítő opciók" + }, "enableContextMenuItem": { "message": "Helyi menü opciók megjelenítése" }, @@ -799,12 +802,39 @@ "message": "Szolarizált sötét", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Exportálás innen:" + }, "exportVault": { "message": "Széf exportálása" }, "fileFormat": { "message": "Fájlformátum" }, + "fileEncryptedExportWarningDesc": { + "message": "Ez a fájl exportálás jelszóval védett és a visszafejtéshez a fájl jelszó megadása szükséges." + }, + "filePassword": { + "message": "Fájl jelszó" + }, + "exportPasswordDescription": { + "message": "Ezt a jelszó kerül használatba a fájl exportálására és importálására." + }, + "accountRestrictedOptionDescription": { + "message": "Használjuk a fiók felhasználónevéből és mesterjelszavából származó fióktitkosítási kulcsot az exportálás titkosításához és az importálást csak az aktuális Bitwarden fiókra korlátozzuk." + }, + "passwordProtectedOptionDescription": { + "message": "Állítsunk be egy fájl jelszót az exportálás titkosításához és importáljuk azt bármely Bitwarden fiókba a visszafejtéshez használt jelszó használatával." + }, + "exportTypeHeading": { + "message": "Exportálási típus" + }, + "accountRestricted": { + "message": "Korlátozott fiók" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "A “Fájl jelszó” és a “Fájl jelszó megerősítés“ nem egyezik." + }, "warning": { "message": "FIGYELEM", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Szervezeti széf exportálása" + }, + "exportingOrganizationVaultDesc": { + "message": "Csak $ORGANIZATION$ névvel társított szervezeti széf kerül exportálásra. Ebbe nem kerülnek be a személyes és más szervezeti széf elemek.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Hiba" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index b151dbbf21f..19770da2782 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Gelap Solarized", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Ekspor Brankas" }, "fileFormat": { "message": "Format Berkas" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "PERINGATAN", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Galat" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 938eb7671e0..614068b1f4d 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Sblocca" }, + "additionalOptions": { + "message": "Opzioni aggiuntive" + }, "enableContextMenuItem": { "message": "Mostra opzioni nel menu contestuale" }, @@ -799,12 +802,39 @@ "message": "Scuro solarizzato", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Esporta da" + }, "exportVault": { "message": "Esporta cassaforte" }, "fileFormat": { "message": "Formato file" }, + "fileEncryptedExportWarningDesc": { + "message": "Questo file esportato sarà protetto e richiederà la password del file per decifrarlo." + }, + "filePassword": { + "message": "Password del file" + }, + "exportPasswordDescription": { + "message": "La password sarà utilizzata per importare ed esportare questo file" + }, + "accountRestrictedOptionDescription": { + "message": "Usa la chiave di crittografia dell'account, derivata dal nome utente e dalla password principale del tuo account, per crittografare il file di esportazione e limitare l'importazione solo all'account Bitwarden corrente." + }, + "passwordProtectedOptionDescription": { + "message": "Imposta una password del file per crittografare il file esportato e importarlo in qualsiasi account Bitwarden usando la password per decrittografarlo." + }, + "exportTypeHeading": { + "message": "Tipo di esportazione" + }, + "accountRestricted": { + "message": "Account limitato" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "Le due password del file non corrispondono." + }, "warning": { "message": "ATTENZIONE", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Esportando cassaforte dell'organizzazione" + }, + "exportingOrganizationVaultDesc": { + "message": "Solo la cassaforte dell'organizzazione associata a $ORGANIZATION$ sarà esportata. Elementi nelle casseforti individuali o in altre organizzazioni non saranno inclusi.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Errore" }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index cb78bd7fda5..9113b95a7d3 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "ロック解除" }, + "additionalOptions": { + "message": "追加オプション" + }, "enableContextMenuItem": { "message": "コンテキストメニューオプションを表示" }, @@ -799,12 +802,39 @@ "message": "Solarized ダーク", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "エクスポート元" + }, "exportVault": { "message": "保管庫のエクスポート" }, "fileFormat": { "message": "ファイル形式" }, + "fileEncryptedExportWarningDesc": { + "message": "エクスポートするファイルはパスワードで保護され、復号するにはファイルパスワードが必要になります。" + }, + "filePassword": { + "message": "ファイルパスワード" + }, + "exportPasswordDescription": { + "message": "このパスワードはこのファイルのエクスポートとインポート時に使用します" + }, + "accountRestrictedOptionDescription": { + "message": "アカウントのユーザー名とマスターパスワードから得られる暗号化キーを使用してエクスポートするデータを暗号化し、現在の Bitwarden アカウントのみがインポートできるよう制限します。" + }, + "passwordProtectedOptionDescription": { + "message": "エクスポートを暗号化するためのファイルパスワードを設定します。そのパスワードを使用して、任意の Bitwarden アカウントにインポートします。" + }, + "exportTypeHeading": { + "message": "エクスポートの種類" + }, + "accountRestricted": { + "message": "アカウント制限" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "「ファイルパスワード」と「ファイルパスワードの確認」が一致しません。" + }, "warning": { "message": "警告", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "組織保管庫のエクスポート" + }, + "exportingOrganizationVaultDesc": { + "message": "$ORGANIZATION$ に関連付けられた組織保管庫のみがエクスポートされます。個々の保管庫または他の組織にあるアイテムは含まれません。", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "エラー" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 62347f4acf4..6abd24dafc8 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 64f3623b5af..c3f03166b25 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 7556ee6afd6..e5e1f708f66 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "ಡಾರ್ಕ್ ಸೌರ", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "ರಫ್ತು ವಾಲ್ಟ್" }, "fileFormat": { "message": "ಕಡತದ ಮಾದರಿ" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "ಎಚ್ಚರಿಕೆ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 86295d469a7..552446ef766 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "잠금 해제" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "보관함 내보내기" }, "fileFormat": { "message": "파일 형식" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "경고", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "오류" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 6f0fc1bbb62..5b426e47d15 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -423,7 +423,7 @@ "message": "Kita" }, "unlockMethods": { - "message": "Unlock options" + "message": "Atrakinti parinktis" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Nustatyk atrakinimo būdą, kad pakeistum saugyklos laiko limito veiksmą." @@ -432,10 +432,10 @@ "message": "Nustatykite nustatymuose atrakinimo metodą" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Baigėsi seanso laikas" }, "otherOptions": { - "message": "Other options" + "message": "Kitos parinktys" }, "rateExtension": { "message": "Įvertinkite šį plėtinį" @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Atrakinti" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Rodyti kontekstinio meniu pasririnkimus" }, @@ -799,12 +802,39 @@ "message": "Saulėtas tamsą", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Eksportuoti saugyklą" }, "fileFormat": { "message": "Failo formatas" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "ĮSPĖJIMAS", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Klaida" }, @@ -2232,7 +2274,7 @@ "message": "Sugeneruoti el. pašto slapyvardį su išorine persiuntimo paslauga." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "„$SERVICENAME$“ klaida: $ERRORMESSAGE$.", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2246,11 +2288,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Sugeneravo „Bitwarden“.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Svetainė: $WEBSITE$. Sugeneravo „Bitwarden“.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2260,7 +2302,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Netinkamas „$SERVICENAME$“ API prieigos raktas.", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2270,7 +2312,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Netinkamas „$SERVICENAME$“ API prieigos raktas: $ERRORMESSAGE$.", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2284,7 +2326,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Nepavyksta gauti „$SERVICENAME$“ užmaskuoto el. pašto paskyros ID.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2294,7 +2336,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Netinkamas „$SERVICENAME$“ domenas.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2304,7 +2346,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Netinkamas „$SERVICENAME$“ URL.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2314,7 +2356,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Įvyko nežinoma „$SERVICENAME$“ klaida.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2324,7 +2366,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Nežinomas persiuntėjas: „$SERVICENAME$“.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3245,13 +3287,13 @@ "message": "Administratoriaus konsolės" }, "accountSecurity": { - "message": "Account security" + "message": "Paskyros saugumas" }, "notifications": { - "message": "Notifications" + "message": "Pranešimai" }, "appearance": { - "message": "Appearance" + "message": "Išvaizda" }, "errorAssigningTargetCollection": { "message": "Klaida priskiriant tikslinę kolekciją." diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index a7b50ae2a47..ac202682bab 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Atslēgt" }, + "additionalOptions": { + "message": "Papildu iespējas" + }, "enableContextMenuItem": { "message": "Rādīt konteksta izvēlnes iespējas" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Izgūt no" + }, "exportVault": { "message": "Izgūt glabātavas saturu" }, "fileFormat": { "message": "Datnes veids" }, + "fileEncryptedExportWarningDesc": { + "message": "Šī datņu izgūšana būs aizsargāta ar paroli, un būs nepieciešama datnes parole, lai to atšifrētu." + }, + "filePassword": { + "message": "Datnes parole" + }, + "exportPasswordDescription": { + "message": "Šī parole tiks izmantota, lai izgūtu un ievietotu šo datni" + }, + "accountRestrictedOptionDescription": { + "message": "Jāizmanto konta šifrēšanas atslēga, kas iegūta no lietotājvārda un galvenās paroles, lai šifrētu izguvi un atļautu ievietošanu tikai pašreizējā Bitwarden kontā." + }, + "passwordProtectedOptionDescription": { + "message": "Uzstādīt paroli, lai šifrētu izguvi un tad to ievietotu jebkurā Bitwarden kontā, izmantojot atšifrēšanas paroli." + }, + "exportTypeHeading": { + "message": "Izgūšanas veids" + }, + "accountRestricted": { + "message": "Konts ir ierobežots" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Datnes parole\" un \"Apstiprināt datnes paroli\" vērtības nesakrīt." + }, "warning": { "message": "UZMANĪBU", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -813,7 +843,7 @@ "message": "Apstiprināt glabātavas satura izgūšanu" }, "exportWarningDesc": { - "message": "Šī izguve satur glabātavas datus nešifrētā veidā. Izdoto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Izdzēst to uzreiz pēc izmantošanas." + "message": "Šī izguve satur glabātavas datus nešifrētā veidā. Izgūto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Tā ir jāizdzēš uzreiz pēc izmantošanas." }, "encExportKeyWarningDesc": { "message": "Šī izguve šifrē datus ar konta šifrēšanas atslēgu. Ja tā jebkad tiks mainīta, izvadi vajadzētu veikt vēlreiz, jo vairs nebūs iespējams atšifrēt šo datni." @@ -2171,7 +2201,7 @@ "message": "Sesijai iestājās noildze. Lūgums mēģināt pieteikties vēlreiz." }, "exportingPersonalVaultTitle": { - "message": "Izdod personīgo glabātavu" + "message": "Izgūst personīgo glabātavu" }, "exportingIndividualVaultDescription": { "message": "Tiks izgūti tikai atsevišķi glabātavas vienumi, kas ir saistīti ar $EMAIL$. Apvienības glabātavas vienumi netiks iekļauti. Tiks izgūta tikai glabātavas vienumu informācija, un saistītie pielikumi netiks iekļauti.", @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Izgūst apvienības glabātavu" + }, + "exportingOrganizationVaultDesc": { + "message": "Tiks izgūta tikai apvienības glabātava, kas ir saistīta ar $ORGANIZATION$. Atsevišķu glabātavu vai citu apvienību vienumi netiks iekļauti.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Kļūda" }, @@ -2907,7 +2949,7 @@ "message": "Kļūda izguves datnes atšifrēšanā. Izmantotā atslēga neatbilst tai, kas tika izmantota satura izgūšanai." }, "invalidFilePassword": { - "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izdošanas datnes izveidošanas brīdī." + "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izgūšanas datnes izveidošanas brīdī." }, "importDestination": { "message": "Ievietošanas galamērķis" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index fba3e5486a5..d2a9b2db389 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "വാൾട് എക്സ്പോർട്" }, "fileFormat": { "message": "ഫയൽ ഫോർമാറ്റ്" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "മുന്നറിയിപ്പ്", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index fe984cb8302..6407be2c68d 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 64f3623b5af..c3f03166b25 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 6993e1f648c..d2feb8798e5 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Lås opp" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Vis alternativer for kontekstmeny" }, @@ -799,12 +802,39 @@ "message": "Solarisert mørk", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Eksporter hvelvet" }, "fileFormat": { "message": "Filformat" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "ADVARSEL", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Feil" }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 64f3623b5af..c3f03166b25 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 36a8ad9470a..07c69ec596c 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Ontgrendelen" }, + "additionalOptions": { + "message": "Extra instellingen" + }, "enableContextMenuItem": { "message": "Contextmenu-opties weergeven" }, @@ -799,12 +802,39 @@ "message": "Overbelicht donker", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Exporteren vanuit" + }, "exportVault": { "message": "Kluis exporteren" }, "fileFormat": { "message": "Bestandsindeling" }, + "fileEncryptedExportWarningDesc": { + "message": "We beveiligen deze bestandsexport met een wachtwoord beveiligd, je hebt het bestandswachtwoord nodig om het te decoderen." + }, + "filePassword": { + "message": "Bestandswachtwoord" + }, + "exportPasswordDescription": { + "message": "We gebruiken dit wachtwoord bij het exporteren en importeren van dit bestand" + }, + "accountRestrictedOptionDescription": { + "message": "Gebruik de encryptiesleutel van je account, afgeleid van je gebruikersnaam en hoodfwachtwoord, om de export te versleutelen en importeren te beperken tot het huidige Bitwarden-account." + }, + "passwordProtectedOptionDescription": { + "message": "Stel een bestandswachtwoord in om de export te versleutelen en te importeren naar een willekeurig Bitwarden-account met het wachtwoord voor decoderen." + }, + "exportTypeHeading": { + "message": "Exporttype" + }, + "accountRestricted": { + "message": "Account beperkt" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Bestandswachtwoord\" en \"Bestandswachtwoord bevestigen\" komen niet overeen." + }, "warning": { "message": "WAARSCHUWING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Organisatiekluis exporteren" + }, + "exportingOrganizationVaultDesc": { + "message": "Exporteert alleen de organisatiekluis van $ORGANIZATION$. Geen persoonlijke kluis-items of items van andere organisaties.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Fout" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 64f3623b5af..c3f03166b25 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 64f3623b5af..c3f03166b25 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index d5e3940682f..c9959508c95 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Odblokuj" }, + "additionalOptions": { + "message": "Dodatkowe opcje" + }, "enableContextMenuItem": { "message": "Pokaż opcje menu kontekstowego" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Eksportuj z" + }, "exportVault": { "message": "Eksportuj sejf" }, "fileFormat": { "message": "Format pliku" }, + "fileEncryptedExportWarningDesc": { + "message": "Plik będzie chroniony hasłem, które będzie wymagane do odszyfrowania pliku." + }, + "filePassword": { + "message": "Hasło do pliku" + }, + "exportPasswordDescription": { + "message": "Hasło będzie używane do eksportowania i importowania pliku" + }, + "accountRestrictedOptionDescription": { + "message": "Użyj klucza szyfrowania konta, pochodzącego z nazwy użytkownika konta i hasła głównego, aby zaszyfrować eksport i ograniczyć import tylko do bieżącego konta Bitwarden." + }, + "passwordProtectedOptionDescription": { + "message": "Ustaw hasło dla pliku, aby zaszyfrować eksport i zaimportować je na dowolne konto Bitwarden przy użyciu hasła do odszyfrowania." + }, + "exportTypeHeading": { + "message": "Rodzaj eksportu" + }, + "accountRestricted": { + "message": "Konto ograniczone" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“Hasło pliku” i “Potwierdź hasło pliku“ nie pasują do siebie." + }, "warning": { "message": "UWAGA", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Eksportowanie sejfu organizacji" + }, + "exportingOrganizationVaultDesc": { + "message": "Tylko sejf organizacji powiązany z $ORGANIZATION$ zostanie wyeksportowany. Pozycje w poszczególnych sejfach lub innych organizacji nie będą uwzględnione.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Błąd" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 0a65acf9586..7f247311df6 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Desbloquear" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Mostrar opções de menu de contexto" }, @@ -799,12 +802,39 @@ "message": "Solarized (escuro)", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exportar Cofre" }, "fileFormat": { "message": "Formato de arquivo" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "AVISO", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Erro" }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 7f4b488fa37..bcbddaff0a2 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Desbloquear" }, + "additionalOptions": { + "message": "Opções adicionais" + }, "enableContextMenuItem": { "message": "Mostrar opções do menu de contexto" }, @@ -799,12 +802,39 @@ "message": "Solarized (escuro)", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Exportar de" + }, "exportVault": { "message": "Exportar cofre" }, "fileFormat": { "message": "Formato do ficheiro" }, + "fileEncryptedExportWarningDesc": { + "message": "A exportação deste ficheiro será protegida por uma palavra-passe e exigirá a palavra-passe do ficheiro para ser desencriptada." + }, + "filePassword": { + "message": "Palavra-passe do ficheiro" + }, + "exportPasswordDescription": { + "message": "Esta palavra-passe será utilizada para exportar e importar este ficheiro" + }, + "accountRestrictedOptionDescription": { + "message": "Utilize a chave de encriptação da sua conta, derivada do nome de utilizador e da palavra-passe mestra da sua conta, para encriptar a exportação e restringir a importação apenas à conta Bitwarden atual." + }, + "passwordProtectedOptionDescription": { + "message": "Defina uma palavra-passe do ficheiro para encriptar a exportação e importe-a para qualquer conta Bitwarden utilizando a palavra-passe de desencriptação." + }, + "exportTypeHeading": { + "message": "Tipo de exportação" + }, + "accountRestricted": { + "message": "Conta restringida" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Palavra-passe do ficheiro\" e \"Confirmar palavra-passe do ficheiro\" não correspondem." + }, "warning": { "message": "AVISO", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "A exportar o cofre da organização" + }, + "exportingOrganizationVaultDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado. Os itens em cofres individuais ou noutras organizações não serão incluídos.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Erro" }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 7c5cd8a3acd..a9dae9496b6 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Deblocare" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Afișați opțiunile meniului contextual" }, @@ -799,12 +802,39 @@ "message": "Întuneric solarizat", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export seif" }, "fileFormat": { "message": "Format fișier" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "AVERTISMENT", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Eroare" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index cbabda5066a..438a04e9639 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Разблокировать" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Показать опции контекстного меню" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Экспорт из" + }, "exportVault": { "message": "Экспорт хранилища" }, "fileFormat": { "message": "Формат файла" }, + "fileEncryptedExportWarningDesc": { + "message": "Экспорт этого файла будет защищен паролем, и для расшифровки потребуется пароль файла." + }, + "filePassword": { + "message": "Пароль к файлу" + }, + "exportPasswordDescription": { + "message": "Этот пароль будет использоваться для экспорта и импорта этого файла" + }, + "accountRestrictedOptionDescription": { + "message": "Использовать ключ шифрования вашего аккаунта, полученный из имени пользователя и мастер-пароля, для шифрования экспорта и ограничения импорта только для текущего аккаунта Bitwarden." + }, + "passwordProtectedOptionDescription": { + "message": "Установите пароль файла для шифрования экспорта и импортируйте его в любую учетную запись Bitwarden, используя пароль для расшифровки." + }, + "exportTypeHeading": { + "message": "Тип экспорта" + }, + "accountRestricted": { + "message": "Ограничено аккаунтом" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Пароль к файлу\" и \"Подтверждение пароля к файлу\" не совпадают." + }, "warning": { "message": "ВНИМАНИЕ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Экспорт хранилища организации" + }, + "exportingOrganizationVaultDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$. Элементы из личных хранилищ и из других организаций включены не будут.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Ошибка" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index b9078c4c0f8..9fa1bb2339c 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "අඳුරු අඳුරු", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "අපනයන සුරක්ෂිතාගාරය" }, "fileFormat": { "message": "ගොනු ආකෘතිය" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "අවවාදයයි", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 63bc7a317c0..b9d7ec78cda 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Odomknúť" }, + "additionalOptions": { + "message": "Ďalšie možnosti" + }, "enableContextMenuItem": { "message": "Zobraziť možnosti kontextovej ponuky" }, @@ -799,12 +802,39 @@ "message": "Solarized –⁠ tmavý", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Exportovať z" + }, "exportVault": { "message": "Export trezoru" }, "fileFormat": { "message": "Formát Súboru" }, + "fileEncryptedExportWarningDesc": { + "message": "Tento exportovaný súbor bude chránený heslom a na dešifrovanie bude potrebné heslo súboru." + }, + "filePassword": { + "message": "Heslo súboru" + }, + "exportPasswordDescription": { + "message": "Toto heslo sa použije na export a import tohto súboru" + }, + "accountRestrictedOptionDescription": { + "message": "Na zašifrovanie exportu a obmedzenie importu len na aktuálny účet Bitwarden použite šifrovací kľúč účtu odvodený z používateľského mena a hlavného hesla účtu." + }, + "passwordProtectedOptionDescription": { + "message": "Nastavte heslo súboru na zašifrovanie exportu a importujte ho do akéhokoľvek účtu Bitwarden pomocou hesla na dešifrovanie." + }, + "exportTypeHeading": { + "message": "Typ exportu" + }, + "accountRestricted": { + "message": "Obmedzený účet" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Heslo súboru\" a \"Potvrdiť heslo súboru\" sa nezhodujú." + }, "warning": { "message": "UPOZORNENIE", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exportovanie trezora organizácie" + }, + "exportingOrganizationVaultDesc": { + "message": "Exportované budú iba položky trezora organizácie spojené s $ORGANIZATION$. Položky osobného trezora a položky z iných organizácií nebudú zahrnuté.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Chyba" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index c12a31436fd..7333c0d91cc 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Prikaži možnosti kontekstnega menuja" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Izvoz trezorja" }, "fileFormat": { "message": "Format datoteke" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "OPOZORILO", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Napaka" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 941b38132a4..3d15f246499 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Откључај" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Прикажи контекстни мени" }, @@ -799,12 +802,39 @@ "message": "Solarized црно", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Извоз сефа" }, "fileFormat": { "message": "Формат датотеке" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "УПОЗОРЕЊЕ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Грешка" }, diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 585d8e2d9a0..deb7fda04b3 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Lås upp" }, + "additionalOptions": { + "message": "Ytterligare alternativ" + }, "enableContextMenuItem": { "message": "Visa alternativ för snabbmenyn" }, @@ -799,12 +802,39 @@ "message": "Solarized mörk", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Exportera från" + }, "exportVault": { "message": "Exportera valv" }, "fileFormat": { "message": "Filformat" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "Fillösenord" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Exporttyp" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "VARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Fel" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 64f3623b5af..c3f03166b25 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Show context menu options" }, @@ -799,12 +802,39 @@ "message": "Solarized dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "WARNING", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index bd708d0d6e4..f6fc2f4c1de 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Unlock" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "แสดงตัวเลือกเมนูบริบท" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export Vault" }, "fileFormat": { "message": "File Format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "คำเตือน", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Error" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 596e779913a..4bee651cdbe 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -185,7 +185,7 @@ "message": "Tarayıcınızın uzantı sitesine gitmek ister misiniz?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Bitwarden'ı başkalarına da tanımak ister misiniz? Tarayıcınızın uzantı mağazasını ziyaret edip Bitwarden'ı değerlendirin." }, "changeMasterPasswordOnWebConfirmation": { "message": "Ana parolanızı Bitwarden web uygulamasında değiştirebilirsiniz." @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Kilidi aç" }, + "additionalOptions": { + "message": "Ek seçenekler" + }, "enableContextMenuItem": { "message": "Bağlam menüsü seçeneklerini göster" }, @@ -799,12 +802,39 @@ "message": "Solarized koyu", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Kasayı dışa aktar" }, "fileFormat": { "message": "Dosya biçimi" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "Dosya parolası" + }, + "exportPasswordDescription": { + "message": "Bu parola, bu dosyayı dışa ve içe aktarmak için kullanılacaktır" + }, + "accountRestrictedOptionDescription": { + "message": "Dışa aktarmayı şifrelemek ve içe aktarmayı yalnızca mevcut Bitwarden hesabıyla kısıtlamak için, hesabınızın kullanıcı adı ve ana parolasından türetilen hesap şifreleme anahtarınızı kullanın." + }, + "passwordProtectedOptionDescription": { + "message": "Dışa aktardığınız dosyayı şifrelemek ve bir Bitwarden hesabına içe aktarmak için kullanacağınız parolayı belirleyin." + }, + "exportTypeHeading": { + "message": "Dışa aktarma türü" + }, + "accountRestricted": { + "message": "Hesap kısıtlı" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Dosya parolası\" ile \"Dosya parolasını onaylayın\" eşleşmiyor." + }, "warning": { "message": "UYARI", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Kuruluş kasasını dışa aktarma" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Hata" }, @@ -2548,7 +2590,7 @@ "message": "Creating account on" }, "checkYourEmail": { - "message": "Check your email" + "message": "E-postanızı kontrol edin" }, "followTheLinkInTheEmailSentTo": { "message": "Follow the link in the email sent to" @@ -2560,10 +2602,10 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "Geri dönüp" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "e-posta adresinizi düzenleyin." }, "eu": { "message": "AB", diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 43861e99e93..a97d9626a4f 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Розблокувати" }, + "additionalOptions": { + "message": "Додаткові налаштування" + }, "enableContextMenuItem": { "message": "Показувати в контекстному меню" }, @@ -799,12 +802,39 @@ "message": "Solarized темна", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Експортувати з" + }, "exportVault": { "message": "Експортувати сховище" }, "fileFormat": { "message": "Формат файлу" }, + "fileEncryptedExportWarningDesc": { + "message": "Цей експортований файл буде захищений паролем, який необхідно ввести для його розшифрування." + }, + "filePassword": { + "message": "Пароль файлу" + }, + "exportPasswordDescription": { + "message": "Цей пароль буде використано для експортування та імпортування цього файлу" + }, + "accountRestrictedOptionDescription": { + "message": "Використовуйте ключ шифрування свого облікового запису, створений на основі імені користувача й головного пароля, щоб зашифрувати експортовані дані та обмежити можливість імпортування лише до поточного облікового запису Bitwarden." + }, + "passwordProtectedOptionDescription": { + "message": "Встановіть пароль файлу, щоб зашифрувати експортовані дані та імпортувати до будь-якого облікового запису Bitwarden за допомогою цього пароля." + }, + "exportTypeHeading": { + "message": "Тип експорту" + }, + "accountRestricted": { + "message": "Обмежено обліковим записом" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "Пароль файлу та підтвердження пароля відрізняються." + }, "warning": { "message": "ПОПЕРЕДЖЕННЯ", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Експортування сховища організації" + }, + "exportingOrganizationVaultDesc": { + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Помилка" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 3d6bd434b1a..c8469e97dc6 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "Mở khóa" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "Hiển thị tuỳ chọn menu ngữ cảnh" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Xuất kho lưu trữ" }, "fileFormat": { "message": "File Format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "CẢNH BÁO", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "Lỗi" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index f8464f892f8..609275567b8 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "解锁​​​​" }, + "additionalOptions": { + "message": "附加选项" + }, "enableContextMenuItem": { "message": "显示上下文菜单选项" }, @@ -799,12 +802,39 @@ "message": "过曝暗", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "导出自" + }, "exportVault": { "message": "导出密码库" }, "fileFormat": { "message": "文件格式" }, + "fileEncryptedExportWarningDesc": { + "message": "此文件导出将受密码保护,需要文件密码才能解密。" + }, + "filePassword": { + "message": "文件密码" + }, + "exportPasswordDescription": { + "message": "此密码将用于导出和导入此文件" + }, + "accountRestrictedOptionDescription": { + "message": "使用衍生自您账户的用户名和主密码的账户加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" + }, + "passwordProtectedOptionDescription": { + "message": "设置一个文件密码用来加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" + }, + "exportTypeHeading": { + "message": "导出类型" + }, + "accountRestricted": { + "message": "账户受限" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "「文件密码」与「确认文件密码」不一致。" + }, "warning": { "message": "警告", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "正在导出组织密码库" + }, + "exportingOrganizationVaultDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库数据。不包括个人密码库和其他组织中的项目。", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "错误" }, diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index a3a7bbfb686..79d8b51382e 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -762,6 +762,9 @@ "notificationUnlock": { "message": "解鎖" }, + "additionalOptions": { + "message": "Additional options" + }, "enableContextMenuItem": { "message": "顯示內容選單選項" }, @@ -799,12 +802,39 @@ "message": "Solarized Dark 主題", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "匯出密碼庫" }, "fileFormat": { "message": "檔案格式" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "warning": { "message": "警告", "description": "WARNING (should stay in capitalized letters if the language permits)" @@ -2182,6 +2212,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "error": { "message": "錯誤" }, diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index 4ea66edb3e1..302b520e336 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -144,7 +144,7 @@ describe("AutofillInit", () => { .mockResolvedValue(pageDetails); const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); - await Promise.resolve(response); + await flushPromises(); expect(response).toBe(true); expect(sendResponse).toHaveBeenCalledWith(pageDetails); diff --git a/apps/browser/src/autofill/utils/index.spec.ts b/apps/browser/src/autofill/utils/index.spec.ts index af67d416015..dcb5aa64696 100644 --- a/apps/browser/src/autofill/utils/index.spec.ts +++ b/apps/browser/src/autofill/utils/index.spec.ts @@ -37,14 +37,29 @@ describe("generateRandomCustomElementName", () => { }); describe("sendExtensionMessage", () => { - it("sends a message to the extention", () => { - const extensionMessageResponse = sendExtensionMessage("updateAutofillOverlayHidden", { + it("sends a message to the extension", async () => { + const extensionMessagePromise = sendExtensionMessage("updateAutofillOverlayHidden", { display: "none", }); - jest.spyOn(chrome.runtime, "sendMessage"); - expect(chrome.runtime.sendMessage).toHaveBeenCalled(); - expect(extensionMessageResponse).toEqual(Promise.resolve({})); + // Jest doesn't give anyway to select the typed overload of "sendMessage", + // a cast is needed to get the correct spy type. + const sendMessageSpy = jest.spyOn(chrome.runtime, "sendMessage") as unknown as jest.SpyInstance< + void, + [message: string, responseCallback: (response: string) => void], + unknown + >; + + expect(sendMessageSpy).toHaveBeenCalled(); + + const [latestCall] = sendMessageSpy.mock.calls; + const responseCallback = latestCall[1]; + + responseCallback("sendMessageResponse"); + + const response = await extensionMessagePromise; + + expect(response).toEqual("sendMessageResponse"); }); }); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 274649ef130..63721466f6f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -9,6 +9,7 @@ import { AuthRequestService, LoginEmailServiceAbstraction, LoginEmailService, + LogoutReason, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -375,8 +376,17 @@ export default class MainBackground { } }; - const logoutCallback = async (expired: boolean, userId?: UserId) => - await this.logout(expired, userId); + const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) => + await this.logout(logoutReason, userId); + + const refreshAccessTokenErrorCallback = () => { + // Send toast to popup + this.messagingService.send("showToast", { + type: "error", + title: this.i18nService.t("errorRefreshingAccessToken"), + message: this.i18nService.t("errorRefreshingAccessTokenDesc"), + }); + }; const isDev = process.env.ENV === "development"; this.logService = new ConsoleLogService(isDev); @@ -523,6 +533,7 @@ export default class MainBackground { this.keyGenerationService, this.encryptService, this.logService, + logoutCallback, ); const migrationRunner = new MigrationRunner( @@ -608,9 +619,12 @@ export default class MainBackground { this.platformUtilsService, this.environmentService, this.appIdService, + refreshAccessTokenErrorCallback, + this.logService, + (logoutReason: LogoutReason, userId?: UserId) => this.logout(logoutReason, userId), this.vaultTimeoutSettingsService, - (expired: boolean) => this.logout(expired), ); + this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); this.fileUploadService = new FileUploadService(this.logService); this.cipherFileUploadService = new CipherFileUploadService( @@ -1283,7 +1297,7 @@ export default class MainBackground { } } - async logout(expired: boolean, userId?: UserId) { + async logout(logoutReason: LogoutReason, userId?: UserId) { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe( map((a) => a?.id), @@ -1349,7 +1363,7 @@ export default class MainBackground { await logoutPromise; this.messagingService.send("doneLoggingOut", { - expired: expired, + logoutReason: logoutReason, userId: userBeingLoggedOut, }); diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 0c1bd2905ad..623e5d1b145 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.5.1", + "version": "2024.6.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 952396758df..9b7dc42732f 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.5.1", + "version": "2024.6.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 7e94e84ef5d..b70a5564ed9 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angula import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; +import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -10,7 +11,12 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; +import { + DialogService, + SimpleDialogOptions, + ToastOptions, + ToastService, +} from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; @@ -83,13 +89,10 @@ export class AppComponent implements OnInit, OnDestroy { .pipe( tap((msg: any) => { if (msg.command === "doneLoggingOut") { + // TODO: PM-8544 - why do we call logout in the popup after receiving the doneLoggingOut message? Hasn't this already completeted logout? this.authService.logOut(async () => { - if (msg.expired) { - this.toastService.showToast({ - variant: "warning", - title: this.i18nService.t("loggedOut"), - message: this.i18nService.t("loginExpired"), - }); + if (msg.logoutReason) { + await this.displayLogoutReason(msg.logoutReason); } }); this.changeDetectorRef.detectChanges(); @@ -233,4 +236,23 @@ export class AppComponent implements OnInit, OnDestroy { this.browserSendStateService.setBrowserSendTypeComponentState(null), ]); } + + // Displaying toasts isn't super useful on the popup due to the reloads we do. + // However, it is visible for a moment on the FF sidebar logout. + private async displayLogoutReason(logoutReason: LogoutReason) { + let toastOptions: ToastOptions; + switch (logoutReason) { + case "invalidSecurityStamp": + case "sessionExpired": { + toastOptions = { + variant: "warning", + title: this.i18nService.t("loggedOut"), + message: this.i18nService.t("loginExpired"), + }; + break; + } + } + + this.toastService.showToast(toastOptions); + } } diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts index c235d53cb05..3044d51fa72 100644 --- a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts @@ -53,7 +53,9 @@ describe("Fido2 page script with native WebAuthn support", () => { const mockCredentialAssertResult = createAssertCredentialResultMock(); setupMockedWebAuthnSupport(); - require("./page-script"); + beforeAll(() => { + require("./page-script"); + }); afterEach(() => { jest.resetModules(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index 0b108e8b814..83b07fc14cb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -4,7 +4,7 @@ [title]="'autofillSuggestions' | i18n" [showRefresh]="showRefresh" (onRefresh)="refreshCurrentTab()" - showAutoFill + showAutofillButton > diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index c00e585e739..9a4670bb4c8 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -3,12 +3,12 @@ import { Component } from "@angular/core"; import { combineLatest, map, Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IconButtonModule, SectionComponent, TypographyModule } from "@bitwarden/components"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component"; import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; +import { PopupCipherView } from "../../../views/popup-cipher.view"; import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component"; @Component({ @@ -30,7 +30,7 @@ export class AutofillVaultListItemsComponent { * The list of ciphers that can be used to autofill the current page. * @protected */ - protected autofillCiphers$: Observable = + protected autofillCiphers$: Observable = this.vaultPopupItemsService.autoFillCiphers$; /** diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html new file mode 100644 index 00000000000..08133c6b466 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts new file mode 100644 index 00000000000..c89fcca3b3f --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components"; +import { CopyCipherFieldDirective } from "@bitwarden/vault"; + +@Component({ + standalone: true, + selector: "app-item-copy-actions", + templateUrl: "item-copy-actions.component.html", + imports: [ + ItemModule, + IconButtonModule, + JslibModule, + MenuModule, + CommonModule, + CopyCipherFieldDirective, + ], +}) +export class ItemCopyActionsComponent { + @Input() cipher: CipherView; + + protected CipherType = CipherType; + + constructor() {} +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html new file mode 100644 index 00000000000..1d7a2a8cd0c --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + {{ "clone" | i18n }} + + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts new file mode 100644 index 00000000000..9834dc553ec --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -0,0 +1,122 @@ +import { CommonModule } from "@angular/common"; +import { booleanAttribute, Component, Input } from "@angular/core"; +import { Router, RouterModule } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService, IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { BrowserApi } from "../../../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; +import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; + +@Component({ + standalone: true, + selector: "app-item-more-options", + templateUrl: "./item-more-options.component.html", + imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule], +}) +export class ItemMoreOptionsComponent { + @Input({ + required: true, + }) + cipher: CipherView; + + /** + * Flag to hide the login specific menu options. Used for login items that are + * already in the autofill list suggestion. + */ + @Input({ transform: booleanAttribute }) + hideLoginOptions: boolean; + + protected autofillAllowed$ = this.vaultPopupItemsService.autofillAllowed$; + + constructor( + private cipherService: CipherService, + private vaultPopupItemsService: VaultPopupItemsService, + private passwordRepromptService: PasswordRepromptService, + private dialogService: DialogService, + private router: Router, + ) {} + + get canEdit() { + return this.cipher.edit; + } + + get isLogin() { + return this.cipher.type === CipherType.Login; + } + + get favoriteText() { + return this.cipher.favorite ? "unfavorite" : "favorite"; + } + + /** + * Determines if the login cipher can be launched in a new browser tab. + */ + get canLaunch() { + return this.isLogin && this.cipher.login.canLaunch; + } + + /** + * Launches the login cipher in a new browser tab. + */ + async launchCipher() { + if (!this.canLaunch) { + return; + } + + await this.cipherService.updateLastLaunchedDate(this.cipher.id); + + await BrowserApi.createNewTab(this.cipher.login.launchUri); + + if (BrowserPopupUtils.inPopup(window)) { + BrowserApi.closePopup(window); + } + } + + /** + * Toggles the favorite status of the cipher and updates it on the server. + */ + async toggleFavorite() { + this.cipher.favorite = !this.cipher.favorite; + const encryptedCipher = await this.cipherService.encrypt(this.cipher); + await this.cipherService.updateWithServer(encryptedCipher); + } + + /** + * Navigate to the clone cipher page with the current cipher as the source. + * A password reprompt is attempted if the cipher requires it. + * A confirmation dialog is shown if the cipher has FIDO2 credentials. + */ + async clone() { + if ( + this.cipher.reprompt === CipherRepromptType.Password && + !(await this.passwordRepromptService.showPasswordPrompt()) + ) { + return; + } + + if (this.cipher.login?.hasFido2Credentials) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "passkeyNotCopied" }, + content: { key: "passkeyNotCopiedAlert" }, + type: "info", + }); + + if (!confirmed) { + return; + } + } + + await this.router.navigate(["/clone-cipher"], { + queryParams: { + cloneMode: true, + cipherId: this.cipher.id, + }, + }); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html new file mode 100644 index 00000000000..6136db59f46 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html @@ -0,0 +1,39 @@ +
+ + + + + + + + + + + + + + +
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts new file mode 100644 index 00000000000..886e1a966a8 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.ts @@ -0,0 +1,28 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ChipSelectComponent } from "@bitwarden/components"; + +import { VaultPopupListFiltersService } from "../../../services/vault-popup-list-filters.service"; + +@Component({ + standalone: true, + selector: "app-vault-list-filters", + templateUrl: "./vault-list-filters.component.html", + imports: [CommonModule, JslibModule, ChipSelectComponent, ReactiveFormsModule], +}) +export class VaultListFiltersComponent implements OnDestroy { + protected filterForm = this.vaultPopupListFiltersService.filterForm; + protected organizations$ = this.vaultPopupListFiltersService.organizations$; + protected collections$ = this.vaultPopupListFiltersService.collections$; + protected folders$ = this.vaultPopupListFiltersService.folders$; + protected cipherTypes = this.vaultPopupListFiltersService.cipherTypes; + + constructor(private vaultPopupListFiltersService: VaultPopupListFiltersService) {} + + ngOnDestroy(): void { + this.vaultPopupListFiltersService.resetFilterForm(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index d3bb85c710f..74ee1af1ff5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -13,31 +13,31 @@ - + {{ cipher.name }} + {{ cipher.subTitle }} - + - - - - - - + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index f9b34e96162..311619fa0ff 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -3,7 +3,7 @@ import { booleanAttribute, Component, EventEmitter, Input, Output } from "@angul import { RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { BadgeModule, ButtonModule, @@ -14,6 +14,9 @@ import { } from "@bitwarden/components"; import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component"; +import { PopupCipherView } from "../../../views/popup-cipher.view"; +import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component"; +import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component"; @Component({ imports: [ @@ -27,6 +30,8 @@ import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup JslibModule, PopupSectionHeaderComponent, RouterLink, + ItemCopyActionsComponent, + ItemMoreOptionsComponent, ], selector: "app-vault-list-items-container", templateUrl: "vault-list-items-container.component.html", @@ -37,7 +42,7 @@ export class VaultListItemsContainerComponent { * The list of ciphers to display. */ @Input() - ciphers: CipherView[]; + ciphers: PopupCipherView[] = []; /** * Title for the vault list item section. @@ -61,5 +66,19 @@ export class VaultListItemsContainerComponent { * Option to show the autofill button for each item. */ @Input({ transform: booleanAttribute }) - showAutoFill: boolean; + showAutofillButton: boolean; + + /** + * The tooltip text for the organization icon for ciphers that belong to an organization. + * @param cipher + */ + orgIconTooltip(cipher: PopupCipherView) { + if (cipher.collectionIds.length > 1) { + return this.i18nService.t("nCollections", cipher.collectionIds.length); + } + + return cipher.collections[0]?.name; + } + + constructor(private i18nService: I18nService) {} } diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index 7d83d9f26cc..f99d3cbb303 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -22,13 +22,15 @@ - +
+ + - - + +
@@ -37,7 +39,17 @@
- +
+ + {{ "organizationIsDeactivated" | i18n }} + {{ "contactYourOrgAdmin" | i18n }} + +
+ + { + let testBed: TestBed; let service: VaultPopupItemsService; let allCiphers: Record; let autoFillCiphers: CipherView[]; + let mockOrg: Organization; + let mockCollections: CollectionView[]; + const cipherServiceMock = mock(); const vaultSettingsServiceMock = mock(); + const organizationServiceMock = mock(); + const vaultPopupListFiltersServiceMock = mock(); const searchService = mock(); + const collectionService = mock(); beforeEach(() => { allCiphers = cipherFactory(10); @@ -35,31 +49,80 @@ describe("VaultPopupItemsService", () => { cipherList[2].favorite = true; cipherList[3].favorite = true; - cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable(); - searchService.searchCiphers.mockImplementation(async () => cipherList); - cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers); - vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable(); - vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable(); + cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList); + cipherServiceMock.ciphers$ = new BehaviorSubject(null).asObservable(); + searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers); + cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => + ciphers.filter((c) => ["0", "1"].includes(c.id)), + ); + vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false); + vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false); + + vaultPopupListFiltersServiceMock.filters$ = new BehaviorSubject({ + organization: null, + collection: null, + cipherType: null, + folder: null, + }); + // Return all ciphers, `filterFunction$` will be tested in `VaultPopupListFiltersService` + vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject( + (ciphers: CipherView[]) => ciphers, + ); jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); jest .spyOn(BrowserApi, "getTabFromCurrentWindow") .mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab); - service = new VaultPopupItemsService( - cipherServiceMock, - vaultSettingsServiceMock, - searchService, - ); + + mockOrg = { + id: "org1", + name: "Organization 1", + planProductType: ProductType.Enterprise, + } as Organization; + + mockCollections = [ + { id: "col1", name: "Collection 1" } as CollectionView, + { id: "col2", name: "Collection 2" } as CollectionView, + ]; + + organizationServiceMock.organizations$ = new BehaviorSubject([mockOrg]); + collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); + + testBed = TestBed.configureTestingModule({ + providers: [ + { provide: CipherService, useValue: cipherServiceMock }, + { provide: VaultSettingsService, useValue: vaultSettingsServiceMock }, + { provide: SearchService, useValue: searchService }, + { provide: OrganizationService, useValue: organizationServiceMock }, + { provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock }, + { provide: CollectionService, useValue: collectionService }, + ], + }); + + service = testBed.inject(VaultPopupItemsService); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it("should be created", () => { - service = new VaultPopupItemsService( - cipherServiceMock, - vaultSettingsServiceMock, - searchService, - ); + service = testBed.inject(VaultPopupItemsService); expect(service).toBeTruthy(); }); + it("should merge cipher views with collections and organization", (done) => { + const cipherList = Object.values(allCiphers); + cipherList[0].organizationId = "org1"; + cipherList[0].collectionIds = ["col1", "col2"]; + + service.autoFillCiphers$.subscribe((ciphers) => { + expect(ciphers[0].organization).toEqual(mockOrg); + expect(ciphers[0].collections).toContain(mockCollections[0]); + expect(ciphers[0].collections).toContain(mockCollections[1]); + done(); + }); + }); + describe("autoFillCiphers$", () => { it("should return empty array if there is no current tab", (done) => { jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); @@ -80,16 +143,10 @@ describe("VaultPopupItemsService", () => { it("should filter ciphers for the current tab and types", (done) => { const currentTab = { url: "https://example.com" } as chrome.tabs.Tab; - vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(true).asObservable(); - vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable(); + (vaultSettingsServiceMock.showCardsCurrentTab$ as BehaviorSubject).next(true); + (vaultSettingsServiceMock.showIdentitiesCurrentTab$ as BehaviorSubject).next(true); jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab); - service = new VaultPopupItemsService( - cipherServiceMock, - vaultSettingsServiceMock, - searchService, - ); - service.autoFillCiphers$.subscribe((ciphers) => { expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1); expect(cipherServiceMock.filterCiphersForUrl).toHaveBeenCalledWith( @@ -114,12 +171,6 @@ describe("VaultPopupItemsService", () => { Object.values(allCiphers), ); - service = new VaultPopupItemsService( - cipherServiceMock, - vaultSettingsServiceMock, - searchService, - ); - service.autoFillCiphers$.subscribe((ciphers) => { expect(ciphers.length).toBe(10); @@ -135,19 +186,18 @@ describe("VaultPopupItemsService", () => { }); it("should filter autoFillCiphers$ down to search term", (done) => { - const cipherList = Object.values(allCiphers); const searchText = "Login"; - searchService.searchCiphers.mockImplementation(async () => { - return cipherList.filter((cipher) => { + searchService.searchCiphers.mockImplementation(async (q, _, ciphers) => { + return ciphers.filter((cipher) => { return cipher.name.includes(searchText); }); }); - // there is only 1 Login returned for filteredCiphers. but two results expected because of other autofill types + // there is only 1 Login returned for filteredCiphers. service.autoFillCiphers$.subscribe((ciphers) => { expect(ciphers[0].name.includes(searchText)).toBe(true); - expect(ciphers.length).toBe(2); + expect(ciphers.length).toBe(1); done(); }); }); @@ -224,12 +274,7 @@ describe("VaultPopupItemsService", () => { describe("emptyVault$", () => { it("should return true if there are no ciphers", (done) => { - cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable(); - service = new VaultPopupItemsService( - cipherServiceMock, - vaultSettingsServiceMock, - searchService, - ); + cipherServiceMock.getAllDecrypted.mockResolvedValue([]); service.emptyVault$.subscribe((empty) => { expect(empty).toBe(true); done(); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 9a66ada08c5..eacb8e013ef 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -1,7 +1,9 @@ -import { Injectable } from "@angular/core"; +import { inject, Injectable, NgZone } from "@angular/core"; import { BehaviorSubject, combineLatest, + distinctUntilKeyChanged, + from, map, Observable, of, @@ -12,13 +14,21 @@ import { } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BrowserApi } from "../../../platform/browser/browser-api"; +import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; +import { PopupCipherView } from "../views/popup-cipher.view"; + +import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; /** * Service for managing the various item lists on the new Vault tab in the browser popup. @@ -67,14 +77,44 @@ export class VaultPopupItemsService { * Observable that contains the list of all decrypted ciphers. * @private */ - private _cipherList$: Observable = this.cipherService.cipherViews$.pipe( + private _cipherList$: Observable = this.cipherService.ciphers$.pipe( + runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular + switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), map((ciphers) => Object.values(ciphers)), - shareReplay({ refCount: false, bufferSize: 1 }), + switchMap((ciphers) => + combineLatest([ + this.organizationService.organizations$, + this.collectionService.decryptedCollections$, + ]).pipe( + map(([organizations, collections]) => { + const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); + const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); + return ciphers.map( + (cipher) => + new PopupCipherView( + cipher, + cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]), + orgMap[cipher.organizationId as OrganizationId], + ), + ); + }), + ), + ), + shareReplay({ refCount: true, bufferSize: 1 }), ); - private _filteredCipherList$ = combineLatest([this._cipherList$, this.searchText$]).pipe( - switchMap(([ciphers, searchText]) => - this.searchService.searchCiphers(searchText, null, ciphers), + private _filteredCipherList$: Observable = combineLatest([ + this._cipherList$, + this.searchText$, + this.vaultPopupListFiltersService.filterFunction$, + ]).pipe( + map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [ + filterFunction(ciphers), + searchText, + ]), + switchMap( + ([ciphers, searchText]) => + this.searchService.searchCiphers(searchText, null, ciphers) as Promise, ), shareReplay({ refCount: true, bufferSize: 1 }), ); @@ -85,7 +125,7 @@ export class VaultPopupItemsService { * * See {@link refreshCurrentTab} to trigger re-evaluation of the current tab. */ - autoFillCiphers$: Observable = combineLatest([ + autoFillCiphers$: Observable = combineLatest([ this._filteredCipherList$, this._otherAutoFillTypes$, this._currentAutofillTab$, @@ -104,7 +144,7 @@ export class VaultPopupItemsService { * List of favorite ciphers that are not currently suggested for autofill. * Ciphers are sorted by last used date, then by name. */ - favoriteCiphers$: Observable = combineLatest([ + favoriteCiphers$: Observable = combineLatest([ this.autoFillCiphers$, this._filteredCipherList$, ]).pipe( @@ -121,7 +161,7 @@ export class VaultPopupItemsService { * List of all remaining ciphers that are not currently suggested for autofill or marked as favorite. * Ciphers are sorted by name. */ - remainingCiphers$: Observable = combineLatest([ + remainingCiphers$: Observable = combineLatest([ this.autoFillCiphers$, this.favoriteCiphers$, this._filteredCipherList$, @@ -137,10 +177,19 @@ export class VaultPopupItemsService { /** * Observable that indicates whether a filter is currently applied to the ciphers. - * @todo Implement filter/search functionality in PM-6824 and PM-6826. */ - hasFilterApplied$: Observable = this.searchText$.pipe( - switchMap((text) => this.searchService.isSearchable(text)), + hasFilterApplied$ = combineLatest([ + this.searchText$, + this.vaultPopupListFiltersService.filters$, + ]).pipe( + switchMap(([searchText, filters]) => { + return from(this.searchService.isSearchable(searchText)).pipe( + map( + (isSearchable) => + isSearchable || Object.values(filters).some((filter) => filter !== null), + ), + ); + }), ); /** @@ -156,16 +205,33 @@ export class VaultPopupItemsService { /** * Observable that indicates whether there are no ciphers to show with the current filter. - * @todo Implement filter/search functionality in PM-6824 and PM-6826. */ noFilteredResults$: Observable = this._filteredCipherList$.pipe( map((ciphers) => !ciphers.length), ); + /** Observable that indicates when the user should see the deactivated org state */ + showDeactivatedOrg$: Observable = combineLatest([ + this.vaultPopupListFiltersService.filters$.pipe(distinctUntilKeyChanged("organization")), + this.organizationService.organizations$, + ]).pipe( + map(([filters, orgs]) => { + if (!filters.organization || filters.organization.id === MY_VAULT_ID) { + return false; + } + + const org = orgs.find((o) => o.id === filters.organization.id); + return org ? !org.enabled : false; + }), + ); + constructor( private cipherService: CipherService, private vaultSettingsService: VaultSettingsService, + private vaultPopupListFiltersService: VaultPopupListFiltersService, + private organizationService: OrganizationService, private searchService: SearchService, + private collectionService: CollectionService, ) {} /** diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts new file mode 100644 index 00000000000..42626b52918 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -0,0 +1,321 @@ +import { TestBed } from "@angular/core/testing"; +import { FormBuilder } from "@angular/forms"; +import { BehaviorSubject, skipWhile } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { Collection } from "@bitwarden/common/vault/models/domain/collection"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; + +import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; + +describe("VaultPopupListFiltersService", () => { + let service: VaultPopupListFiltersService; + const memberOrganizations$ = new BehaviorSubject<{ name: string; id: string }[]>([]); + const folderViews$ = new BehaviorSubject([]); + const cipherViews$ = new BehaviorSubject({}); + const decryptedCollections$ = new BehaviorSubject([]); + + const collectionService = { + decryptedCollections$, + getAllNested: () => Promise.resolve([]), + } as unknown as CollectionService; + + const folderService = { + folderViews$, + } as unknown as FolderService; + + const cipherService = { + cipherViews$, + } as unknown as CipherService; + + const organizationService = { + memberOrganizations$, + } as unknown as OrganizationService; + + const i18nService = { + t: (key: string) => key, + } as I18nService; + + beforeEach(() => { + memberOrganizations$.next([]); + decryptedCollections$.next([]); + + collectionService.getAllNested = () => Promise.resolve([]); + TestBed.configureTestingModule({ + providers: [ + { + provide: FolderService, + useValue: folderService, + }, + { + provide: CipherService, + useValue: cipherService, + }, + { + provide: OrganizationService, + useValue: organizationService, + }, + { + provide: I18nService, + useValue: i18nService, + }, + { + provide: CollectionService, + useValue: collectionService, + }, + { provide: FormBuilder, useClass: FormBuilder }, + ], + }); + + service = TestBed.inject(VaultPopupListFiltersService); + }); + + describe("cipherTypes", () => { + it("returns all cipher types", () => { + expect(service.cipherTypes.map((c) => c.value)).toEqual([ + CipherType.Login, + CipherType.Card, + CipherType.Identity, + CipherType.SecureNote, + ]); + }); + }); + + describe("organizations$", () => { + it('does not add "myVault" to the list of organizations when there are no organizations', (done) => { + memberOrganizations$.next([]); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([]); + done(); + }); + }); + + it('adds "myVault" to the list of organizations when there are other organizations', (done) => { + memberOrganizations$.next([{ name: "bobby's org", id: "1234-3323-23223" }]); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual(["myVault", "bobby's org"]); + done(); + }); + }); + + it("sorts organizations by name", (done) => { + memberOrganizations$.next([ + { name: "bobby's org", id: "1234-3323-23223" }, + { name: "alice's org", id: "2223-4343-99888" }, + ]); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([ + "myVault", + "alice's org", + "bobby's org", + ]); + done(); + }); + }); + }); + + describe("collections$", () => { + const testCollection = { + id: "14cbf8e9-7a2a-4105-9bf6-b15c01203cef", + name: "Test collection", + organizationId: "3f860945-b237-40bc-a51e-b15c01203ccf", + } as CollectionView; + + const testCollection2 = { + id: "b15c0120-7a2a-4105-9bf6-b15c01203ceg", + name: "Test collection 2", + organizationId: "1203ccf-2432-123-acdd-b15c01203ccf", + } as CollectionView; + + const testCollections = [testCollection, testCollection2]; + + beforeEach(() => { + decryptedCollections$.next(testCollections); + + collectionService.getAllNested = () => + Promise.resolve( + testCollections.map((c) => ({ + children: [], + node: c, + parent: null, + })), + ); + }); + + it("returns all collections", (done) => { + service.collections$.subscribe((collections) => { + expect(collections.map((c) => c.label)).toEqual(["Test collection", "Test collection 2"]); + done(); + }); + }); + + it("filters out collections that do not belong to an organization", () => { + service.filterForm.patchValue({ + organization: { id: testCollection2.organizationId } as Organization, + }); + + service.collections$.subscribe((collections) => { + expect(collections.map((c) => c.label)).toEqual(["Test collection 2"]); + }); + }); + + it("sets collection icon", (done) => { + service.collections$.subscribe((collections) => { + expect(collections.every(({ icon }) => icon === "bwi-collection")).toBeTruthy(); + done(); + }); + }); + }); + + describe("folders$", () => { + it('returns no folders when "No Folder" is the only option', (done) => { + folderViews$.next([{ id: null, name: "No Folder" }]); + + service.folders$.subscribe((folders) => { + expect(folders).toEqual([]); + done(); + }); + }); + + it('moves "No Folder" to the end of the list', (done) => { + folderViews$.next([ + { id: null, name: "No Folder" }, + { id: "2345", name: "Folder 2" }, + { id: "1234", name: "Folder 1" }, + ]); + + service.folders$.subscribe((folders) => { + expect(folders.map((f) => f.label)).toEqual(["Folder 1", "Folder 2", "itemsWithNoFolder"]); + done(); + }); + }); + + it("returns all folders when MyVault is selected", (done) => { + service.filterForm.patchValue({ + organization: { id: MY_VAULT_ID } as Organization, + }); + + folderViews$.next([ + { id: "1234", name: "Folder 1" }, + { id: "2345", name: "Folder 2" }, + ]); + + service.folders$.subscribe((folders) => { + expect(folders.map((f) => f.label)).toEqual(["Folder 1", "Folder 2"]); + done(); + }); + }); + + it("sets folder icon", (done) => { + service.filterForm.patchValue({ + organization: { id: MY_VAULT_ID } as Organization, + }); + + folderViews$.next([ + { id: "1234", name: "Folder 1" }, + { id: "2345", name: "Folder 2" }, + ]); + + service.folders$.subscribe((folders) => { + expect(folders.every(({ icon }) => icon === "bwi-folder")).toBeTruthy(); + done(); + }); + }); + + it("returns folders that have ciphers within the selected organization", (done) => { + service.folders$.pipe(skipWhile((folders) => folders.length === 2)).subscribe((folders) => { + expect(folders.map((f) => f.label)).toEqual(["Folder 1"]); + done(); + }); + + service.filterForm.patchValue({ + organization: { id: "1234" } as Organization, + }); + + folderViews$.next([ + { id: "1234", name: "Folder 1" }, + { id: "2345", name: "Folder 2" }, + ]); + + cipherViews$.next({ + "1": { folderId: "1234", organizationId: "1234" }, + "2": { folderId: "2345", organizationId: "56789" }, + }); + }); + }); + + describe("filterFunction$", () => { + const ciphers = [ + { type: CipherType.Login, collectionIds: [], organizationId: null }, + { type: CipherType.Card, collectionIds: ["1234"], organizationId: "8978" }, + { type: CipherType.Identity, collectionIds: [], folderId: "5432", organizationId: null }, + { type: CipherType.SecureNote, collectionIds: [], organizationId: null }, + ] as CipherView[]; + + it("filters by cipherType", (done) => { + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[0]]); + done(); + }); + + service.filterForm.patchValue({ cipherType: CipherType.Login }); + }); + + it("filters by collection", (done) => { + const collection = { id: "1234" } as Collection; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[1]]); + done(); + }); + + service.filterForm.patchValue({ collection }); + }); + + it("filters by folder", (done) => { + const folder = { id: "5432" } as FolderView; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[2]]); + done(); + }); + + service.filterForm.patchValue({ folder }); + }); + + describe("organizationId", () => { + it("filters out ciphers that belong to an organization when MyVault is selected", (done) => { + const organization = { id: MY_VAULT_ID } as Organization; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[0], ciphers[2], ciphers[3]]); + done(); + }); + + service.filterForm.patchValue({ organization }); + }); + + it("filters out ciphers that do not belong to the selected organization", (done) => { + const organization = { id: "8978" } as Organization; + + service.filterFunction$.subscribe((filterFunction) => { + expect(filterFunction(ciphers)).toEqual([ciphers[1]]); + done(); + }); + + service.filterForm.patchValue({ organization }); + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts new file mode 100644 index 00000000000..bc42e7cb0a5 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -0,0 +1,376 @@ +import { Injectable } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder } from "@angular/forms"; +import { + Observable, + combineLatest, + distinctUntilChanged, + map, + startWith, + switchMap, + tap, +} from "rxjs"; + +import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductType } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { Collection } from "@bitwarden/common/vault/models/domain/collection"; +import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { ChipSelectOption } from "@bitwarden/components"; + +/** All available cipher filters */ +export type PopupListFilter = { + organization: Organization | null; + collection: Collection | null; + folder: FolderView | null; + cipherType: CipherType | null; +}; + +/** Delimiter that denotes a level of nesting */ +const NESTING_DELIMITER = "/"; + +/** Id assigned to the "My vault" organization */ +export const MY_VAULT_ID = "MyVault"; + +const INITIAL_FILTERS: PopupListFilter = { + organization: null, + collection: null, + folder: null, + cipherType: null, +}; + +@Injectable({ + providedIn: "root", +}) +export class VaultPopupListFiltersService { + /** + * UI form for all filters + */ + filterForm = this.formBuilder.group(INITIAL_FILTERS); + + /** + * Observable for `filterForm` value + */ + filters$ = this.filterForm.valueChanges.pipe( + startWith(INITIAL_FILTERS), + ) as Observable; + + /** + * Static list of ciphers views used in synchronous context + */ + private cipherViews: CipherView[] = []; + + /** + * Observable of cipher views + */ + private cipherViews$: Observable = this.cipherService.cipherViews$.pipe( + tap((cipherViews) => { + this.cipherViews = Object.values(cipherViews); + }), + map((ciphers) => Object.values(ciphers)), + ); + + constructor( + private folderService: FolderService, + private cipherService: CipherService, + private organizationService: OrganizationService, + private i18nService: I18nService, + private collectionService: CollectionService, + private formBuilder: FormBuilder, + ) { + this.filterForm.controls.organization.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(this.validateOrganizationChange.bind(this)); + } + + /** + * Observable whose value is a function that filters an array of `CipherView` objects based on the current filters + */ + filterFunction$: Observable<(ciphers: CipherView[]) => CipherView[]> = this.filters$.pipe( + map( + (filters) => (ciphers: CipherView[]) => + ciphers.filter((cipher) => { + if (filters.cipherType !== null && cipher.type !== filters.cipherType) { + return false; + } + + if ( + filters.collection !== null && + !cipher.collectionIds.includes(filters.collection.id) + ) { + return false; + } + + if (filters.folder !== null && cipher.folderId !== filters.folder.id) { + return false; + } + + const isMyVault = filters.organization?.id === MY_VAULT_ID; + + if (isMyVault) { + if (cipher.organizationId !== null) { + return false; + } + } else if (filters.organization !== null) { + if (cipher.organizationId !== filters.organization.id) { + return false; + } + } + + return true; + }), + ), + ); + + /** + * All available cipher types + */ + readonly cipherTypes: ChipSelectOption[] = [ + { + value: CipherType.Login, + label: this.i18nService.t("logins"), + icon: "bwi-globe", + }, + { + value: CipherType.Card, + label: this.i18nService.t("cards"), + icon: "bwi-credit-card", + }, + { + value: CipherType.Identity, + label: this.i18nService.t("identities"), + icon: "bwi-id-card", + }, + { + value: CipherType.SecureNote, + label: this.i18nService.t("notes"), + icon: "bwi-sticky-note", + }, + ]; + + /** Resets `filterForm` to the original state */ + resetFilterForm(): void { + this.filterForm.reset(INITIAL_FILTERS); + } + + /** + * Organization array structured to be directly passed to `ChipSelectComponent` + */ + organizations$: Observable[]> = + this.organizationService.memberOrganizations$.pipe( + map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), + map((orgs) => { + if (!orgs.length) { + return []; + } + + return [ + // When the user is a member of an organization, make the "My Vault" option available + { + value: { id: MY_VAULT_ID } as Organization, + label: this.i18nService.t("myVault"), + icon: "bwi-user", + }, + ...orgs.map((org) => { + let icon = "bwi-business"; + + if (!org.enabled) { + // Show a warning icon if the organization is deactivated + icon = "bwi-exclamation-triangle tw-text-danger"; + } else if (org.planProductType === ProductType.Families) { + // Show a family icon if the organization is a family org + icon = "bwi-family"; + } + + return { + value: org, + label: org.name, + icon, + }; + }), + ]; + }), + ); + + /** + * Folder array structured to be directly passed to `ChipSelectComponent` + */ + folders$: Observable[]> = combineLatest([ + this.filters$.pipe( + distinctUntilChanged( + (previousFilter, currentFilter) => + // Only update the collections when the organizationId filter changes + previousFilter.organization?.id === currentFilter.organization?.id, + ), + ), + this.folderService.folderViews$, + this.cipherViews$, + ]).pipe( + map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => { + if (folders.length === 1 && folders[0].id === null) { + // Do not display folder selections when only the "no folder" option is available. + return [filters, [], cipherViews]; + } + + // Sort folders by alphabetic name + folders.sort(Utils.getSortFunction(this.i18nService, "name")); + let arrangedFolders = folders; + + const noFolder = folders.find((f) => f.id === null); + + if (noFolder) { + // Update `name` of the "no folder" option to "Items with no folder" + noFolder.name = this.i18nService.t("itemsWithNoFolder"); + + // Move the "no folder" option to the end of the list + arrangedFolders = [...folders.filter((f) => f.id !== null), noFolder]; + } + return [filters, arrangedFolders, cipherViews]; + }), + map(([filters, folders, cipherViews]) => { + const organizationId = filters.organization?.id ?? null; + + // When no org or "My vault" is selected, return all folders + if (organizationId === null || organizationId === MY_VAULT_ID) { + return folders; + } + + const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId); + + // Return only the folders that have ciphers within the filtered organization + return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id)); + }), + map((folders) => { + const nestedFolders = this.getAllFoldersNested(folders); + return new DynamicTreeNode({ + fullList: folders, + nestedList: nestedFolders, + }); + }), + map((folders) => + folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")), + ), + ); + + /** + * Collection array structured to be directly passed to `ChipSelectComponent` + */ + collections$: Observable[]> = combineLatest([ + this.filters$.pipe( + distinctUntilChanged( + (previousFilter, currentFilter) => + // Only update the collections when the organizationId filter changes + previousFilter.organization?.id === currentFilter.organization?.id, + ), + ), + this.collectionService.decryptedCollections$, + ]).pipe( + map(([filters, allCollections]) => { + const organizationId = filters.organization?.id ?? null; + // When the organization filter is selected, filter out collections that do not belong to the selected organization + const collections = + organizationId === null + ? allCollections + : allCollections.filter((c) => c.organizationId === organizationId); + + return collections; + }), + switchMap(async (collections) => { + const nestedCollections = await this.collectionService.getAllNested(collections); + + return new DynamicTreeNode({ + fullList: collections, + nestedList: nestedCollections, + }); + }), + map((collections) => + collections.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection")), + ), + ); + + /** + * Converts the given item into the `ChipSelectOption` structure + */ + private convertToChipSelectOption( + item: TreeNode, + icon: string, + ): ChipSelectOption { + return { + value: item.node, + label: item.node.name, + icon, + children: item.children + ? item.children.map((i) => this.convertToChipSelectOption(i, icon)) + : undefined, + }; + } + + /** + * Returns a nested folder structure based on the input FolderView array + */ + private getAllFoldersNested(folders: FolderView[]): TreeNode[] { + const nodes: TreeNode[] = []; + + folders.forEach((f) => { + const folderCopy = new FolderView(); + folderCopy.id = f.id; + folderCopy.revisionDate = f.revisionDate; + + // Remove "/" from beginning and end of the folder name + // then split the folder name by the delimiter + const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NESTING_DELIMITER) : []; + ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NESTING_DELIMITER); + }); + + return nodes; + } + + /** + * Validate collection & folder filters when the organization filter changes + */ + private validateOrganizationChange(organization: Organization | null): void { + if (!organization) { + return; + } + + const currentFilters = this.filterForm.getRawValue(); + + // When the organization filter changes and a collection is already selected, + // reset the collection filter if the collection does not belong to the new organization filter + if (currentFilters.collection && currentFilters.collection.organizationId !== organization.id) { + this.filterForm.get("collection").setValue(null); + } + + // When the organization filter changes and a folder is already selected, + // reset the folder filter if the folder does not belong to the new organization filter + if ( + currentFilters.folder && + currentFilters.folder.id !== null && + organization.id !== MY_VAULT_ID + ) { + // Get all ciphers that belong to the new organization + const orgCiphers = this.cipherViews.filter((c) => c.organizationId === organization.id); + + // Find any ciphers within the organization that belong to the current folder + const newOrgContainsFolder = orgCiphers.some( + (oc) => oc.folderId === currentFilters.folder.id, + ); + + // If the new organization does not contain the current folder, reset the folder filter + if (!newOrgContainsFolder) { + this.filterForm.get("folder").setValue(null); + } + } + } +} diff --git a/apps/browser/src/vault/popup/views/popup-cipher.view.ts b/apps/browser/src/vault/popup/views/popup-cipher.view.ts new file mode 100644 index 00000000000..4707eb9eb0f --- /dev/null +++ b/apps/browser/src/vault/popup/views/popup-cipher.view.ts @@ -0,0 +1,41 @@ +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductType } from "@bitwarden/common/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; + +/** + * Extended cipher view for the popup. Includes the associated collections and organization + * if applicable. + */ +export class PopupCipherView extends CipherView { + collections?: CollectionView[]; + organization?: Organization; + + constructor( + cipher: CipherView, + collections: CollectionView[] = null, + organization: Organization = null, + ) { + super(); + Object.assign(this, cipher); + this.collections = collections; + this.organization = organization; + } + + /** + * Get the bwi icon for the cipher according to the organization type. + */ + get orgIcon(): "bwi-family" | "bwi-business" | null { + switch (this.organization?.planProductType) { + case ProductType.Free: + case ProductType.Families: + return "bwi-family"; + case ProductType.Teams: + case ProductType.Enterprise: + case ProductType.TeamsStarter: + return "bwi-business"; + default: + return null; + } + } +} diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 4800b4c17f3..5435c6fd7fe 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -1,3 +1,5 @@ +import "jest-preset-angular/setup-jest"; + // Add chrome storage api const QUOTA_BYTES = 10; const storage = { diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 2756ab4395f..a0b86f06d5d 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -66,8 +66,7 @@ const moduleRules = [ { loader: "babel-loader", options: { - configFile: false, - plugins: ["@angular/compiler-cli/linker/babel"], + configFile: "../../babel.config.json", }, }, ], diff --git a/apps/cli/package.json b/apps/cli/package.json index d8ddde3d670..1ad09cc17a5 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.5.0", + "version": "2024.6.0", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/platform/services/node-api.service.ts b/apps/cli/src/platform/services/node-api.service.ts index 4849aef1512..c480d9d1aff 100644 --- a/apps/cli/src/platform/services/node-api.service.ts +++ b/apps/cli/src/platform/services/node-api.service.ts @@ -6,6 +6,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ApiService } from "@bitwarden/common/services/api.service"; @@ -21,8 +22,10 @@ export class NodeApiService extends ApiService { platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, appIdService: AppIdService, + refreshAccessTokenErrorCallback: () => Promise, + logService: LogService, + logoutCallback: () => Promise, vaultTimeoutSettingsService: VaultTimeoutSettingsService, - logoutCallback: (expired: boolean) => Promise, customUserAgent: string = null, ) { super( @@ -30,8 +33,10 @@ export class NodeApiService extends ApiService { platformUtilsService, environmentService, appIdService, - vaultTimeoutSettingsService, + refreshAccessTokenErrorCallback, + logService, logoutCallback, + vaultTimeoutSettingsService, customUserAgent, ); } diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index 882791ef9c9..53039e91473 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -255,6 +255,8 @@ export class ServiceContainer { p = path.join(process.env.HOME, ".config/Bitwarden CLI"); } + const logoutCallback = async () => await this.logout(); + this.platformUtilsService = new CliPlatformUtilsService(ClientType.Cli, packageJson); this.logService = new ConsoleLogService( this.platformUtilsService.isDev(), @@ -337,6 +339,7 @@ export class ServiceContainer { this.keyGenerationService, this.encryptService, this.logService, + logoutCallback, ); const migrationRunner = new MigrationRunner( @@ -421,13 +424,19 @@ export class ServiceContainer { VaultTimeoutStringType.Never, // default vault timeout ); + const refreshAccessTokenErrorCallback = () => { + throw new Error("Refresh Access token error"); + }; + this.apiService = new NodeApiService( this.tokenService, this.platformUtilsService, this.environmentService, this.appIdService, + refreshAccessTokenErrorCallback, + this.logService, + logoutCallback, this.vaultTimeoutSettingsService, - async (expired: boolean) => await this.logout(), customUserAgent, ); @@ -485,7 +494,7 @@ export class ServiceContainer { this.logService, this.organizationService, this.keyGenerationService, - async (expired: boolean) => await this.logout(), + logoutCallback, this.stateProvider, ); @@ -660,7 +669,7 @@ export class ServiceContainer { this.sendApiService, this.userDecryptionOptionsService, this.avatarService, - async (expired: boolean) => await this.logout(), + logoutCallback, this.billingAccountProfileStateService, this.tokenService, this.authService, diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index b921cab37b2..8617553b377 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" @@ -83,9 +83,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 4b2bc2e905e..cded3d57ef6 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -14,9 +14,9 @@ manual_test = [] [dependencies] aes = "=0.8.4" -anyhow = "=1.0.80" +anyhow = "=1.0.86" arboard = { version = "=3.3.2", default-features = false, features = ["wayland-data-control"] } -base64 = "=0.22.0" +base64 = "=0.22.1" cbc = { version = "=0.1.2", features = ["alloc"] } napi = { version = "=2.16.0", features = ["async"] } napi-derive = "=2.16.0" diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index ef0927296a0..39c62998a67 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -127,7 +127,7 @@ "entitlementsLoginHelper": "resources/entitlements.mas.loginhelper.plist", "hardenedRuntime": false, "extendInfo": { - "LSMinimumSystemVersion": "10.15.0", + "LSMinimumSystemVersion": "12", "ElectronTeamID": "LTZ2PFU5D6" } }, diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 747d8ec9811..ac12731398b 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -18,8 +18,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@tsconfig/node16": "1.0.4", - "@types/node": "18.19.29", + "@types/node": "20.14.1", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -99,9 +98,10 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" }, "node_modules/@types/node": { - "version": "18.19.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz", - "integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==", + "version": "20.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.1.tgz", + "integrity": "sha512-T2MzSGEu+ysB/FkWfqmhV3PLyQlowdptmmgD20C6QxsS8Fmv5SjpZ1ayXaEC0S21/h5UJ9iA6W/5vSNU5l00OA==", + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 72b2587a4ae..0f92d5b0b3d 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -23,8 +23,7 @@ "yargs": "17.7.2" }, "devDependencies": { - "@tsconfig/node16": "1.0.4", - "@types/node": "18.19.29", + "@types/node": "20.14.1", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 90d9841a618..129e9c43f09 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.5.0", + "version": "2024.6.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 7feea649c30..561e9b2df9c 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -1,3 +1,4 @@ +import { DialogRef } from "@angular/cdk/dialog"; import { Component, NgZone, @@ -13,6 +14,7 @@ import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; +import { LogoutReason } from "@bitwarden/auth/common"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -48,7 +50,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { DialogService, ToastOptions, ToastService } from "@bitwarden/components"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; @@ -108,6 +110,7 @@ export class AppComponent implements OnInit, OnDestroy { private idleTimer: number = null; private isIdle = false; private activeUserId: UserId = null; + private activeSimpleDialog: DialogRef = null; private destroy$ = new Subject(); @@ -207,7 +210,7 @@ export class AppComponent implements OnInit, OnDestroy { break; case "logout": this.loading = message.userId == null || message.userId === this.activeUserId; - await this.logOut(!!message.expired, message.userId); + await this.logOut(message.logoutReason, message.userId); this.loading = false; break; case "lockVault": @@ -545,9 +548,73 @@ export class AppComponent implements OnInit, OnDestroy { this.messagingService.send("updateAppMenu", { updateRequest: updateRequest }); } + private async displayLogoutReason(logoutReason: LogoutReason) { + let toastOptions: ToastOptions; + + switch (logoutReason) { + case "invalidSecurityStamp": + case "sessionExpired": { + toastOptions = { + variant: "warning", + title: this.i18nService.t("loggedOut"), + message: this.i18nService.t("loginExpired"), + }; + break; + } + // We don't expect these scenarios to be common, but we want the user to + // understand why they are being logged out before a process reload. + case "accessTokenUnableToBeDecrypted": { + // Don't create multiple dialogs if this fires multiple times + if (this.activeSimpleDialog) { + // Let the caller of this function listen for the dialog to close + return firstValueFrom(this.activeSimpleDialog.closed); + } + + this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({ + title: { key: "loggedOut" }, + content: { key: "accessTokenUnableToBeDecrypted" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + + await firstValueFrom(this.activeSimpleDialog.closed); + this.activeSimpleDialog = null; + + break; + } + case "refreshTokenSecureStorageRetrievalFailure": { + // Don't create multiple dialogs if this fires multiple times + if (this.activeSimpleDialog) { + // Let the caller of this function listen for the dialog to close + return firstValueFrom(this.activeSimpleDialog.closed); + } + + this.activeSimpleDialog = this.dialogService.openSimpleDialogRef({ + title: { key: "loggedOut" }, + content: { key: "refreshTokenSecureStorageRetrievalFailure" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + + await firstValueFrom(this.activeSimpleDialog.closed); + this.activeSimpleDialog = null; + + break; + } + } + + if (toastOptions) { + this.toastService.showToast(toastOptions); + } + } + // Even though the userId parameter is no longer optional doesn't mean a message couldn't be // passing null-ish values to us. - private async logOut(expired: boolean, userId: UserId) { + private async logOut(logoutReason: LogoutReason, userId: UserId) { + await this.displayLogoutReason(logoutReason); + const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -620,15 +687,7 @@ export class AppComponent implements OnInit, OnDestroy { // This must come last otherwise the logout will prematurely trigger // a process reload before all the state service user data can be cleaned up if (userBeingLoggedOut === activeUserId) { - this.authService.logOut(async () => { - if (expired) { - this.platformUtilsService.showToast( - "warning", - this.i18nService.t("loggedOut"), - this.i18nService.t("loginExpired"), - ); - } - }); + this.authService.logOut(async () => {}); } } @@ -710,7 +769,7 @@ export class AppComponent implements OnInit, OnDestroy { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises options[1] === "logOut" - ? this.logOut(false, userId as UserId) + ? this.logOut("vaultTimeout", userId as UserId) : await this.vaultTimeoutService.lock(userId); } } diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 28efbc7de4e..27d0977a2e1 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -1299,12 +1299,42 @@ "message": "Wagwoord bygewerk", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Stuur Kluis Uit" }, "fileFormat": { "message": "Lêerformaat" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha-bronadres", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Vergrendel" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 25a91b95b6c..a800840dd4a 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -1299,12 +1299,42 @@ "message": "تم تحديث كلمة المرور", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "تصدير الخزنة" }, "fileFormat": { "message": "صيغة الملف" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "رابط hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "مقفل" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index c26e49d743c..0143e6c2745 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1299,12 +1299,42 @@ "message": "Parol güncəlləndi", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Buradan xaricə köçür" + }, "exportVault": { "message": "Anbarı xaricə köçür" }, "fileFormat": { "message": "Fayl formatı" }, + "fileEncryptedExportWarningDesc": { + "message": "Bu faylın xaricə köçürülməsi, parolla qorunacaq və şifrəsini açmaq üçün fayl parolu tələb olunacaq." + }, + "filePassword": { + "message": "Fayl parolu" + }, + "exportPasswordDescription": { + "message": "Bu parol, bu faylı daxilə və xaricə köçürmək üçün istifadə olunacaq" + }, + "accountRestrictedOptionDescription": { + "message": "Xaricə köçürməni şifrələmək və daxilə köçürməni yalnız mövcud Bitwarden hesabı ilə məhdudlaşdırmaq üçün hesabınızın istifadəçi adı və Ana Parolundan əldə edilən hesab şifrələmə açarınızı istifadə edin." + }, + "passwordProtected": { + "message": "Parolla qorunan" + }, + "passwordProtectedOptionDescription": { + "message": "Xaricə köçürməni şifrələmək üçün bir fayl parolu təyin edin və şifrəni açma parolunu istifadə edərək bunu istənilən Bitwarden hesabına köçürün." + }, + "exportTypeHeading": { + "message": "Xaricə köçürmə növü" + }, + "accountRestricted": { + "message": "Hesab məhdudlaşdırıldı" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Fayl parolu\" və \"Fayl parolunu təsdiqlə\" uyuşmur." + }, "hCaptchaUrl": { "message": "hCaptcha ünvanı", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Təşkilat anbarını xaricə köçürmə" + }, + "exportingOrganizationVaultDesc": { + "message": "Yalnız $ORGANIZATION$ ilə əlaqələndirilmiş təşkilat anbarı ixrac ediləcək. Fərdi anbardakı və digər təşkilat elementlər daxil edilmir.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Kilidli" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 0371f3bcebb..4adb3be3e5a 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -1299,12 +1299,42 @@ "message": "Пароль абноўлены", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Экспартаваць сховішча" }, "fileFormat": { "message": "Фармат файла" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "URL-адрас hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Заблакіравана" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 071399419c7..7471bebe029 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1299,12 +1299,42 @@ "message": "Обновена парола", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Изнасяне от" + }, "exportVault": { "message": "Изнасяне на трезора" }, "fileFormat": { "message": "Формат на файла" }, + "fileEncryptedExportWarningDesc": { + "message": "Изнесеният файл ще бъде защитен с парола, която ще бъде необходима за дешифриране на файла." + }, + "filePassword": { + "message": "Парола на файла" + }, + "exportPasswordDescription": { + "message": "Парола ще се използва при изнасянето и при внасянето на този файл" + }, + "accountRestrictedOptionDescription": { + "message": "Използвайте ключа си за шифриране, който се получава чрез комбиниране на потребителското име на регистрацията Ви и главната парола. С него изнасянето ще се шифрира и внасянето ще бъда възможно само в текущата регистрация в Битуорден." + }, + "passwordProtected": { + "message": "Защита с парола" + }, + "passwordProtectedOptionDescription": { + "message": "Задайте парола за файла, за да шифровате изнесените данни. Ще можете да внесете данните във всяка регистрация в Битуорден използвайки паролата за дешифриране." + }, + "exportTypeHeading": { + "message": "Вид изнасяне" + }, + "accountRestricted": { + "message": "Регистрацията е ограничена" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "Дънните в полетата „Парола на файла“ и „Потвърждаване на паролата на файла“ не съвпадат." + }, "hCaptchaUrl": { "message": "Адрес за hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Изнасяне на трезора на организацията" + }, + "exportingOrganizationVaultDesc": { + "message": "Ще бъдат изнесени само записите от трезора свързан с $ORGANIZATION$. Записите в отделните лични трезори и тези в други организации няма да бъдат включени.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Заключено" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index a6f0e712063..71749add465 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -1299,12 +1299,42 @@ "message": "পাসওয়ার্ড হালনাগাদকৃত", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "ভল্ট রফতানি" }, "fileFormat": { "message": "ফাইলের ধরণ" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 1a489ea397d..a11dbb7e9b9 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -1299,12 +1299,42 @@ "message": "Lozinka ažurirana", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Izvezi trezor" }, "fileFormat": { "message": "Format datoteke" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index e55128ceeb4..abee9ac2ff0 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1299,12 +1299,42 @@ "message": "Contrasenya actualitzada", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exporta caixa forta" }, "fileFormat": { "message": "Format de fitxer" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "Url hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Bloquejat" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index b5f2a4f5f0c..bcb08de2bea 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -1299,12 +1299,42 @@ "message": "Heslo bylo aktualizováno", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Exportovat z" + }, "exportVault": { "message": "Exportovat trezor" }, "fileFormat": { "message": "Formát souboru" }, + "fileEncryptedExportWarningDesc": { + "message": "Tento soubor exportu bude chráněn heslem a k dešifrování bude vyžadovat heslo souboru." + }, + "filePassword": { + "message": "Heslo souboru" + }, + "exportPasswordDescription": { + "message": "Toto heslo bude použito pro export a import tohoto souboru" + }, + "accountRestrictedOptionDescription": { + "message": "Pro zašifrování exportu a omezení importu pouze na aktuální účet Bitwardenu použijte šifrovací klíč Vašeho účtu odvozený z uživatelského jména a hlavního hesla." + }, + "passwordProtected": { + "message": "Chráněno heslem" + }, + "passwordProtectedOptionDescription": { + "message": "Nastavte heslo pro šifrování exportu a importujte ho do libovolného účtu Bitwardenu pomocí hesla pro dešifrování." + }, + "exportTypeHeading": { + "message": "Typ exportu" + }, + "accountRestricted": { + "message": "Účet je omezený" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Heslo souboru\" a \"Potvrzení hesla souboru\" se neshodují." + }, "hCaptchaUrl": { "message": "URL hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exportování trezoru organizace" + }, + "exportingOrganizationVaultDesc": { + "message": "Exportován bude jen trezor organizace přidružený k položce $ORGANIZATION$. Osobní položky trezoru a položky z jiných organizací nebudou zahrnuty.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Uzamčeno" }, @@ -2379,7 +2421,7 @@ "message": "Tento požadavek již není platný." }, "approveLoginRequestDesc": { - "message": "Použijte toto zařízení pro schvalování žádostí o přihlášení z jiných zařízení." + "message": "Použije toto zařízení pro schvalování žádostí o přihlášení z jiných zařízení." }, "confirmLoginAtemptForMail": { "message": "Potvrďte pokus o přihlášení z $EMAIL$", diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index aeb76cba552..5085234b826 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 8965c8e2a12..e851f1df451 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1299,12 +1299,42 @@ "message": "Adgangskode opdateret", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Eksportér fra" + }, "exportVault": { "message": "Eksportér boks" }, "fileFormat": { "message": "Filformat" }, + "fileEncryptedExportWarningDesc": { + "message": "Denne fileksport vil være adgangskodebeskyttet og kræve filadgangskoden at dekryptere." + }, + "filePassword": { + "message": "Filadgangskode" + }, + "exportPasswordDescription": { + "message": "Denne adgangskode vil blive brugt ved eksport og import af denne fil" + }, + "accountRestrictedOptionDescription": { + "message": "Brug kontokrypteringsnøglen, dannet af kontobrugernavn og Hovedadgangskode, for at kryptere eksporten og hindre import til andre end den aktuelle Bitwarden-konto." + }, + "passwordProtected": { + "message": "Adgangskodebeskyttet" + }, + "passwordProtectedOptionDescription": { + "message": "Opsæt en adgangskode til både at kryptere eksporten samt dekryptere denne ved import til enhver Bitwarden-konto." + }, + "exportTypeHeading": { + "message": "Eksporttype" + }, + "accountRestricted": { + "message": "Konto begrænset" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“Filadgangskode” og “Bekræft filadgangskode“ matcher ikke." + }, "hCaptchaUrl": { "message": "hCaptcha-URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Eksport af organisationsboks" + }, + "exportingOrganizationVaultDesc": { + "message": "Kun organisationsboksen tilknyttet $ORGANIZATION$ eksporteres. Emner i individuelle bokse eller andre organisationer medtages ikke.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Låst" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index d544b10bb64..e5abc443723 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -1299,12 +1299,42 @@ "message": "Passwort aktualisiert", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export aus" + }, "exportVault": { "message": "Tresor exportieren" }, "fileFormat": { "message": "Dateiformat" }, + "fileEncryptedExportWarningDesc": { + "message": "Dieser Datei-Export ist passwortgeschützt und erfordert das Dateipasswort zum Entschlüsseln." + }, + "filePassword": { + "message": "Dateipasswort" + }, + "exportPasswordDescription": { + "message": "Dieses Passwort wird zum Exportieren und Importieren dieser Datei verwendet" + }, + "accountRestrictedOptionDescription": { + "message": "Verwende den Verschlüsselungsschlüssel deines Kontos, abgeleitet vom Benutzernamen und Master-Passwort, um den Export zu verschlüsseln und den Import auf das aktuelle Bitwarden-Konto zu beschränken." + }, + "passwordProtected": { + "message": "Passwortgeschützt" + }, + "passwordProtectedOptionDescription": { + "message": "Lege ein Dateipasswort fest, um den Export zu verschlüsseln und importiere ihn in ein beliebiges Bitwarden-Konto, wobei das Passwort zum Entschlüsseln genutzt wird." + }, + "exportTypeHeading": { + "message": "Exporttyp" + }, + "accountRestricted": { + "message": "Konto eingeschränkt" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "„Dateipasswort“ und „Dateipasswort bestätigen“ stimmen nicht überein." + }, "hCaptchaUrl": { "message": "hCaptcha URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Tresor der Organisation wird exportiert" + }, + "exportingOrganizationVaultDesc": { + "message": "Nur der mit $ORGANIZATION$ verbundene Organisationstresor wird exportiert. Einträge in persönlichen Tresoren oder anderen Organisationen werden nicht berücksichtigt.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Gesperrt" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 03c7d2183c0..af932beb008 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -1299,12 +1299,42 @@ "message": "Ο Κωδικός Ενημερώθηκε", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Εξαγωγή Vault" }, "fileFormat": { "message": "Μορφή Αρχείου" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Κλειδωμένο" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 8a91771da20..82d57c205d4 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -695,6 +695,15 @@ "selfHostedEnvironmentFooter": { "message": "Specify the base URL of your on-premises hosted Bitwarden installation." }, + "selfHostedBaseUrlHint": { + "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + }, + "selfHostedCustomEnvHeader" :{ + "message": "For advanced configuration, you can specify the base URL of each service independently." + }, + "selfHostedEnvFormInvalid" :{ + "message": "You must add either the base Server URL or at least one custom environment." + }, "customEnvironment": { "message": "Custom environment" }, @@ -743,6 +752,9 @@ "loggedOut": { "message": "Logged out" }, + "loggedOutDesc": { + "message": "You have been logged out of your account." + }, "loginExpired": { "message": "Your login session has expired." }, @@ -1212,6 +1224,12 @@ } } }, + "errorRefreshingAccessToken":{ + "message": "Access Token Refresh Error" + }, + "errorRefreshingAccessTokenDesc":{ + "message": "No refresh token or API keys found. Please try logging out and logging back in." + }, "help": { "message": "Help" }, @@ -2474,6 +2492,12 @@ "important": { "message": "Important:" }, + "accessTokenUnableToBeDecrypted": { + "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." + }, + "refreshTokenSecureStorageRetrievalFailure": { + "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + }, "masterPasswordHint": { "message": "Your master password cannot be recovered if you forget it!" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 5e337875324..45b724e47e4 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organisation vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organisations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index efc49892145..da4dca84e4a 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organisation vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organisation vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organisations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 0b6c91a9212..55e9daa72b3 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index a6a8b2a8027..130c18b391e 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -1299,12 +1299,42 @@ "message": "Contraseña actualizada", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exportar caja fuerte" }, "fileFormat": { "message": "Formato de archivo" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Bloqueado" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 7217568b510..d26fc652131 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -1299,12 +1299,42 @@ "message": "Parool on uuendatud", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Ekspordi hoidla" }, "fileFormat": { "message": "Failivorming" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Lukustatud" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 0f41c667e91..82406869f5a 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1299,12 +1299,42 @@ "message": "Pasahitza eguneratu da", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Esportatu kutxa gotorra" }, "fileFormat": { "message": "Fitxategiaren formatua" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Blokeatuta" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index f5a5c3575ce..3c5fb785b7f 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -1299,12 +1299,42 @@ "message": "کلمه عبور به‌روزرسانی شد", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "برون ریزی گاوصندوق" }, "fileFormat": { "message": "فرمت پرونده" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "نشانی اینترنتی hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "قفل شده" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 893da1309f3..08447614433 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -1299,12 +1299,42 @@ "message": "Salasana vaihdettiin", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Vie lähteestä" + }, "exportVault": { "message": "Vie holvi" }, "fileFormat": { "message": "Tiedostomuoto" }, + "fileEncryptedExportWarningDesc": { + "message": "Tämä vientitiedosto suojataan salasanalla, joka on syötettävä ja salauksen purkamiseksi." + }, + "filePassword": { + "message": "Tiedoston salasana" + }, + "exportPasswordDescription": { + "message": "Tätä salasanaa käytetään tämän tiedoston viennissä ja tuonnissa" + }, + "accountRestrictedOptionDescription": { + "message": "Salaa vienti ja rajoita tuonti vain nykyiselle Bitwarden-tilille tilisi käyttäjätunnukseen ja pääsalasanaan pohjautuvalla salausavaimella." + }, + "passwordProtected": { + "message": "Salasanasuojattu" + }, + "passwordProtectedOptionDescription": { + "message": "Salaa vientitiedosto salasanalla, joka mahdollistaa sen tuonnin mille tahansa Bitwarden-tilille." + }, + "exportTypeHeading": { + "message": "Viennin tyyppi" + }, + "accountRestricted": { + "message": "Tiliä on rajoitettu" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Tiedoston salasana\" ja \"Vahvista tiedoston salasana\" eivät täsmää." + }, "hCaptchaUrl": { "message": "hCaptcha-URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Organisaation holvin vienti" + }, + "exportingOrganizationVaultDesc": { + "message": "Vain organisaatioon $ORGANIZATION$ liitetyn holvin kohteet viedään. Yksityisen holvin ja muiden organisaatioiden kohteita ei sisällytetä.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Lukittu" }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index e45770ab180..0180d46959e 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -1299,12 +1299,42 @@ "message": "Na-update ang password", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "I-export vault" }, "fileFormat": { "message": "Format ng file" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Naka-lock" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 163bc548acf..ef8f783dd31 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -1299,12 +1299,42 @@ "message": "Mot de passe mis à jour", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exporter le coffre" }, "fileFormat": { "message": "Format de fichier" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "URL hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Verrouillé" }, diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index c489ab72602..7b9cd9ef0f1 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 7a28b04d2e0..0f664ed02e7 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -1299,12 +1299,42 @@ "message": "הסיסמה עודכנה", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "יצוא כספת" }, "fileFormat": { "message": "תבנית קובץ" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "כתובת אתר hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "נָעוּל" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 3e752df95eb..765510dc32e 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index be3c7c28e37..d1d4a4672b5 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1299,12 +1299,42 @@ "message": "Lozinka ažurirana", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Izvezi trezor" }, "fileFormat": { "message": "Format datoteke" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Zaključano" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 927fe859009..869b92167f6 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -1299,12 +1299,42 @@ "message": "A jelszó frissítésre került.", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Exportálás innen:" + }, "exportVault": { "message": "Széf exportálása" }, "fileFormat": { "message": "Fájlformátum" }, + "fileEncryptedExportWarningDesc": { + "message": "Ez a fájl exportálás jelszóval védett és a visszafejtéshez a fájl jelszó megadása szükséges." + }, + "filePassword": { + "message": "Fájl jelszó" + }, + "exportPasswordDescription": { + "message": "Ezt a jelszó kerül használatba a fájl exportálására és importálására." + }, + "accountRestrictedOptionDescription": { + "message": "Használjuk a fiók felhasználónevéből és mesterjelszavából származó fióktitkosítási kulcsot az exportálás titkosításához és az importálást csak az aktuális Bitwarden fiókra korlátozzuk." + }, + "passwordProtected": { + "message": "Jelszóval védett" + }, + "passwordProtectedOptionDescription": { + "message": "Állítsunk be egy fájl jelszót az exportálás titkosításához és importáljuk azt bármely Bitwarden fiókba a visszafejtéshez használt jelszó használatával." + }, + "exportTypeHeading": { + "message": "Exportálási típus" + }, + "accountRestricted": { + "message": "Korlátozott fiók" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "A “Fájl jelszó” és a “Fájl jelszó megerősítés“ nem egyezik." + }, "hCaptchaUrl": { "message": "hCaptcha webcím", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Szervezeti széf exportálása" + }, + "exportingOrganizationVaultDesc": { + "message": "Csak $ORGANIZATION$ névvel társított szervezeti széf kerül exportálásra. Ebbe nem kerülnek be a személyes és más szervezeti széf elemek.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Lezárva" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 46735671f04..cc25a1c73ff 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -1299,12 +1299,42 @@ "message": "Kata Sandi telah Diperbarui", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Ekspor Brankas" }, "fileFormat": { "message": "File Format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Terkunci" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 7ab75fd9815..b016dc5e480 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1299,12 +1299,42 @@ "message": "Password aggiornata", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Esporta da" + }, "exportVault": { "message": "Esporta cassaforte" }, "fileFormat": { "message": "Formato file" }, + "fileEncryptedExportWarningDesc": { + "message": "Questo file esportato sarà protetto e richiederà la password del file per decifrarlo." + }, + "filePassword": { + "message": "Password del file" + }, + "exportPasswordDescription": { + "message": "La password sarà utilizzata per importare ed esportare questo file" + }, + "accountRestrictedOptionDescription": { + "message": "Usa la chiave di crittografia dell'account, derivata dal nome utente e dalla password principale del tuo account, per crittografare il file di esportazione e limitare l'importazione solo all'account Bitwarden corrente." + }, + "passwordProtected": { + "message": "Protetto da password" + }, + "passwordProtectedOptionDescription": { + "message": "Imposta una password del file per crittografare il file esportato e importarlo in qualsiasi account Bitwarden usando la password per decrittografarlo." + }, + "exportTypeHeading": { + "message": "Tipo di esportazione" + }, + "accountRestricted": { + "message": "Account limitato" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "Le due password del file non corrispondono." + }, "hCaptchaUrl": { "message": "URL hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Esportando cassaforte dell'organizzazione" + }, + "exportingOrganizationVaultDesc": { + "message": "Solo la cassaforte dell'organizzazione associata a $ORGANIZATION$ sarà esportata. Elementi nelle casseforti individuali o in altre organizzazioni non saranno inclusi.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Bloccato" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index e5a3fbcfb93..0be6aea461f 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -1299,12 +1299,42 @@ "message": "パスワード更新日", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "エクスポート元" + }, "exportVault": { "message": "保管庫のエクスポート" }, "fileFormat": { "message": "ファイル形式" }, + "fileEncryptedExportWarningDesc": { + "message": "エクスポートするファイルはパスワードで保護され、復号するにはファイルパスワードが必要になります。" + }, + "filePassword": { + "message": "ファイルパスワード" + }, + "exportPasswordDescription": { + "message": "このパスワードはこのファイルのエクスポートとインポート時に使用します" + }, + "accountRestrictedOptionDescription": { + "message": "アカウントのユーザー名とマスターパスワードから得られる暗号化キーを使用してエクスポートするデータを暗号化し、現在の Bitwarden アカウントのみがインポートできるよう制限します。" + }, + "passwordProtected": { + "message": "パスワード保護あり" + }, + "passwordProtectedOptionDescription": { + "message": "エクスポートを暗号化するためのファイルパスワードを設定します。そのパスワードを使用して、任意の Bitwarden アカウントにインポートします。" + }, + "exportTypeHeading": { + "message": "エクスポートの種類" + }, + "accountRestricted": { + "message": "アカウント制限" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "「ファイルパスワード」と「ファイルパスワードの確認」が一致しません。" + }, "hCaptchaUrl": { "message": "hCaptcha URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "組織保管庫のエクスポート" + }, + "exportingOrganizationVaultDesc": { + "message": "$ORGANIZATION$ に関連付けられた組織保管庫のみがエクスポートされます。個々の保管庫または他の組織にあるアイテムは含まれません。", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "ロック中" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index c489ab72602..7b9cd9ef0f1 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index c489ab72602..7b9cd9ef0f1 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 8379628c437..63f2d0326d0 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -1299,12 +1299,42 @@ "message": "ಪಾಸ್ವರ್ಡ್ ನವೀಕರಿಸಲಾಗಿದೆ", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "ರಫ್ತು ವಾಲ್ಟ್" }, "fileFormat": { "message": "ಕಡತದ ಮಾದರಿ" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 5b1bcafa425..1a5e6353520 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -1299,12 +1299,42 @@ "message": "비밀번호 업데이트됨", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "보관함 내보내기" }, "fileFormat": { "message": "파일 형식" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "잠김" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index e24f4b78f8a..edb15332c22 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1299,12 +1299,42 @@ "message": "Slaptažodis atnaujintas", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Eksportuoti saugyklą" }, "fileFormat": { "message": "Failo formatas" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha nuoroda", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Užrakinta" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index d93dc9e8599..4a91e491a54 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1299,12 +1299,42 @@ "message": "Parole atjaunināta", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Izgūt no" + }, "exportVault": { "message": "Izgūt glabātavas saturu" }, "fileFormat": { "message": "Datnes veids" }, + "fileEncryptedExportWarningDesc": { + "message": "Šī datņu izgūšana būs aizsargāta ar paroli, un būs nepieciešama datnes parole, lai to atšifrētu." + }, + "filePassword": { + "message": "Datnes parole" + }, + "exportPasswordDescription": { + "message": "Šī parole tiks izmantota, lai izgūtu un ievietotu šo datni" + }, + "accountRestrictedOptionDescription": { + "message": "Jāizmanto konta šifrēšanas atslēga, kas iegūta no lietotājvārda un galvenās paroles, lai šifrētu izguvi un atļautu ievietošanu tikai pašreizējā Bitwarden kontā." + }, + "passwordProtected": { + "message": "Aizsargāts ar paroli" + }, + "passwordProtectedOptionDescription": { + "message": "Uzstādīt paroli, lai šifrētu izguvi un tad to ievietotu jebkurā Bitwarden kontā, izmantojot atšifrēšanas paroli." + }, + "exportTypeHeading": { + "message": "Izgūšanas veids" + }, + "accountRestricted": { + "message": "Konts ir ierobežots" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Datnes parole\" un \"Apstiprināt datnes paroli\" vērtības nesakrīt." + }, "hCaptchaUrl": { "message": "hCaptcha URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -1347,7 +1377,7 @@ "message": "Apstiprināt glabātavas satura izgūšanu" }, "exportWarningDesc": { - "message": "Šī izguve satur glabātavas datus nešifrētā veidā. Izdoto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Izdzēst to uzreiz pēc izmantošanas." + "message": "Šī izguve satur glabātavas datus nešifrētā veidā. Izdoto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Tā ir jāizdzēš uzreiz pēc izmantošanas." }, "encExportKeyWarningDesc": { "message": "Šī izguve šifrē datus ar konta šifrēšanas atslēgu. Ja tā jebkad tiks mainīta, izvadi vajadzētu veikt vēlreiz, jo vairs nebūs iespējams atšifrēt šo datni." @@ -2060,7 +2090,7 @@ "message": "Sesijai iestājās noildze. Lūgums mēģināt pieteikties vēlreiz." }, "exportingPersonalVaultTitle": { - "message": "Izdod personīgo glabātavu" + "message": "Izgūst personīgo glabātavu" }, "exportingIndividualVaultDescription": { "message": "Tiks izgūti tikai atsevišķi glabātavas vienumi, kas ir saistīti ar $EMAIL$. Apvienības glabātavas vienumi netiks iekļauti. Tiks izgūta tikai glabātavas vienumu informācija, un saistītie pielikumi netiks iekļauti.", @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Izgūst apvienības glabātavu" + }, + "exportingOrganizationVaultDesc": { + "message": "Tiks izgūta tikai apvienības glabātava, kas ir saistīta ar $ORGANIZATION$. Atsevišķu glabātavu vai citu apvienību vienumi netiks iekļauti.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Aizslēgta" }, @@ -2685,7 +2727,7 @@ "message": "Kļūda izguves datnes atšifrēšanā. Izmantotā atslēga neatbilst tai, kas tika izmantota satura izgūšanai." }, "invalidFilePassword": { - "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izdošanas datnes izveidošanas brīdī." + "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izgūšanas datnes izveidošanas brīdī." }, "importDestination": { "message": "Ievietošanas galamērķis" diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index a19623dbf9e..400fc1bba9e 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -1299,12 +1299,42 @@ "message": "Lozinka ažurirana", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Izvezi trezor" }, "fileFormat": { "message": "Format datoteke" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index d1a20855478..fec41cd119c 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -1299,12 +1299,42 @@ "message": "പാസ്‍വേഡ് പുതുക്കി", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "വാൾട് എക്സ്പോർട്" }, "fileFormat": { "message": "ഫയൽ ഫോർമാറ്റ്" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index c489ab72602..7b9cd9ef0f1 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 2f19a2f513f..54f943a72c8 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 5af3025f0e5..1f6c7fed646 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -1299,12 +1299,42 @@ "message": "Passordet ble oppdatert den", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Eksporter hvelvet" }, "fileFormat": { "message": "Filformat" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Låst" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 6f6f7d95989..a766c4bb6e5 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 7d5132c872f..f3fe4886abc 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -1299,12 +1299,42 @@ "message": "Wachtwoord bijgewerkt", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Exporteren vanuit" + }, "exportVault": { "message": "Kluis exporteren" }, "fileFormat": { "message": "Bestandsindeling" }, + "fileEncryptedExportWarningDesc": { + "message": "We beveiligen deze bestandsexport met een wachtwoord beveiligd, je hebt het bestandswachtwoord nodig om het te decoderen." + }, + "filePassword": { + "message": "Bestandswachtwoord" + }, + "exportPasswordDescription": { + "message": "We gebruiken dit wachtwoord bij het exporteren en importeren van dit bestand" + }, + "accountRestrictedOptionDescription": { + "message": "Gebruik de encryptiesleutel van je account, afgeleid van je gebruikersnaam en hoodfwachtwoord, om de export te versleutelen en importeren te beperken tot het huidige Bitwarden-account." + }, + "passwordProtected": { + "message": "Beveiligd met wachtwoord" + }, + "passwordProtectedOptionDescription": { + "message": "Stel een bestandswachtwoord in om de export te versleutelen en te importeren naar een willekeurig Bitwarden-account met het wachtwoord voor decoderen." + }, + "exportTypeHeading": { + "message": "Exporttype" + }, + "accountRestricted": { + "message": "Account beperkt" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Bestandswachtwoord\" en \"Bestandswachtwoord bevestigen\" komen niet overeen." + }, "hCaptchaUrl": { "message": "hCaptcha-URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Organisatiekluis exporteren" + }, + "exportingOrganizationVaultDesc": { + "message": "Exporteert alleen de organisatiekluis van $ORGANIZATION$. Geen persoonlijke kluis-items of items van andere organisaties.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Vergrendeld" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 0de15cbe56b..10016168346 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "Filformat" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha-nettadresse", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index bcc358730ae..8146636277b 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 237d94faa38..01fe7622e71 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -1299,12 +1299,42 @@ "message": "Hasło zostało zaktualizowane", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Eksportuj z" + }, "exportVault": { "message": "Eksportuj sejf" }, "fileFormat": { "message": "Format pliku" }, + "fileEncryptedExportWarningDesc": { + "message": "Plik będzie chroniony hasłem, które będzie wymagane do odszyfrowania pliku." + }, + "filePassword": { + "message": "Hasło do pliku" + }, + "exportPasswordDescription": { + "message": "Hasło będzie używane do eksportowania i importowania pliku" + }, + "accountRestrictedOptionDescription": { + "message": "Użyj klucza szyfrowania konta, pochodzącego z nazwy użytkownika konta i hasła głównego, aby zaszyfrować eksport i ograniczyć import tylko do bieżącego konta Bitwarden." + }, + "passwordProtected": { + "message": "Chroniona hasłem" + }, + "passwordProtectedOptionDescription": { + "message": "Ustaw hasło dla pliku, aby zaszyfrować eksport i zaimportować je na dowolne konto Bitwarden przy użyciu hasła do odszyfrowania." + }, + "exportTypeHeading": { + "message": "Rodzaj eksportu" + }, + "accountRestricted": { + "message": "Konto ograniczone" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“Hasło pliku” i “Potwierdź hasło pliku“ nie pasują do siebie." + }, "hCaptchaUrl": { "message": "Adres URL hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Eksportowanie sejfu organizacji" + }, + "exportingOrganizationVaultDesc": { + "message": "Tylko sejf organizacji powiązany z $ORGANIZATION$ zostanie wyeksportowany. Pozycje w poszczególnych sejfach lub innych organizacji nie będą uwzględnione.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Zablokowany" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 225bef63126..77ae4a5592b 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -1299,12 +1299,42 @@ "message": "Senha atualizada", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Exportar cofre" }, "fileFormat": { "message": "Formato do arquivo" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Bloqueado" }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index b2ff16748ec..5596e6ee45e 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -1299,12 +1299,42 @@ "message": "Palavra-passe atualizada a", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Exportar de" + }, "exportVault": { "message": "Exportar cofre" }, "fileFormat": { "message": "Formato do ficheiro" }, + "fileEncryptedExportWarningDesc": { + "message": "A exportação deste ficheiro será protegida por uma palavra-passe e exigirá a palavra-passe do ficheiro para ser desencriptada." + }, + "filePassword": { + "message": "Palavra-passe do ficheiro" + }, + "exportPasswordDescription": { + "message": "Esta palavra-passe será utilizada para exportar e importar este ficheiro" + }, + "accountRestrictedOptionDescription": { + "message": "Utilize a chave de encriptação da sua conta, derivada do nome de utilizador e da palavra-passe mestra da sua conta, para encriptar a exportação e restringir a importação apenas à conta Bitwarden atual." + }, + "passwordProtected": { + "message": "Protegido por palavra-passe" + }, + "passwordProtectedOptionDescription": { + "message": "Defina uma palavra-passe do ficheiro para encriptar a exportação e importe-a para qualquer conta Bitwarden utilizando a palavra-passe de desencriptação." + }, + "exportTypeHeading": { + "message": "Tipo de exportação" + }, + "accountRestricted": { + "message": "Conta restringida" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Palavra-passe do ficheiro\" e \"Confirmar palavra-passe do ficheiro\" não correspondem." + }, "hCaptchaUrl": { "message": "URL do hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "A exportar o cofre da organização" + }, + "exportingOrganizationVaultDesc": { + "message": "Apenas o cofre da organização associado a $ORGANIZATION$ será exportado. Os itens em cofres individuais ou noutras organizações não serão incluídos.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Bloqueado" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 27c9bcd1229..9ffe8da928c 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1299,12 +1299,42 @@ "message": "Parolă actualizată", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export de seif" }, "fileFormat": { "message": "Format de fișier" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "Url-ul hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Blocat" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index e6e621ed55f..665a18d1cb7 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -1299,12 +1299,42 @@ "message": "Пароль обновлен", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Экспорт из" + }, "exportVault": { "message": "Экспорт хранилища" }, "fileFormat": { "message": "Формат файла" }, + "fileEncryptedExportWarningDesc": { + "message": "Экспорт этого файла будет защищен паролем, и для расшифровки потребуется пароль файла." + }, + "filePassword": { + "message": "Пароль к файлу" + }, + "exportPasswordDescription": { + "message": "Этот пароль будет использоваться для экспорта и импорта этого файла" + }, + "accountRestrictedOptionDescription": { + "message": "Использовать ключ шифрования вашего аккаунта, полученный из имени пользователя и мастер-пароля, для шифрования экспорта и ограничения импорта только для текущего аккаунта Bitwarden." + }, + "passwordProtected": { + "message": "Пароль защищен" + }, + "passwordProtectedOptionDescription": { + "message": "Установите пароль файла для шифрования экспорта и импортируйте его в любую учетную запись Bitwarden, используя пароль для расшифровки." + }, + "exportTypeHeading": { + "message": "Тип экспорта" + }, + "accountRestricted": { + "message": "Ограничено аккаунтом" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Пароль к файлу\" и \"Подтверждение пароля к файлу\" не совпадают." + }, "hCaptchaUrl": { "message": "URL hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Экспорт хранилища организации" + }, + "exportingOrganizationVaultDesc": { + "message": "Будет экспортировано только хранилище организации, связанное с $ORGANIZATION$. Элементы из личных хранилищ и из других организаций включены не будут.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Заблокировано" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 829febb7a93..b2c744761fe 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index f6ab99455c4..af48e2ed529 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -1299,12 +1299,42 @@ "message": "Heslo bolo aktualizované", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Exportovať z" + }, "exportVault": { "message": "Export trezoru" }, "fileFormat": { "message": "Formát Súboru" }, + "fileEncryptedExportWarningDesc": { + "message": "Tento exportovaný súbor bude chránený heslom a na dešifrovanie bude potrebné heslo súboru." + }, + "filePassword": { + "message": "Heslo súboru" + }, + "exportPasswordDescription": { + "message": "Toto heslo sa použije na export a import tohto súboru" + }, + "accountRestrictedOptionDescription": { + "message": "Na zašifrovanie exportu a obmedzenie importu len na aktuálny účet Bitwarden použite šifrovací kľúč účtu odvodený z používateľského mena a hlavného hesla účtu." + }, + "passwordProtected": { + "message": "Chránené heslom" + }, + "passwordProtectedOptionDescription": { + "message": "Nastavte heslo súboru na zašifrovanie exportu a importujte ho do akéhokoľvek účtu Bitwarden pomocou hesla na dešifrovanie." + }, + "exportTypeHeading": { + "message": "Typ exportu" + }, + "accountRestricted": { + "message": "Obmedzený účet" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "\"Heslo súboru\" a \"Potvrdiť heslo súboru\" sa nezhodujú." + }, "hCaptchaUrl": { "message": "URL hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exportovanie trezora organizácie" + }, + "exportingOrganizationVaultDesc": { + "message": "Exportované budú iba položky trezora organizácie spojené s $ORGANIZATION$. Položky osobného trezora a položky z iných organizácií nebudú zahrnuté.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Zamknutý" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 968f34ad747..9bdec66f19c 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -1299,12 +1299,42 @@ "message": "Geslo je bilo posodobljeno", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Izvoz trezorja" }, "fileFormat": { "message": "Format datoteke" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 7967e25b3f6..de403629d14 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1299,12 +1299,42 @@ "message": "Лозинка ажурирана", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Извоз сефа" }, "fileFormat": { "message": "Формат датотеке" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Закључано" }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 6e731f777c5..9bae4e883dc 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -1299,12 +1299,42 @@ "message": "Lösenordet uppdaterades", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Exportera från" + }, "exportVault": { "message": "Exportera valv" }, "fileFormat": { "message": "Filformat" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha-URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Låst" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index c489ab72602..7b9cd9ef0f1 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -1299,12 +1299,42 @@ "message": "Password updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export vault" }, "fileFormat": { "message": "File format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index dce9013765f..cfc701aa5aa 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -1299,12 +1299,42 @@ "message": "Password Updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export Vault" }, "fileFormat": { "message": "File Format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha Url", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Locked" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index ad828189877..224bf36cfd7 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1299,12 +1299,42 @@ "message": "Parola güncelleme", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Kasayı dışa aktar" }, "fileFormat": { "message": "Dosya biçimi" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha adresi", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Kilitli" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 6503f3c19ab..546005db20c 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1299,12 +1299,42 @@ "message": "Пароль оновлено", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Експортувати з" + }, "exportVault": { "message": "Експортувати сховище" }, "fileFormat": { "message": "Формат файлу" }, + "fileEncryptedExportWarningDesc": { + "message": "Цей експортований файл буде захищений паролем, який необхідно ввести для його розшифрування." + }, + "filePassword": { + "message": "Пароль файлу" + }, + "exportPasswordDescription": { + "message": "Цей пароль буде використано для експортування та імпортування цього файлу" + }, + "accountRestrictedOptionDescription": { + "message": "Використовуйте ключ шифрування свого облікового запису, створений на основі імені користувача й головного пароля, щоб зашифрувати експортовані дані та обмежити можливість імпортування лише до поточного облікового запису Bitwarden." + }, + "passwordProtected": { + "message": "Захищено паролем" + }, + "passwordProtectedOptionDescription": { + "message": "Встановіть пароль файлу, щоб зашифрувати експортовані дані та імпортувати до будь-якого облікового запису Bitwarden за допомогою цього пароля." + }, + "exportTypeHeading": { + "message": "Тип експорту" + }, + "accountRestricted": { + "message": "Обмежено обліковим записом" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "Пароль файлу та підтвердження пароля відрізняються." + }, "hCaptchaUrl": { "message": "URL-адреса hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Експортування сховища організації" + }, + "exportingOrganizationVaultDesc": { + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Заблоковано" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 1a0689b1329..0fa1d7253af 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -1299,12 +1299,42 @@ "message": "Password Updated", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "Export Vault" }, "fileFormat": { "message": "File Format" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "Url hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "Đã khóa" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 35cb9ec07ba..22b96d6e4b6 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1299,12 +1299,42 @@ "message": "密码更新于", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "导出自" + }, "exportVault": { "message": "导出密码库" }, "fileFormat": { "message": "文件格式" }, + "fileEncryptedExportWarningDesc": { + "message": "此文件导出将受密码保护,需要文件密码才能解密。" + }, + "filePassword": { + "message": "文件密码" + }, + "exportPasswordDescription": { + "message": "此密码将用于导出和导入此文件" + }, + "accountRestrictedOptionDescription": { + "message": "使用衍生自您账户的用户名和主密码的账户加密密钥,以加密此导出并限制只能导入到当前的 Bitwarden 账户。" + }, + "passwordProtected": { + "message": "密码保护" + }, + "passwordProtectedOptionDescription": { + "message": "设置一个文件密码用来加密此导出,并使用此密码解密以导入到任意 Bitwarden 账户。" + }, + "exportTypeHeading": { + "message": "导出类型" + }, + "accountRestricted": { + "message": "账户受限" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "「文件密码」与「确认文件密码」不一致。" + }, "hCaptchaUrl": { "message": "hCaptcha URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "正在导出组织密码库" + }, + "exportingOrganizationVaultDesc": { + "message": "仅会导出与 $ORGANIZATION$ 关联的组织密码库数据。不包括个人密码库和其他组织中的项目。", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "已锁定" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 9ff900a41e2..099865217c0 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -1299,12 +1299,42 @@ "message": "密碼更新於", "description": "ex. Date this password was updated" }, + "exportFrom": { + "message": "Export from" + }, "exportVault": { "message": "匯出密碼庫" }, "fileFormat": { "message": "檔案格式" }, + "fileEncryptedExportWarningDesc": { + "message": "This file export will be password protected and require the file password to decrypt." + }, + "filePassword": { + "message": "File password" + }, + "exportPasswordDescription": { + "message": "This password will be used to export and import this file" + }, + "accountRestrictedOptionDescription": { + "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + }, + "passwordProtected": { + "message": "Password protected" + }, + "passwordProtectedOptionDescription": { + "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + }, + "exportTypeHeading": { + "message": "Export type" + }, + "accountRestricted": { + "message": "Account restricted" + }, + "filePasswordAndConfirmFilePasswordDoNotMatch": { + "message": "“File password” and “Confirm file password“ do not match." + }, "hCaptchaUrl": { "message": "hCaptcha URL", "description": "hCaptcha is the name of a website, should not be translated" @@ -2071,6 +2101,18 @@ } } }, + "exportingOrganizationVaultTitle": { + "message": "Exporting organization vault" + }, + "exportingOrganizationVaultDesc": { + "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "placeholders": { + "organization": { + "content": "$1", + "example": "ACME Moving Co." + } + } + }, "locked": { "message": "已鎖定" }, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index d30d6ad821b..59a306189a5 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -3,6 +3,7 @@ import * as path from "path"; import { app } from "electron"; import { Subject, firstValueFrom } from "rxjs"; +import { LogoutReason } from "@bitwarden/auth/common"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -31,6 +32,7 @@ import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider"; import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service"; import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; +import { UserId } from "@bitwarden/common/types/guid"; /* eslint-enable import/no-restricted-paths */ import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service"; @@ -182,6 +184,7 @@ export class Main { this.keyGenerationService, this.encryptService, this.logService, + async (logoutReason: LogoutReason, userId?: UserId) => {}, ); this.migrationRunner = new MigrationRunner( @@ -207,11 +210,9 @@ export class Main { ); this.desktopSettingsService = new DesktopSettingsService(stateProvider); - const biometricStateService = new DefaultBiometricStateService(stateProvider); this.windowMain = new WindowMain( - this.stateService, biometricStateService, this.logService, this.storageService, diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index 64b4bc48d28..e82d16ee9fd 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -6,7 +6,6 @@ import { app, BrowserWindow, ipcMain, nativeTheme, screen, session } from "elect import { firstValueFrom } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; @@ -38,7 +37,6 @@ export class WindowMain { readonly defaultHeight = 600; constructor( - private stateService: StateService, private biometricStateService: BiometricStateService, private logService: LogService, private storageService: AbstractStorageService, diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 508c42fa720..34a4dc99f65 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.5.0", + "version": "2024.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.5.0", + "version": "2024.6.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index ea4b95491cb..3a629f37cb0 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.5.0", + "version": "2024.6.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/webpack.renderer.js b/apps/desktop/webpack.renderer.js index 1ebeadef055..dc3cdf1fef5 100644 --- a/apps/desktop/webpack.renderer.js +++ b/apps/desktop/webpack.renderer.js @@ -24,8 +24,7 @@ const common = { { loader: "babel-loader", options: { - configFile: false, - plugins: ["@angular/compiler-cli/linker/babel"], + configFile: "../../babel.config.json", }, }, ], diff --git a/apps/web/package.json b/apps/web/package.json index 6e5355c7086..286811dd5c6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.5.0", + "version": "2024.6.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index d1a48a78e11..237e2c6e30c 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -52,7 +52,7 @@ *ngIf="canShowBillingTab(organization)" > - + diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 47ca0998bbc..4383656bee1 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, RouterModule } from "@angular/router"; -import { map, mergeMap, Observable, Subject, takeUntil } from "rxjs"; +import { combineLatest, map, mergeMap, Observable, Subject, switchMap, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -16,7 +16,8 @@ import { OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -55,9 +56,14 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { organization$: Observable; showPaymentAndHistory$: Observable; hideNewOrgButton$: Observable; + organizationIsUnmanaged$: Observable; private _destroy = new Subject(); + protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + ); + protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, ); @@ -68,6 +74,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private configService: ConfigService, private policyService: PolicyService, + private providerService: ProviderService, ) {} async ngOnInit() { @@ -94,6 +101,24 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { ); this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg); + + const provider$ = this.organization$.pipe( + switchMap((organization) => this.providerService.get$(organization.providerId)), + ); + + this.organizationIsUnmanaged$ = combineLatest([ + this.consolidatedBillingEnabled$, + this.organization$, + provider$, + ]).pipe( + map( + ([consolidatedBillingEnabled, organization, provider]) => + !consolidatedBillingEnabled || + !organization.hasProvider || + !provider || + provider.providerStatus !== ProviderStatusType.Billable, + ), + ); } ngOnDestroy() { diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 149277b8179..2a092e26100 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -144,6 +144,7 @@ { - if (expired) { - this.platformUtilsService.showToast( - "warning", - this.i18nService.t("loggedOut"), - this.i18nService.t("loginExpired"), - ); - } - await this.stateService.clean({ userId: userId }); await this.accountService.clean(userId); diff --git a/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts b/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts index ef3d657f2f9..f7c391b0ee2 100644 --- a/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts +++ b/apps/web/src/app/auth/core/services/webauthn-login/request/webauthn-login-attestation-response.request.ts @@ -20,8 +20,8 @@ export class WebauthnLoginAttestationResponseRequest extends WebauthnLoginAuthen } this.response = { - attestationObject: Utils.fromBufferToB64(credential.response.attestationObject), - clientDataJson: Utils.fromBufferToB64(credential.response.clientDataJSON), + attestationObject: Utils.fromBufferToUrlB64(credential.response.attestationObject), + clientDataJson: Utils.fromBufferToUrlB64(credential.response.clientDataJSON), }; } } diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html b/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html index 4abb44db4f0..f57fb7a3510 100644 --- a/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/enterprise-content.component.html @@ -1,8 +1,8 @@ -

Start your 7-day free trial of Bitwarden

+

Start your 7-day Enterprise free trial

- Strengthen business security with the password manager designed for seamless administration and - employee usability. + Bitwarden is the most trusted password manager designed for seamless administration and employee + usability.

    @@ -15,14 +15,14 @@
  • Strengthen employee security practices through centralized administrative control and + >Strengthen company-wide security through centralized administrative control and policies
  • Streamline user onboarding and automate account provisioning with turnkey SSO and SCIM + >Streamline user onboarding and automate account provisioning with flexible SSO and SCIM integrations
  • @@ -35,14 +35,7 @@
  • Save time and increase productivity with autofill and instant device syncing -
  • -
  • - Empower employees to secure their digital life at home, at work, and on the go by offering a - free Families plan to all Enterprise usersGive all Enterprise users the gift of 360º security with a free Families plan
diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html b/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html index 120748d4c0d..f57fb7a3510 100644 --- a/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/enterprise1-content.component.html @@ -1,34 +1,44 @@ -

The Password Manager Trusted by Millions

-
-

Everything enterprises need out of a password manager:

+

Start your 7-day Enterprise free trial

+
+

+ Bitwarden is the most trusted password manager designed for seamless administration and employee + usability. +

    -
  • Secure password sharing
  • -
  • - Easy, flexible SSO and SCIM integrations +
  • + Instantly and securely share credentials with the groups and individuals who need them +
  • +
  • + Strengthen company-wide security through centralized administrative control and + policies +
  • +
  • + Streamline user onboarding and automate account provisioning with flexible SSO and SCIM + integrations +
  • +
  • + Migrate to Bitwarden in minutes with comprehensive import options +
  • +
  • + Give all Enterprise users the gift of 360º security with a free Families plan
  • -
  • Free families plan for users
  • -
  • Quick import and migration tools
  • -
  • Simple, streamlined user experience
  • -
  • Priority support and trainers
- -
- - - -
+
diff --git a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html b/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html index 120748d4c0d..f57fb7a3510 100644 --- a/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/enterprise2-content.component.html @@ -1,34 +1,44 @@ -

The Password Manager Trusted by Millions

-
-

Everything enterprises need out of a password manager:

+

Start your 7-day Enterprise free trial

+
+

+ Bitwarden is the most trusted password manager designed for seamless administration and employee + usability. +

    -
  • Secure password sharing
  • -
  • - Easy, flexible SSO and SCIM integrations +
  • + Instantly and securely share credentials with the groups and individuals who need them +
  • +
  • + Strengthen company-wide security through centralized administrative control and + policies +
  • +
  • + Streamline user onboarding and automate account provisioning with flexible SSO and SCIM + integrations +
  • +
  • + Migrate to Bitwarden in minutes with comprehensive import options +
  • +
  • + Give all Enterprise users the gift of 360º security with a free Families plan
  • -
  • Free families plan for users
  • -
  • Quick import and migration tools
  • -
  • Simple, streamlined user experience
  • -
  • Priority support and trainers
- -
- - - -
+
diff --git a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html b/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html index 42f99be26b8..f51c370bebd 100644 --- a/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/teams1-content.component.html @@ -1,6 +1,5 @@ -

Start your 7-day free trial for Teams

-
-
+

Start your 7-day free trial for Teams

+

Strengthen business security with an easy-to-use password manager your team will love.

diff --git a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html b/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html index 3145e20d4f8..f51c370bebd 100644 --- a/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/content/teams2-content.component.html @@ -1,17 +1,35 @@ -

Start Your Free Trial Now

-
+

Start your 7-day free trial for Teams

+

- Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password - storage and sharing. + Strengthen business security with an easy-to-use password manager your team will love.

-
    -
  • Collaborate and share securely
  • -
  • Deploy and manage quickly and easily
  • -
  • Access anywhere on any device
  • -
  • Create your account to get started
  • +
      +
    • + Instantly and securely share credentials with the groups and individuals who need them +
    • +
    • + Migrate to Bitwarden in minutes with comprehensive import options +
    • +
    • + Save time and increase productivity with autofill and instant device syncing +
    • +
    • + Enhance security practices across your team with easy user management +
    - - +
    diff --git a/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts new file mode 100644 index 00000000000..a915d8f8a6c --- /dev/null +++ b/apps/web/src/app/billing/guards/organization-is-unmanaged.guard.ts @@ -0,0 +1,36 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +export const organizationIsUnmanaged: CanActivateFn = async (route: ActivatedRouteSnapshot) => { + const configService = inject(ConfigService); + const organizationService = inject(OrganizationService); + const providerService = inject(ProviderService); + + const consolidatedBillingEnabled = await configService.getFeatureFlag( + FeatureFlag.EnableConsolidatedBilling, + ); + + if (!consolidatedBillingEnabled) { + return true; + } + + const organization = await organizationService.get(route.params.organizationId); + + if (!organization.hasProvider) { + return true; + } + + const provider = await providerService.get(organization.providerId); + + if (!provider) { + return true; + } + + return provider.providerStatus !== ProviderStatusType.Billable; +}; diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 8535f23f820..9b615f3a690 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -146,19 +146,17 @@ export class UserSubscriptionComponent implements OnInit { } }; - adjustStorage = (add: boolean) => { - return async () => { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: 4, - add: add, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); - } - }; + adjustStorage = async (add: boolean) => { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: 4, + add: add, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } }; get subscriptionMarkedForCancel() { diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts index 8ca7226b97d..4af06628754 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts @@ -5,6 +5,7 @@ import { canAccessBillingTab } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; +import { organizationIsUnmanaged } from "../../billing/guards/organization-is-unmanaged.guard"; import { WebPlatformUtilsService } from "../../core/web-platform-utils.service"; import { PaymentMethodComponent } from "../shared"; @@ -29,7 +30,7 @@ const routes: Routes = [ { path: "payment-method", component: PaymentMethodComponent, - canActivate: [OrganizationPermissionsGuard], + canActivate: [OrganizationPermissionsGuard, organizationIsUnmanaged], data: { titleId: "paymentMethod", organizationPermissions: (org: Organization) => org.canEditPaymentMethods, @@ -38,7 +39,7 @@ const routes: Routes = [ { path: "history", component: OrgBillingHistoryViewComponent, - canActivate: [OrganizationPermissionsGuard], + canActivate: [OrganizationPermissionsGuard, organizationIsUnmanaged], data: { titleId: "billingHistory", organizationPermissions: (org: Organization) => org.canViewBillingHistory, diff --git a/apps/web/src/app/core/router.service.ts b/apps/web/src/app/core/router.service.ts index c0c1ec26407..2944732aee6 100644 --- a/apps/web/src/app/core/router.service.ts +++ b/apps/web/src/app/core/router.service.ts @@ -12,6 +12,14 @@ import { GlobalState, } from "@bitwarden/common/platform/state"; +/** + * Data properties acceptable for use in route objects (see usage in oss-routing.module.ts for example) + */ +export interface DataProperties { + titleId?: string; // sets the title of the current HTML document (shows in browser tab) + doNotSaveUrl?: boolean; // choose to not keep track of the previous URL in memory +} + const DEEP_LINK_REDIRECT_URL = new KeyDefinition(ROUTER_DISK, "deepLinkRedirectUrl", { deserializer: (value: string) => value, }); diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index e543a6f0835..c7b4631fa30 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -40,6 +40,7 @@ import { UpdatePasswordComponent } from "./auth/update-password.component"; import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component"; import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "./auth/verify-recover-delete.component"; +import { DataProperties } from "./core"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; import { DomainRulesComponent } from "./settings/domain-rules.component"; @@ -54,7 +55,7 @@ const routes: Routes = [ { path: "", component: FrontendLayoutComponent, - data: { doNotSaveUrl: true }, + data: { doNotSaveUrl: true } satisfies DataProperties, children: [ { path: "", @@ -66,17 +67,17 @@ const routes: Routes = [ { path: "login-with-device", component: LoginViaAuthRequestComponent, - data: { titleId: "loginWithDevice" }, + data: { titleId: "loginWithDevice" } satisfies DataProperties, }, { path: "login-with-passkey", component: LoginViaWebAuthnComponent, - data: { titleId: "loginWithPasskey" }, + data: { titleId: "loginWithPasskey" } satisfies DataProperties, }, { path: "admin-approval-requested", component: LoginViaAuthRequestComponent, - data: { titleId: "adminApprovalRequested" }, + data: { titleId: "adminApprovalRequested" } satisfies DataProperties, }, { path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] }, { @@ -88,7 +89,7 @@ const routes: Routes = [ path: "register", component: TrialInitiationComponent, canActivate: [UnauthGuard], - data: { titleId: "createAccount" }, + data: { titleId: "createAccount" } satisfies DataProperties, }, { path: "trial", @@ -99,18 +100,18 @@ const routes: Routes = [ path: "sso", component: SsoComponent, canActivate: [UnauthGuard], - data: { titleId: "enterpriseSingleSignOn" }, + data: { titleId: "enterpriseSingleSignOn" } satisfies DataProperties, }, { path: "set-password", component: SetPasswordComponent, - data: { titleId: "setMasterPassword" }, + data: { titleId: "setMasterPassword" } satisfies DataProperties, }, { path: "hint", component: HintComponent, canActivate: [UnauthGuard], - data: { titleId: "passwordHint" }, + data: { titleId: "passwordHint" } satisfies DataProperties, }, { path: "lock", @@ -122,12 +123,12 @@ const routes: Routes = [ path: "accept-organization", canActivate: [deepLinkGuard()], component: AcceptOrganizationComponent, - data: { titleId: "joinOrganization", doNotSaveUrl: false }, + data: { titleId: "joinOrganization", doNotSaveUrl: false } satisfies DataProperties, }, { path: "accept-emergency", canActivate: [deepLinkGuard()], - data: { titleId: "acceptEmergency", doNotSaveUrl: false }, + data: { titleId: "acceptEmergency", doNotSaveUrl: false } satisfies DataProperties, loadComponent: () => import("./auth/emergency-access/accept/accept-emergency.component").then( (mod) => mod.AcceptEmergencyComponent, @@ -137,26 +138,26 @@ const routes: Routes = [ path: "accept-families-for-enterprise", component: AcceptFamilySponsorshipComponent, canActivate: [deepLinkGuard()], - data: { titleId: "acceptFamilySponsorship", doNotSaveUrl: false }, + data: { titleId: "acceptFamilySponsorship", doNotSaveUrl: false } satisfies DataProperties, }, { path: "recover", pathMatch: "full", redirectTo: "recover-2fa" }, { path: "recover-2fa", component: RecoverTwoFactorComponent, canActivate: [UnauthGuard], - data: { titleId: "recoverAccountTwoStep" }, + data: { titleId: "recoverAccountTwoStep" } satisfies DataProperties, }, { path: "recover-delete", component: RecoverDeleteComponent, canActivate: [UnauthGuard], - data: { titleId: "deleteAccount" }, + data: { titleId: "deleteAccount" } satisfies DataProperties, }, { path: "verify-recover-delete", component: VerifyRecoverDeleteComponent, canActivate: [UnauthGuard], - data: { titleId: "deleteAccount" }, + data: { titleId: "deleteAccount" } satisfies DataProperties, }, { path: "verify-recover-delete-org", @@ -168,30 +169,30 @@ const routes: Routes = [ path: "verify-recover-delete-provider", component: VerifyRecoverDeleteProviderComponent, canActivate: [UnauthGuard], - data: { titleId: "deleteAccount" }, + data: { titleId: "deleteAccount" } satisfies DataProperties, }, { path: "send/:sendId/:key", component: AccessComponent, - data: { title: "Bitwarden Send" }, + data: { titleId: "Bitwarden Send" } satisfies DataProperties, }, { path: "update-temp-password", component: UpdateTempPasswordComponent, canActivate: [AuthGuard], - data: { titleId: "updateTempPassword" }, + data: { titleId: "updateTempPassword" } satisfies DataProperties, }, { path: "update-password", component: UpdatePasswordComponent, canActivate: [AuthGuard], - data: { titleId: "updatePassword" }, + data: { titleId: "updatePassword" } satisfies DataProperties, }, { path: "remove-password", component: RemovePasswordComponent, canActivate: [AuthGuard], - data: { titleId: "removeMasterPassword" }, + data: { titleId: "removeMasterPassword" } satisfies DataProperties, }, { path: "migrate-legacy-encryption", @@ -211,21 +212,29 @@ const routes: Routes = [ path: "vault", loadChildren: () => VaultModule, }, - { path: "sends", component: SendComponent, data: { titleId: "send" } }, + { + path: "sends", + component: SendComponent, + data: { titleId: "send" } satisfies DataProperties, + }, { path: "create-organization", component: CreateOrganizationComponent, - data: { titleId: "newOrganization" }, + data: { titleId: "newOrganization" } satisfies DataProperties, }, { path: "settings", children: [ { path: "", pathMatch: "full", redirectTo: "account" }, - { path: "account", component: AccountComponent, data: { titleId: "myAccount" } }, + { + path: "account", + component: AccountComponent, + data: { titleId: "myAccount" } satisfies DataProperties, + }, { path: "preferences", component: PreferencesComponent, - data: { titleId: "preferences" }, + data: { titleId: "preferences" } satisfies DataProperties, }, { path: "security", @@ -234,7 +243,7 @@ const routes: Routes = [ { path: "domain-rules", component: DomainRulesComponent, - data: { titleId: "domainRules" }, + data: { titleId: "domainRules" } satisfies DataProperties, }, { path: "subscription", @@ -249,19 +258,19 @@ const routes: Routes = [ { path: "", component: EmergencyAccessComponent, - data: { titleId: "emergencyAccess" }, + data: { titleId: "emergencyAccess" } satisfies DataProperties, }, { path: ":id", component: EmergencyAccessViewComponent, - data: { titleId: "emergencyAccess" }, + data: { titleId: "emergencyAccess" } satisfies DataProperties, }, ], }, { path: "sponsored-families", component: SponsoredFamiliesComponent, - data: { titleId: "sponsoredFamilies" }, + data: { titleId: "sponsoredFamilies" } satisfies DataProperties, }, ], }, @@ -276,7 +285,7 @@ const routes: Routes = [ import("./tools/import/import-web.component").then((mod) => mod.ImportWebComponent), data: { titleId: "importData", - }, + } satisfies DataProperties, }, { path: "export", @@ -286,7 +295,7 @@ const routes: Routes = [ { path: "generator", component: GeneratorComponent, - data: { titleId: "generator" }, + data: { titleId: "generator" } satisfies DataProperties, }, ], }, diff --git a/apps/web/src/images/register-layout/vault-signup-badges.png b/apps/web/src/images/register-layout/vault-signup-badges.png index 7a80ffaebb9..c8a7ae2f48f 100644 Binary files a/apps/web/src/images/register-layout/vault-signup-badges.png and b/apps/web/src/images/register-layout/vault-signup-badges.png differ diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 06a957e8833..0bbcde1f493 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Toegangsvlak" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Uitgeteken" }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index e5f0fbe933b..92d90de8ded 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "تم تسجيل الخروج" }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 963ea19cc74..71f7f573d65 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Müraciət səviyyəsi" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Çıxış edildi" }, diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 8ec953ead5d..3127daed942 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Вы выйшлі" }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 17f37b27775..a1be0b8fdf1 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Ниво на достъп" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Бяхте отписани" }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index edf5b44ba56..525586cc863 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index b4a453bafe7..84dc065cec5 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index a759371fd4e..24f5f561e56 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Nivell d'accés" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Sessió tancada" }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 63be12e4052..7b08b7ef2f2 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Úroveň přístupu" }, + "accessing": { + "message": "Přistupování" + }, "loggedOut": { "message": "Odhlášení" }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 2b5bdb55473..a9b33e0e14f 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 9f689aa000d..f4031107f1c 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Adgangsniveau" }, + "accessing": { + "message": "Tilgår" + }, "loggedOut": { "message": "Logget ud" }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 008e0261d88..ecb3bf97291 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Zugriffsebene" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Ausgeloggt" }, diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index deed63f8e1a..42cc702f271 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Αποσυνδεθήκατε" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index c8f19318599..d7a21ad6d6a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -587,6 +587,9 @@ "loggedOut": { "message": "Logged out" }, + "loggedOutDesc": { + "message": "You have been logged out of your account." + }, "loginExpired": { "message": "Your login session has expired." }, @@ -1050,6 +1053,12 @@ "copyUuid": { "message": "Copy UUID" }, + "errorRefreshingAccessToken":{ + "message": "Access Token Refresh Error" + }, + "errorRefreshingAccessTokenDesc":{ + "message": "No refresh token or API keys found. Please try logging out and logging back in." + }, "warning": { "message": "Warning" }, @@ -5586,6 +5595,39 @@ "rotateBillingSyncTokenTitle": { "message": "Rotating the billing sync token will invalidate the previous token." }, + "selfHostedServer": { + "message": "self-hosted" + }, + "customEnvironment": { + "message": "Custom environment" + }, + "selfHostedBaseUrlHint": { + "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + }, + "selfHostedCustomEnvHeader" :{ + "message": "For advanced configuration, you can specify the base URL of each service independently." + }, + "selfHostedEnvFormInvalid" :{ + "message": "You must add either the base Server URL or at least one custom environment." + }, + "apiUrl": { + "message": "API server URL" + }, + "webVaultUrl": { + "message": "Web vault server URL" + }, + "identityUrl": { + "message": "Identity server URL" + }, + "notificationsUrl": { + "message": "Notifications server URL" + }, + "iconsUrl": { + "message": "Icons server URL" + }, + "environmentSaved": { + "message": "Environment URLs saved" + }, "selfHostingTitle": { "message": "Self-hosting" }, @@ -8297,5 +8339,20 @@ }, "allLoginRequestsApproved": { "message": "All login requests approved" + }, + "payPal": { + "message": "PayPal" + }, + "bitcoin": { + "message": "Bitcoin" + }, + "updatedTaxInformation": { + "message": "Updated tax information" + }, + "unverified": { + "message": "Unverified" + }, + "verified": { + "message": "Verified" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 77e378f8bed..93f1832301a 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index f71834e6d81..f1fa51236e5 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 8c518ab273e..8e0b58c3f1c 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Adiaŭita" }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 69d7a5ddbbe..8eed13491ee 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Nivel de acceso" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Sesión terminada" }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index a601c437b15..27f15dc4745 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Välja logitud" }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0e8fc63c3aa..3bb6a81998c 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Saioa itxita" }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index d12af42e3b6..42e1ff68cc5 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "خارج شد" }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 53b9723509a..3b0e8b4b844 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Käyttöoikeustaso" }, + "accessing": { + "message": "Avataan" + }, "loggedOut": { "message": "Kirjauduttu ulos" }, @@ -8290,9 +8293,9 @@ "message": "Pysy haavoittuvuuksien edellä tehostamalla valvontaa päivittämällä maksulliseen tilaukseeen." }, "approveAllRequests": { - "message": "Approve all requests" + "message": "Hyväksy kaikki pyynnöt" }, "allLoginRequestsApproved": { - "message": "All login requests approved" + "message": "Kaikki kirjautumispyynnöt hyväksyttiin" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 96d7398e4e3..1a9f2d67823 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Naka-log out" }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index a915955960b..189571f70d0 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Niveau d'accès" }, + "accessing": { + "message": "Accès en cours" + }, "loggedOut": { "message": "Déconnecté" }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 59167b94d9d..99c15045cc9 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 3ab0bf2dc14..f84612ef46b 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "בוצעה יציאה" }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 04d2d691c3a..65a353768a5 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index e88ba8016f3..7f939ba90ca 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Odjavljen/a" }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index af4e018159a..9226307792f 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Hozzáférési szint" }, + "accessing": { + "message": "Elérés" + }, "loggedOut": { "message": "Megtörtént a kijelentkezés." }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index d1d81cc3382..a767d3bcb0a 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Keluar" }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 02d01034ef9..d7869336003 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Livello di accesso" }, + "accessing": { + "message": "Accedendo a" + }, "loggedOut": { "message": "Uscito" }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index f4bd38e6554..b8016fb1f80 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "アクセスレベル" }, + "accessing": { + "message": "アクセス中" + }, "loggedOut": { "message": "ログアウトしました" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 6c0a32b6672..c1cb6d9a96a 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "გამოსვლა" }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 59167b94d9d..99c15045cc9 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index ef3d2f4d180..3d369d33fc7 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "ಲಾಗ್ ಔಟ್" }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index df54ad4d32e..6a7b32983b6 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "접근 권한" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "로그아웃됨" }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index febb28d0565..61edf711b4d 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Piekļuves līmenis" }, + "accessing": { + "message": "Piekļūst" + }, "loggedOut": { "message": "Atteicies" }, @@ -833,7 +836,7 @@ "message": "Nederīga galvenā parole" }, "invalidFilePassword": { - "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izdošanas datnes izveidošanas brīdī." + "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izgūšanas datnes izveidošanas brīdī." }, "lockNow": { "message": "Aizslēgt" @@ -1057,10 +1060,10 @@ "message": "Apstiprināt noslēpumu izgūšanu" }, "exportWarningDesc": { - "message": "Šī izguve satur glabātavas datus nešifrētā veidā. Izdoto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Izdzēst to uzreiz pēc izmantošanas." + "message": "Šī izguve satur glabātavas datus nešifrētā veidā. Izgūto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Tā ir jāizdzēš uzreiz pēc izmantošanas." }, "exportSecretsWarningDesc": { - "message": "Šī izguve satur noslēpumu datus nešifrētā veidā. Izdoto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Izdzēst to uzreiz pēc izmantošanas." + "message": "Šī izguve satur noslēpumu datus nešifrētā veidā. Izgūto datni nevajadzētu glabāt vai sūtīt nedrošos veidos (piemēram, e-pastā). Tā ir jāizdzēš uzreiz pēc izmantošanas." }, "encExportKeyWarningDesc": { "message": "Šī izguve šifrē datus ar konta šifrēšanas atslēgu. Ja tā jebkad tiks mainīta, izvadi vajadzētu veikt vēlreiz, jo vairs nebūs iespējams atšifrēt šo datni." @@ -1084,10 +1087,10 @@ "message": "Datnes veids" }, "fileEncryptedExportWarningDesc": { - "message": "Šī datņu izdošana būs aizsargāta ar paroli, un būs nepieciešama datnes parole, lai to atšifrētu." + "message": "Šī datņu izgūšana būs aizsargāta ar paroli, un būs nepieciešama datnes parole, lai to atšifrētu." }, "exportPasswordDescription": { - "message": "Šī parole tiks izmantota, lai izdotu un ievietotu šo datni" + "message": "Šī parole tiks izmantota, lai izgūtu un ievietotu šo datni" }, "confirmMasterPassword": { "message": "Apstiprināt galveno paroli" @@ -1102,13 +1105,13 @@ "message": "Apstiprināt datnes paroli" }, "accountRestrictedOptionDescription": { - "message": "Izmantot konta šifrēšanas atslēgu, kas iegūta no lietotājvārda un galvenās paroles, lai šifrētu izdošanu un atļautu ievietošanu tikai pašreizējā Bitwarden kontā." + "message": "Jāizmanto konta šifrēšanas atslēga, kas iegūta no lietotājvārda un galvenās paroles, lai šifrētu izguvi un atļautu ievietošanu tikai pašreizējā Bitwarden kontā." }, "passwordProtectedOptionDescription": { - "message": "Uzstādīt paroli, lai šifrētu izdošanu un tad to ievietotu jebkurā Bitwarden kontā, izmantojot atšifrēšanas paroli." + "message": "Uzstādīt paroli, lai šifrētu izguvi un tad to ievietotu jebkurā Bitwarden kontā, izmantojot atšifrēšanas paroli." }, "exportTypeHeading": { - "message": "Izdošanas veids" + "message": "Izgūšanas veids" }, "accountRestricted": { "message": "Konts ir ierobežots" @@ -5681,10 +5684,10 @@ "message": "Sesijai iestājās noildze. Lūgums mēģināt pieteikties vēlreiz." }, "exportingPersonalVaultTitle": { - "message": "Izdod personīgo glabātavu" + "message": "Izgūst personīgo glabātavu" }, "exportingOrganizationVaultTitle": { - "message": "Izdod apvienības glabātavu" + "message": "Izgūst apvienības glabātavu" }, "exportingIndividualVaultDescription": { "message": "Tiks izgūti tikai atsevišķi glabātavas vienumi, kas ir saistīti ar $EMAIL$. Apvienības glabātavas vienumi netiks iekļauti. Tiks izgūta tikai glabātavas vienumu informācija, un saistītie pielikumi netiks iekļauti.", @@ -5696,7 +5699,7 @@ } }, "exportingOrganizationVaultDesc": { - "message": "Tiks izdota tikai apvienības glabātava, kas ir saistīta ar $ORGANIZATION$. Atsevišķu glabātavu vai citu apvienību vienumi netiks iekļauti.", + "message": "Tiks izgūta tikai apvienības glabātava, kas ir saistīta ar $ORGANIZATION$. Atsevišķu glabātavu vai citu apvienību vienumi netiks iekļauti.", "placeholders": { "organization": { "content": "$1", diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 36067eec14f..ca944a6bc2f 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "ലോഗ് ഔട്ട് ചെയ്തിരിക്കുന്നു" }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 59167b94d9d..99c15045cc9 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 59167b94d9d..99c15045cc9 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 13a5eaf0f44..15d8a6aeef7 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logget av" }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index e0c227c3578..c67c7570a11 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 2c6e1803227..70a171bf756 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Toegangsniveau" }, + "accessing": { + "message": "Toegang verkrijgen" + }, "loggedOut": { "message": "Uitgelogd" }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 903acc72e10..b9231eac28a 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 59167b94d9d..99c15045cc9 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 621a81147ed..1cd176861cf 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Poziom dostępu" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Wylogowano" }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 9b6f32309d7..b7a248ff691 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Nível de acesso" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Sessão encerrada" }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 341c12de976..30d8dad0ca6 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Nível de acesso" }, + "accessing": { + "message": "A aceder" + }, "loggedOut": { "message": "Sessão terminada" }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 4995ca9f2b0..50b7329f760 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Deconectat" }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 6e433fb2a8e..c331541cfd9 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Уровень доступа" }, + "accessing": { + "message": "Доступ" + }, "loggedOut": { "message": "Вы вышли из хранилища" }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index a1410974d79..2e9b80899c1 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 85a8d39fd91..73f52e77637 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Úroveň prístupu" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Odhlásený" }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index d9364aa29c5..56f6a14c394 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Odjavljen" }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 0c23abb5ccd..be4ceddfe97 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Ниво приступа" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Одјављено" }, diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index d3fef41494b..12dd9c91e8c 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Odjavljeni ste" }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 64a3007043e..16d838f3ccf 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Åtkomstnivå" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Utloggad" }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 59167b94d9d..99c15045cc9 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Logged out" }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index b7b7d7887f5..ed1fb050472 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Access level" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "ออกจากระบบ" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index b40091486e5..4c7862da277 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Erişim seviyesi" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Çıkış yapıldı" }, diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 0b1843a84c3..a937da85ee8 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Рівень доступу" }, + "accessing": { + "message": "Доступ" + }, "loggedOut": { "message": "Ви вийшли" }, diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 5a7c513faed..55a34e2f2ab 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "Cấp độ truy cập" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "Đã đăng xuất" }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 7babb5dadef..652a1f3d671 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "访问权限等级" }, + "accessing": { + "message": "访问中" + }, "loggedOut": { "message": "已注销" }, @@ -2102,7 +2105,7 @@ "message": "Bitwarden 家庭版计划。" }, "addons": { - "message": "附加项目" + "message": "插件" }, "premiumAccess": { "message": "高级会员" diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 61cad90e3ae..b4abbc7f3e2 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -581,6 +581,9 @@ "accessLevel": { "message": "存取等級" }, + "accessing": { + "message": "Accessing" + }, "loggedOut": { "message": "已登出" }, diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 815a8aff9e3..f22d98f081d 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -68,8 +68,7 @@ const moduleRules = [ { loader: "babel-loader", options: { - configFile: false, - plugins: ["@angular/compiler-cli/linker/babel"], + configFile: "../../babel.config.json", }, }, ], diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 00000000000..4d817f0abf4 --- /dev/null +++ b/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "bugfixes": true + } + ] + ], + "plugins": ["@angular/compiler-cli/linker/babel"] +} diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts index a3a6c4943f8..3214a0fc41e 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve-all.command.ts @@ -1,9 +1,52 @@ +import { firstValueFrom } from "rxjs"; + +import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; +import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; +import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; export class ApproveAllCommand { - constructor() {} + constructor( + private organizationAuthRequestService: OrganizationAuthRequestService, + private organizationService: OrganizationService, + ) {} async run(organizationId: string): Promise { - throw new Error("Not implemented"); + if (organizationId != null) { + organizationId = organizationId.toLowerCase(); + } + + if (!Utils.isGuid(organizationId)) { + return Response.badRequest("`" + organizationId + "` is not a GUID."); + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (!organization?.canManageUsersPassword) { + return Response.error( + "You do not have permission to approve pending device authorization requests.", + ); + } + + try { + const pendingApprovals = + await this.organizationAuthRequestService.listPendingRequests(organizationId); + if (pendingApprovals.length == 0) { + const res = new MessageResponse( + "No pending device authorization requests to approve.", + null, + ); + return Response.success(res); + } + + await this.organizationAuthRequestService.approvePendingRequests( + organizationId, + pendingApprovals, + ); + + return Response.success(); + } catch (e) { + return Response.error(e); + } } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts index b3a30165ce3..8efa172296c 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/approve.command.ts @@ -1,9 +1,54 @@ +import { firstValueFrom } from "rxjs"; + import { Response } from "@bitwarden/cli/models/response"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { OrganizationAuthRequestService } from "../../../../bit-common/src/admin-console/auth-requests"; export class ApproveCommand { - constructor() {} + constructor( + private organizationService: OrganizationService, + private organizationAuthRequestService: OrganizationAuthRequestService, + ) {} - async run(id: string): Promise { - throw new Error("Not implemented"); + async run(organizationId: string, id: string): Promise { + if (organizationId != null) { + organizationId = organizationId.toLowerCase(); + } + + if (!Utils.isGuid(organizationId)) { + return Response.badRequest("`" + organizationId + "` is not a GUID."); + } + + if (id != null) { + id = id.toLowerCase(); + } + + if (!Utils.isGuid(id)) { + return Response.badRequest("`" + id + "` is not a GUID."); + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (!organization?.canManageUsersPassword) { + return Response.error( + "You do not have permission to approve pending device authorization requests.", + ); + } + + try { + const pendingRequests = + await this.organizationAuthRequestService.listPendingRequests(organizationId); + + const request = pendingRequests.find((r) => r.id == id); + if (request == null) { + return Response.error("Invalid request id"); + } + + await this.organizationAuthRequestService.approvePendingRequest(organizationId, request); + return Response.success(); + } catch (e) { + return Response.error(e); + } } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts index 521a7e8ded6..59cc4235ebf 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny-all.command.ts @@ -1,9 +1,49 @@ +import { firstValueFrom } from "rxjs"; + import { Response } from "@bitwarden/cli/models/response"; +import { MessageResponse } from "@bitwarden/cli/models/response/message.response"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { OrganizationAuthRequestService } from "../../../../bit-common/src/admin-console/auth-requests"; export class DenyAllCommand { - constructor() {} + constructor( + private organizationService: OrganizationService, + private organizationAuthRequestService: OrganizationAuthRequestService, + ) {} async run(organizationId: string): Promise { - throw new Error("Not implemented"); + if (organizationId != null) { + organizationId = organizationId.toLowerCase(); + } + + if (!Utils.isGuid(organizationId)) { + return Response.badRequest("`" + organizationId + "` is not a GUID."); + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (!organization?.canManageUsersPassword) { + return Response.error( + "You do not have permission to approve pending device authorization requests.", + ); + } + + try { + const pendingRequests = + await this.organizationAuthRequestService.listPendingRequests(organizationId); + if (pendingRequests.length == 0) { + const res = new MessageResponse("No pending device authorization requests to deny.", null); + return Response.success(res); + } + + await this.organizationAuthRequestService.denyPendingRequests( + organizationId, + ...pendingRequests.map((r) => r.id), + ); + return Response.success(); + } catch (e) { + return Response.error(e); + } } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts index a366bfb05a0..a9676d3fc54 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/deny.command.ts @@ -1,9 +1,46 @@ +import { firstValueFrom } from "rxjs"; + import { Response } from "@bitwarden/cli/models/response"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { OrganizationAuthRequestService } from "../../../../bit-common/src/admin-console/auth-requests"; export class DenyCommand { - constructor() {} + constructor( + private organizationService: OrganizationService, + private organizationAuthRequestService: OrganizationAuthRequestService, + ) {} - async run(id: string): Promise { - throw new Error("Not implemented"); + async run(organizationId: string, id: string): Promise { + if (organizationId != null) { + organizationId = organizationId.toLowerCase(); + } + + if (!Utils.isGuid(organizationId)) { + return Response.badRequest("`" + organizationId + "` is not a GUID."); + } + + if (id != null) { + id = id.toLowerCase(); + } + + if (!Utils.isGuid(id)) { + return Response.badRequest("`" + id + "` is not a GUID."); + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (!organization?.canManageUsersPassword) { + return Response.error( + "You do not have permission to approve pending device authorization requests.", + ); + } + + try { + await this.organizationAuthRequestService.denyPendingRequests(organizationId, id); + return Response.success(); + } catch (e) { + return Response.error(e); + } } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts index 152dd48c7b7..0b0f3bb0f91 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/device-approval.program.ts @@ -3,6 +3,8 @@ import { program, Command } from "commander"; import { BaseProgram } from "@bitwarden/cli/base-program"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ServiceContainer } from "../../service-container"; + import { ApproveAllCommand } from "./approve-all.command"; import { ApproveCommand } from "./approve.command"; import { DenyAllCommand } from "./deny-all.command"; @@ -10,6 +12,10 @@ import { DenyCommand } from "./deny.command"; import { ListCommand } from "./list.command"; export class DeviceApprovalProgram extends BaseProgram { + constructor(protected serviceContainer: ServiceContainer) { + super(serviceContainer); + } + register() { program.addCommand(this.deviceApprovalCommand()); } @@ -32,7 +38,10 @@ export class DeviceApprovalProgram extends BaseProgram { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ListCommand(); + const cmd = new ListCommand( + this.serviceContainer.organizationAuthRequestService, + this.serviceContainer.organizationService, + ); const response = await cmd.run(organizationId); this.processResponse(response); }); @@ -40,27 +49,34 @@ export class DeviceApprovalProgram extends BaseProgram { private approveCommand(): Command { return new Command("approve") - .argument("") + .argument("", "The id of the organization") + .argument("", "The id of the request to approve") .description("Approve a pending request") - .action(async (id: string) => { + .action(async (organizationId: string, id: string) => { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ApproveCommand(); - const response = await cmd.run(id); + const cmd = new ApproveCommand( + this.serviceContainer.organizationService, + this.serviceContainer.organizationAuthRequestService, + ); + const response = await cmd.run(organizationId, id); this.processResponse(response); }); } private approveAllCommand(): Command { - return new Command("approveAll") + return new Command("approve-all") .description("Approve all pending requests for an organization") .argument("") .action(async (organizationId: string) => { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new ApproveAllCommand(); + const cmd = new ApproveAllCommand( + this.serviceContainer.organizationAuthRequestService, + this.serviceContainer.organizationService, + ); const response = await cmd.run(organizationId); this.processResponse(response); }); @@ -68,27 +84,34 @@ export class DeviceApprovalProgram extends BaseProgram { private denyCommand(): Command { return new Command("deny") - .argument("") + .argument("", "The id of the organization") + .argument("", "The id of the request to deny") .description("Deny a pending request") - .action(async (id: string) => { + .action(async (organizationId: string, id: string) => { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new DenyCommand(); - const response = await cmd.run(id); + const cmd = new DenyCommand( + this.serviceContainer.organizationService, + this.serviceContainer.organizationAuthRequestService, + ); + const response = await cmd.run(organizationId, id); this.processResponse(response); }); } private denyAllCommand(): Command { - return new Command("denyAll") + return new Command("deny-all") .description("Deny all pending requests for an organization") .argument("") .action(async (organizationId: string) => { await this.exitIfFeatureFlagDisabled(FeatureFlag.BulkDeviceApproval); await this.exitIfLocked(); - const cmd = new DenyAllCommand(); + const cmd = new DenyAllCommand( + this.serviceContainer.organizationService, + this.serviceContainer.organizationAuthRequestService, + ); const response = await cmd.run(organizationId); this.processResponse(response); }); diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts index 11fb6ec3ee2..10da11b35cb 100644 --- a/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/list.command.ts @@ -1,9 +1,42 @@ +import { firstValueFrom } from "rxjs"; + +import { OrganizationAuthRequestService } from "@bitwarden/bit-common/admin-console/auth-requests"; import { Response } from "@bitwarden/cli/models/response"; +import { ListResponse } from "@bitwarden/cli/models/response/list.response"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; + +import { PendingAuthRequestResponse } from "./pending-auth-request.response"; export class ListCommand { - constructor() {} + constructor( + private organizationAuthRequestService: OrganizationAuthRequestService, + private organizationService: OrganizationService, + ) {} async run(organizationId: string): Promise { - throw new Error("Not implemented"); + if (organizationId != null) { + organizationId = organizationId.toLowerCase(); + } + + if (!Utils.isGuid(organizationId)) { + return Response.badRequest("`" + organizationId + "` is not a GUID."); + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (!organization?.canManageUsersPassword) { + return Response.error( + "You do not have permission to approve pending device authorization requests.", + ); + } + + try { + const requests = + await this.organizationAuthRequestService.listPendingRequests(organizationId); + const res = new ListResponse(requests.map((r) => new PendingAuthRequestResponse(r))); + return Response.success(res); + } catch (e) { + return Response.error(e); + } } } diff --git a/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts b/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts new file mode 100644 index 00000000000..991b3fb8e58 --- /dev/null +++ b/bitwarden_license/bit-cli/src/admin-console/device-approval/pending-auth-request.response.ts @@ -0,0 +1,26 @@ +import { PendingAuthRequestView } from "@bitwarden/bit-common/admin-console/auth-requests/"; +import { BaseResponse } from "@bitwarden/cli/models/response/base.response"; + +export class PendingAuthRequestResponse implements BaseResponse { + object = "auth-request"; + + id: string; + userId: string; + organizationUserId: string; + email: string; + requestDeviceIdentifier: string; + requestDeviceType: string; + requestIpAddress: string; + creationDate: Date; + + constructor(authRequest: PendingAuthRequestView) { + this.id = authRequest.id; + this.userId = authRequest.userId; + this.organizationUserId = authRequest.organizationUserId; + this.email = authRequest.email; + this.requestDeviceIdentifier = authRequest.requestDeviceIdentifier; + this.requestDeviceType = authRequest.requestDeviceType; + this.requestIpAddress = authRequest.requestIpAddress; + this.creationDate = authRequest.creationDate; + } +} diff --git a/bitwarden_license/bit-cli/src/service-container.ts b/bitwarden_license/bit-cli/src/service-container.ts index 369d54113d6..995e14531d7 100644 --- a/bitwarden_license/bit-cli/src/service-container.ts +++ b/bitwarden_license/bit-cli/src/service-container.ts @@ -1,7 +1,24 @@ +import { + OrganizationAuthRequestService, + OrganizationAuthRequestApiService, +} from "@bitwarden/bit-common/admin-console/auth-requests"; import { ServiceContainer as OssServiceContainer } from "@bitwarden/cli/service-container"; /** * Instantiates services and makes them available for dependency injection. * Any Bitwarden-licensed services should be registered here. */ -export class ServiceContainer extends OssServiceContainer {} +export class ServiceContainer extends OssServiceContainer { + organizationAuthRequestApiService: OrganizationAuthRequestApiService; + organizationAuthRequestService: OrganizationAuthRequestService; + + constructor() { + super(); + this.organizationAuthRequestApiService = new OrganizationAuthRequestApiService(this.apiService); + this.organizationAuthRequestService = new OrganizationAuthRequestService( + this.organizationAuthRequestApiService, + this.cryptoService, + this.organizationUserService, + ); + } +} diff --git a/bitwarden_license/bit-cli/tsconfig.json b/bitwarden_license/bit-cli/tsconfig.json index 1989aa08f9b..e8a57e5eb04 100644 --- a/bitwarden_license/bit-cli/tsconfig.json +++ b/bitwarden_license/bit-cli/tsconfig.json @@ -21,7 +21,8 @@ "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], - "@bitwarden/node/*": ["../../libs/node/src/*"] + "@bitwarden/node/*": ["../../libs/node/src/*"], + "@bitwarden/bit-common/*": ["../../bitwarden_license/bit-common/src/*"] } }, "include": ["src", "src/**/*.spec.ts"] diff --git a/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts b/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts index d8c4bacd697..517dc8699b5 100644 --- a/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts +++ b/bitwarden_license/bit-common/src/admin-console/auth-requests/index.ts @@ -1,2 +1,4 @@ export * from "./pending-organization-auth-request.response"; export * from "./organization-auth-request.service"; +export * from "./organization-auth-request-api.service"; +export * from "./pending-auth-request.view"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index d16b0a8aa2a..ffcfcd0ad81 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -33,6 +33,7 @@ *ngIf="canAccessBilling$ | async" > + + + + {{ "loading" | i18n }} + + + + +

    + {{ "accountCredit" | i18n }} +

    +

    {{ accountCredit | currency: "$" }}

    +

    {{ "creditAppliedDesc" | i18n }}

    + +
    + + +

    {{ "paymentMethod" | i18n }}

    +

    {{ "noPaymentMethod" | i18n }}

    + + +

    + + {{ paymentMethodDescription }} +

    +
    + +
    + + +

    {{ "taxInformation" | i18n }}

    +

    {{ "taxInformationDesc" | i18n }}

    + +
    +
    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.ts new file mode 100644 index 00000000000..42a7dbdec05 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-payment-method.component.ts @@ -0,0 +1,140 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { from, lastValueFrom, Subject, switchMap } from "rxjs"; +import { takeUntil } from "rxjs/operators"; + +import { openAddAccountCreditDialog } from "@bitwarden/angular/billing/components"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { MaskedPaymentMethod, TaxInformation } from "@bitwarden/common/billing/models/domain"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { + openProviderSelectPaymentMethodDialog, + ProviderSelectPaymentMethodDialogResultType, +} from "./provider-select-payment-method-dialog.component"; + +@Component({ + selector: "app-provider-payment-method", + templateUrl: "./provider-payment-method.component.html", +}) +export class ProviderPaymentMethodComponent implements OnInit, OnDestroy { + protected providerId: string; + protected loading: boolean; + + protected accountCredit: number; + protected maskedPaymentMethod: MaskedPaymentMethod; + protected taxInformation: TaxInformation; + + private destroy$ = new Subject(); + + constructor( + private activatedRoute: ActivatedRoute, + private billingApiService: BillingApiServiceAbstraction, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + ) {} + + addAccountCredit = () => + openAddAccountCreditDialog(this.dialogService, { + data: { + providerId: this.providerId, + }, + }); + + changePaymentMethod = async () => { + const dialogRef = openProviderSelectPaymentMethodDialog(this.dialogService, { + data: { + providerId: this.providerId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result == ProviderSelectPaymentMethodDialogResultType.Submitted) { + await this.load(); + } + }; + + async load() { + this.loading = true; + const paymentInformation = await this.billingApiService.getProviderPaymentInformation( + this.providerId, + ); + this.accountCredit = paymentInformation.accountCredit; + this.maskedPaymentMethod = MaskedPaymentMethod.from(paymentInformation.paymentMethod); + this.taxInformation = TaxInformation.from(paymentInformation.taxInformation); + this.loading = false; + } + + onDataUpdated = async () => await this.load(); + + updateTaxInformation = async (taxInformation: TaxInformation) => { + const request = ExpandedTaxInfoUpdateRequest.From(taxInformation); + await this.billingApiService.updateProviderTaxInformation(this.providerId, request); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedTaxInformation"), + }); + }; + + verifyBankAccount = async (amount1: number, amount2: number) => { + const request = new VerifyBankAccountRequest(amount1, amount2); + await this.billingApiService.verifyProviderBankAccount(this.providerId, request); + }; + + ngOnInit() { + this.activatedRoute.params + .pipe( + switchMap(({ providerId }) => { + this.providerId = providerId; + return from(this.load()); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + protected get hasPaymentMethod(): boolean { + return !!this.maskedPaymentMethod; + } + + protected get hasUnverifiedPaymentMethod(): boolean { + return !!this.maskedPaymentMethod && this.maskedPaymentMethod.needsVerification; + } + + protected get paymentMethodClass(): string[] { + switch (this.maskedPaymentMethod.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal tw-text-primary"]; + default: + return []; + } + } + + protected get paymentMethodDescription(): string { + let description = this.maskedPaymentMethod.description; + if (this.maskedPaymentMethod.type === PaymentMethodType.BankAccount) { + if (this.hasUnverifiedPaymentMethod) { + description += " - " + this.i18nService.t("unverified"); + } else { + description += " - " + this.i18nService.t("verified"); + } + } + return description; + } +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.html new file mode 100644 index 00000000000..03e8405a48c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.html @@ -0,0 +1,18 @@ +
    + + + {{ "addPaymentMethod" | i18n }} + + + + + + + + + +
    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.ts new file mode 100644 index 00000000000..09a293d12d8 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/payment-method/provider-select-payment-method-dialog.component.ts @@ -0,0 +1,60 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, EventEmitter, Inject, Output, ViewChild } from "@angular/core"; +import { FormGroup } from "@angular/forms"; + +import { SelectPaymentMethodComponent } from "@bitwarden/angular/billing/components"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { TokenizedPaymentMethodRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-method.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +type ProviderSelectPaymentMethodDialogParams = { + providerId: string; +}; + +export enum ProviderSelectPaymentMethodDialogResultType { + Closed = "closed", + Submitted = "submitted", +} + +export const openProviderSelectPaymentMethodDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig, +) => + dialogService.open< + ProviderSelectPaymentMethodDialogResultType, + ProviderSelectPaymentMethodDialogParams + >(ProviderSelectPaymentMethodDialogComponent, dialogConfig); + +@Component({ + templateUrl: "provider-select-payment-method-dialog.component.html", +}) +export class ProviderSelectPaymentMethodDialogComponent { + @ViewChild(SelectPaymentMethodComponent) + selectPaymentMethodComponent: SelectPaymentMethodComponent; + @Output() providerPaymentMethodUpdated = new EventEmitter(); + + protected readonly formGroup = new FormGroup({}); + protected readonly ResultType = ProviderSelectPaymentMethodDialogResultType; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + @Inject(DIALOG_DATA) private dialogParams: ProviderSelectPaymentMethodDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + private toastService: ToastService, + ) {} + + submit = async () => { + const tokenizedPaymentMethod = await this.selectPaymentMethodComponent.tokenizePaymentMethod(); + const request = TokenizedPaymentMethodRequest.From(tokenizedPaymentMethod); + await this.billingApiService.updateProviderPaymentMethod(this.dialogParams.providerId, request); + this.providerPaymentMethodUpdated.emit(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedPaymentMethod"), + }); + this.dialogRef.close(this.ResultType.Submitted); + }; +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.html rename to bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.html diff --git a/bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts similarity index 100% rename from bitwarden_license/bit-web/src/app/billing/providers/provider-subscription.component.ts rename to bitwarden_license/bit-web/src/app/billing/providers/subscription/provider-subscription.component.ts diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index 95c17642538..56c02e1ed43 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -17,6 +17,7 @@ import { import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; @@ -94,6 +95,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private smOnboardingTasksService: SMOnboardingTasksService, + private logService: LogService, ) {} ngOnInit() { @@ -297,12 +299,13 @@ export class OverviewComponent implements OnInit, OnDestroy { SecretsListComponent.copySecretName(name, this.platformUtilsService, this.i18nService); } - copySecretValue(id: string) { - SecretsListComponent.copySecretValue( + async copySecretValue(id: string) { + await SecretsListComponent.copySecretValue( id, this.platformUtilsService, this.i18nService, this.secretService, + this.logService, ); } @@ -310,11 +313,9 @@ export class OverviewComponent implements OnInit, OnDestroy { SecretsListComponent.copySecretUuid(id, this.platformUtilsService, this.i18nService); } - protected hideOnboarding() { + protected async hideOnboarding() { this.showOnboarding = false; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.saveCompletedTasks(this.organizationId, { + await this.saveCompletedTasks(this.organizationId, { importSecrets: true, createSecret: true, createProject: true, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts index 0b65bd0a26b..d30d5f664e2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/dialog/project-dialog.component.ts @@ -82,9 +82,7 @@ export class ProjectDialogComponent implements OnInit { const projectView = this.getProjectView(); if (this.data.operation === OperationType.Add) { const newProject = await this.createProject(projectView); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["sm", this.data.organizationId, "projects", newProject.id]); + await this.router.navigate(["sm", this.data.organizationId, "projects", newProject.id]); } else { projectView.id = this.data.projectId; await this.updateProject(projectView); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts new file mode 100644 index 00000000000..84bc1483fd1 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts @@ -0,0 +1,120 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { RouterService } from "../../../../../../../apps/web/src/app/core/router.service"; +import { ProjectView } from "../../models/view/project.view"; +import { ProjectService } from "../project.service"; + +import { projectAccessGuard } from "./project-access.guard"; + +@Component({ + template: "", +}) +export class GuardedRouteTestComponent {} + +@Component({ + template: "", +}) +export class RedirectTestComponent {} + +describe("Project Redirect Guard", () => { + let organizationService: MockProxy; + let routerService: MockProxy; + let projectServiceMock: MockProxy; + let i18nServiceMock: MockProxy; + let platformUtilsService: MockProxy; + let router: Router; + + const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; + const projectView = { + id: "123", + organizationId: "123", + name: "project-name", + creationDate: Date.now.toString(), + revisionDate: Date.now.toString(), + read: true, + write: true, + } as ProjectView; + + beforeEach(async () => { + organizationService = mock(); + routerService = mock(); + projectServiceMock = mock(); + i18nServiceMock = mock(); + platformUtilsService = mock(); + + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { + path: "sm/:organizationId/projects/:projectId", + component: GuardedRouteTestComponent, + canActivate: [projectAccessGuard], + }, + { + path: "sm", + component: RedirectTestComponent, + }, + { + path: "sm/:organizationId/projects", + component: RedirectTestComponent, + }, + ]), + ], + providers: [ + { provide: OrganizationService, useValue: organizationService }, + { provide: RouterService, useValue: routerService }, + { provide: ProjectService, useValue: projectServiceMock }, + { provide: I18nService, useValue: i18nServiceMock }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, + ], + }); + + router = TestBed.inject(Router); + }); + + it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => { + // Arrange + organizationService.getAll.mockResolvedValue([smOrg1]); + projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView)); + + // Act + await router.navigateByUrl("sm/123/projects/123"); + + // Assert + expect(router.url).toBe("/sm/123/projects/123"); + }); + + it("redirects to sm/projects if project does not exist", async () => { + // Arrange + organizationService.getAll.mockResolvedValue([smOrg1]); + + // Act + await router.navigateByUrl("sm/123/projects/124"); + + // Assert + expect(router.url).toBe("/sm/123/projects"); + }); + + it("redirects to sm/123/projects if exception occurs while looking for Project", async () => { + // Arrange + jest.spyOn(projectServiceMock, "getByProjectId").mockImplementation(() => { + throw new Error("Test error"); + }); + jest.spyOn(i18nServiceMock, "t").mockReturnValue("Project not found"); + + // Act + await router.navigateByUrl("sm/123/projects/123"); + // Assert + expect(platformUtilsService.showToast).toHaveBeenCalledWith("error", null, "Project not found"); + expect(router.url).toBe("/sm/123/projects"); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts new file mode 100644 index 00000000000..6c08fcc3aa7 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts @@ -0,0 +1,31 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { ProjectService } from "../project.service"; + +/** + * Redirects to projects list if the user doesn't have access to project. + */ +export const projectAccessGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { + const projectService = inject(ProjectService); + const platformUtilsService = inject(PlatformUtilsService); + const i18nService = inject(I18nService); + + try { + const project = await projectService.getByProjectId(route.params.projectId); + if (project) { + return true; + } + } catch { + platformUtilsService.showToast( + "error", + null, + i18nService.t("notFound", i18nService.t("project")), + ); + return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]); + } + return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]); +}; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts index 835d3825a05..c49008c580b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, Subject, switchMap, takeUntil, catchError } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { DialogService } from "@bitwarden/components"; @@ -38,8 +39,9 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { }), ), catchError(async () => { + this.logService.info("Error fetching project people access policies."); await this.router.navigate(["/sm", this.organizationId, "projects"]); - return []; + return undefined; }), ); @@ -70,6 +72,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private accessPolicySelectorService: AccessPolicySelectorService, + private logService: LogService, ) {} ngOnInit(): void { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index 07d50b28ee1..21d6e576a01 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -4,6 +4,7 @@ import { combineLatest, combineLatestWith, filter, Observable, startWith, switch import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; @@ -42,6 +43,7 @@ export class ProjectSecretsComponent { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private logService: LogService, ) {} ngOnInit() { @@ -109,12 +111,13 @@ export class ProjectSecretsComponent { SecretsListComponent.copySecretName(name, this.platformUtilsService, this.i18nService); } - copySecretValue(id: string) { - SecretsListComponent.copySecretValue( + async copySecretValue(id: string) { + await SecretsListComponent.copySecretValue( id, this.platformUtilsService, this.i18nService, this.secretService, + this.logService, ); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts index 742c2bea1d8..07ca32600a9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.ts @@ -1,9 +1,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { - catchError, combineLatest, - EMPTY, filter, Observable, startWith, @@ -58,18 +56,6 @@ export class ProjectComponent implements OnInit, OnDestroy { this.project$ = combineLatest([this.route.params, currentProjectEdited]).pipe( switchMap(([params, _]) => this.projectService.getByProjectId(params.projectId)), - catchError(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/sm", this.organizationId, "projects"]).then(() => { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("notFound", this.i18nService.t("project")), - ); - }); - return EMPTY; - }), ); const projectId$ = this.route.params.pipe(map((p) => p.projectId)); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts index 6078520989a..231486703c9 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts @@ -1,6 +1,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { projectAccessGuard } from "./guards/project-access.guard"; import { ProjectPeopleComponent } from "./project/project-people.component"; import { ProjectSecretsComponent } from "./project/project-secrets.component"; import { ProjectServiceAccountsComponent } from "./project/project-service-accounts.component"; @@ -15,6 +16,7 @@ const routes: Routes = [ { path: ":projectId", component: ProjectComponent, + canActivate: [projectAccessGuard], children: [ { path: "", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts index b1bd91a04fb..0287cdd4251 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-dialog.component.ts @@ -199,7 +199,7 @@ export class SecretDialogComponent implements OnInit { return await this.projectService.create(this.data.organizationId, projectView); } - protected openDeleteSecretDialog() { + protected async openDeleteSecretDialog() { const secretListView: SecretListView[] = this.getSecretListView(); const dialogRef = this.dialogService.open( @@ -212,9 +212,7 @@ export class SecretDialogComponent implements OnInit { ); // If the secret is deleted, chain close this dialog after the delete dialog - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - lastValueFrom(dialogRef.closed).then( + await lastValueFrom(dialogRef.closed).then( (closeData) => closeData !== undefined && this.dialogRef.close(), ); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index a7413c9b59f..2717f96a686 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -4,6 +4,7 @@ import { combineLatestWith, Observable, startWith, switchMap } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; @@ -39,6 +40,7 @@ export class SecretsComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private organizationService: OrganizationService, + private logService: LogService, ) {} ngOnInit() { @@ -97,12 +99,13 @@ export class SecretsComponent implements OnInit { SecretsListComponent.copySecretName(name, this.platformUtilsService, this.i18nService); } - copySecretValue(id: string) { - SecretsListComponent.copySecretValue( + async copySecretValue(id: string) { + await SecretsListComponent.copySecretValue( id, this.platformUtilsService, this.i18nService, this.secretService, + this.logService, ); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts index 105ca59e57f..de753d88138 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts @@ -47,9 +47,7 @@ export class ServiceAccountDialogComponent { async ngOnInit() { if (this.data.operation == OperationType.Edit) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.loadData(); + await this.loadData(); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts new file mode 100644 index 00000000000..956935ac6ac --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts @@ -0,0 +1,122 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { RouterService } from "../../../../../../../../clients/apps/web/src/app/core/router.service"; +import { ServiceAccountView } from "../../models/view/service-account.view"; +import { ServiceAccountService } from "../service-account.service"; + +import { serviceAccountAccessGuard } from "./service-account-access.guard"; + +@Component({ + template: "", +}) +export class GuardedRouteTestComponent {} + +@Component({ + template: "", +}) +export class RedirectTestComponent {} + +describe("Service account Redirect Guard", () => { + let organizationService: MockProxy; + let routerService: MockProxy; + let serviceAccountServiceMock: MockProxy; + let i18nServiceMock: MockProxy; + let platformUtilsService: MockProxy; + let router: Router; + + const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; + const serviceAccountView = { + id: "123", + organizationId: "123", + name: "service-account-name", + } as ServiceAccountView; + + beforeEach(async () => { + organizationService = mock(); + routerService = mock(); + serviceAccountServiceMock = mock(); + i18nServiceMock = mock(); + platformUtilsService = mock(); + + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { + path: "sm/:organizationId/machine-accounts/:serviceAccountId", + component: GuardedRouteTestComponent, + canActivate: [serviceAccountAccessGuard], + }, + { + path: "sm", + component: RedirectTestComponent, + }, + { + path: "sm/:organizationId/machine-accounts", + component: RedirectTestComponent, + }, + ]), + ], + providers: [ + { provide: OrganizationService, useValue: organizationService }, + { provide: RouterService, useValue: routerService }, + { provide: ServiceAccountService, useValue: serviceAccountServiceMock }, + { provide: I18nService, useValue: i18nServiceMock }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, + ], + }); + + router = TestBed.inject(Router); + }); + + it("redirects to sm/{orgId}/machine-accounts/{serviceAccountId} if machine account exists", async () => { + // Arrange + organizationService.getAll.mockResolvedValue([smOrg1]); + serviceAccountServiceMock.getByServiceAccountId.mockReturnValue( + Promise.resolve(serviceAccountView), + ); + + // Act + await router.navigateByUrl("sm/123/machine-accounts/123"); + + // Assert + expect(router.url).toBe("/sm/123/machine-accounts/123"); + }); + + it("redirects to sm/machine-accounts if machine account does not exist", async () => { + // Arrange + organizationService.getAll.mockResolvedValue([smOrg1]); + + // Act + await router.navigateByUrl("sm/123/machine-accounts/124"); + + // Assert + expect(router.url).toBe("/sm/123/machine-accounts"); + }); + + it("redirects to sm/123/machine-accounts if exception occurs while looking for service account", async () => { + // Arrange + jest.spyOn(serviceAccountServiceMock, "getByServiceAccountId").mockImplementation(() => { + throw new Error("Test error"); + }); + jest.spyOn(i18nServiceMock, "t").mockReturnValue("Service account not found"); + + // Act + await router.navigateByUrl("sm/123/machine-accounts/123"); + // Assert + expect(platformUtilsService.showToast).toHaveBeenCalledWith( + "error", + null, + "Service account not found", + ); + expect(router.url).toBe("/sm/123/machine-accounts"); + }); +}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts index c474ec44d55..b72fc5a1fe2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.ts @@ -1,6 +1,9 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + import { ServiceAccountService } from "../service-account.service"; /** @@ -8,6 +11,8 @@ import { ServiceAccountService } from "../service-account.service"; */ export const serviceAccountAccessGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { const serviceAccountService = inject(ServiceAccountService); + const platformUtilsService = inject(PlatformUtilsService); + const i18nService = inject(I18nService); try { const serviceAccount = await serviceAccountService.getByServiceAccountId( @@ -18,6 +23,12 @@ export const serviceAccountAccessGuard: CanActivateFn = async (route: ActivatedR return true; } } catch { + platformUtilsService.showToast( + "error", + null, + i18nService.t("notFound", i18nService.t("machineAccount")), + ); + return createUrlTreeFromSnapshot(route, [ "/sm", route.params.organizationId, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts index bb687c51c62..51b663acce6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts @@ -1,15 +1,6 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { - EMPTY, - Subject, - catchError, - combineLatest, - filter, - startWith, - switchMap, - takeUntil, -} from "rxjs"; +import { Subject, combineLatest, filter, startWith, switchMap, takeUntil } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -42,18 +33,6 @@ export class ServiceAccountComponent implements OnInit, OnDestroy { params.organizationId, ), ), - catchError(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/sm", this.organizationId, "machine-accounts"]).then(() => { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("notFound", this.i18nService.t("machineAccount")), - ); - }); - return EMPTY; - }), ); constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html index e926ba6a13d..454b497fcdb 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html @@ -78,9 +78,11 @@ -
    - {{ emptyMessage }} -
    + + + {{ emptyMessage }} + +
    diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html index f8d5d1081e0..4b629ca4885 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html @@ -93,8 +93,9 @@ variant="secondary" class="tw-ml-1" [title]="project.name" + maxWidthClass="tw-max-w-60" > - {{ project.name | ellipsis: 32 }} + {{ project.name }} secret.value); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - SecretsListComponent.copyToClipboardAsync(value, platformUtilsService).then(() => { + try { + const value = await secretService.getBySecretId(id).then((secret) => secret.value); + platformUtilsService.copyToClipboard(value); platformUtilsService.showToast( "success", null, i18nService.t("valueCopied", i18nService.t("value")), ); - }); + } catch { + logService.info("Error fetching secret value."); + } } static copySecretUuid( diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html new file mode 100644 index 00000000000..c9c0c296ada --- /dev/null +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.html @@ -0,0 +1,55 @@ +
    + + +

    {{ "creditDelayed" | i18n }}

    +
    + + + {{ "payPal" | i18n }} + + + {{ "bitcoin" | i18n }} + + +
    +
    + + {{ "amount" | i18n }} + + $USD + +
    +
    + + + + +
    +
    +
    + + + + + + + + + + + + + + + +
    diff --git a/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts new file mode 100644 index 00000000000..d3c262c4b7d --- /dev/null +++ b/libs/angular/src/billing/components/add-account-credit-dialog/add-account-credit-dialog.component.ts @@ -0,0 +1,153 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { DialogService } from "@bitwarden/components"; + +export type AddAccountCreditDialogParams = { + organizationId?: string; + providerId?: string; +}; + +export enum AddAccountCreditDialogResultType { + Closed = "closed", + Submitted = "submitted", +} + +export const openAddAccountCreditDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig, +) => + dialogService.open( + AddAccountCreditDialogComponent, + dialogConfig, + ); + +type PayPalConfig = { + businessId?: string; + buttonAction?: string; + returnUrl?: string; + customField?: string; + subject?: string; +}; + +@Component({ + templateUrl: "./add-account-credit-dialog.component.html", +}) +export class AddAccountCreditDialogComponent implements OnInit { + @ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef; + protected formGroup = new FormGroup({ + paymentMethod: new FormControl(PaymentMethodType.PayPal), + creditAmount: new FormControl(null, [Validators.required, Validators.min(0.01)]), + }); + protected payPalConfig: PayPalConfig; + protected ResultType = AddAccountCreditDialogResultType; + + private organization?: Organization; + private provider?: Provider; + private user?: { id: UserId } & AccountInfo; + + constructor( + private accountService: AccountService, + private apiService: ApiService, + private configService: ConfigService, + @Inject(DIALOG_DATA) private dialogParams: AddAccountCreditDialogParams, + private dialogRef: DialogRef, + private organizationService: OrganizationService, + private platformUtilsService: PlatformUtilsService, + private providerService: ProviderService, + ) { + this.payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; + } + + protected readonly paymentMethodType = PaymentMethodType; + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + if (this.formGroup.value.paymentMethod === PaymentMethodType.PayPal) { + this.payPalForm.nativeElement.submit(); + return; + } + + if (this.formGroup.value.paymentMethod === PaymentMethodType.BitPay) { + const request = this.getBitPayInvoiceRequest(); + const bitPayUrl = await this.apiService.postBitPayInvoice(request); + this.platformUtilsService.launchUri(bitPayUrl); + return; + } + + this.dialogRef.close(AddAccountCreditDialogResultType.Submitted); + }; + + async ngOnInit(): Promise { + let payPalCustomField: string; + + if (this.dialogParams.organizationId) { + this.formGroup.patchValue({ + creditAmount: 20.0, + }); + this.organization = await this.organizationService.get(this.dialogParams.organizationId); + payPalCustomField = "organization_id:" + this.organization.id; + this.payPalConfig.subject = this.organization.name; + } else if (this.dialogParams.providerId) { + this.formGroup.patchValue({ + creditAmount: 20.0, + }); + this.provider = await this.providerService.get(this.dialogParams.providerId); + payPalCustomField = "provider_id:" + this.provider.id; + this.payPalConfig.subject = this.provider.name; + } else { + this.formGroup.patchValue({ + creditAmount: 10.0, + }); + this.user = await firstValueFrom(this.accountService.activeAccount$); + payPalCustomField = "user_id:" + this.user.id; + this.payPalConfig.subject = this.user.email; + } + + const region = await firstValueFrom(this.configService.cloudRegion$); + + payPalCustomField += ",account_credit:1"; + payPalCustomField += `,region:${region}`; + + this.payPalConfig.customField = payPalCustomField; + this.payPalConfig.returnUrl = window.location.href; + } + + getBitPayInvoiceRequest(): BitPayInvoiceRequest { + const request = new BitPayInvoiceRequest(); + if (this.organization) { + request.name = this.organization.name; + request.organizationId = this.organization.id; + } else if (this.provider) { + request.name = this.provider.name; + request.providerId = this.provider.id; + } else { + request.email = this.user.email; + request.userId = this.user.id; + } + + request.credit = true; + request.amount = this.formGroup.value.creditAmount; + request.returnUrl = window.location.href; + + return request; + } +} diff --git a/libs/angular/src/billing/components/index.ts b/libs/angular/src/billing/components/index.ts new file mode 100644 index 00000000000..748a005df83 --- /dev/null +++ b/libs/angular/src/billing/components/index.ts @@ -0,0 +1,4 @@ +export * from "./add-account-credit-dialog/add-account-credit-dialog.component"; +export * from "./manage-tax-information/manage-tax-information.component"; +export * from "./select-payment-method/select-payment-method.component"; +export * from "./verify-bank-account/verify-bank-account.component"; diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html new file mode 100644 index 00000000000..f9cfa8e0faf --- /dev/null +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html @@ -0,0 +1,72 @@ +
    +
    +
    + + {{ "country" | i18n }} + + + + +
    +
    + + {{ "zipPostalCode" | i18n }} + + +
    +
    + + + {{ "includeVAT" | i18n }} + +
    +
    +
    +
    + + {{ "taxIdNumber" | i18n }} + + +
    +
    +
    +
    + + {{ "address1" | i18n }} + + +
    +
    + + {{ "address2" | i18n }} + + +
    +
    + + {{ "cityTown" | i18n }} + + +
    +
    + + {{ "stateProvince" | i18n }} + + +
    +
    + +
    diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts new file mode 100644 index 00000000000..58342548ca3 --- /dev/null +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -0,0 +1,406 @@ +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { TaxInformation } from "@bitwarden/common/billing/models/domain"; + +type Country = { + name: string; + value: string; + disabled: boolean; +}; + +@Component({ + selector: "app-manage-tax-information", + templateUrl: "./manage-tax-information.component.html", +}) +export class ManageTaxInformationComponent implements OnInit { + @Input({ required: true }) taxInformation: TaxInformation; + @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; + @Output() taxInformationUpdated = new EventEmitter(); + + protected formGroup = this.formBuilder.group({ + country: ["", Validators.required], + postalCode: ["", Validators.required], + includeTaxId: false, + taxId: "", + line1: "", + line2: "", + city: "", + state: "", + }); + + constructor(private formBuilder: FormBuilder) {} + + submit = async () => { + await this.onSubmit({ + country: this.formGroup.value.country, + postalCode: this.formGroup.value.postalCode, + taxId: this.formGroup.value.taxId, + line1: this.formGroup.value.line1, + line2: this.formGroup.value.line2, + city: this.formGroup.value.city, + state: this.formGroup.value.state, + }); + + this.taxInformationUpdated.emit(); + }; + + async ngOnInit() { + if (this.taxInformation) { + this.formGroup.patchValue({ + ...this.taxInformation, + includeTaxId: + this.countrySupportsTax(this.taxInformation.country) && + (!!this.taxInformation.taxId || + !!this.taxInformation.line1 || + !!this.taxInformation.line2 || + !!this.taxInformation.city || + !!this.taxInformation.state), + }); + } + } + + protected countrySupportsTax(countryCode: string) { + return this.taxSupportedCountryCodes.includes(countryCode); + } + + protected get includeTaxIdIsSelected() { + return this.formGroup.value.includeTaxId; + } + + protected get selectionSupportsAdditionalOptions() { + return ( + this.formGroup.value.country !== "US" && this.countrySupportsTax(this.formGroup.value.country) + ); + } + + protected countries: Country[] = [ + { name: "-- Select --", value: "", disabled: false }, + { name: "United States", value: "US", disabled: false }, + { name: "China", value: "CN", disabled: false }, + { name: "France", value: "FR", disabled: false }, + { name: "Germany", value: "DE", disabled: false }, + { name: "Canada", value: "CA", disabled: false }, + { name: "United Kingdom", value: "GB", disabled: false }, + { name: "Australia", value: "AU", disabled: false }, + { name: "India", value: "IN", disabled: false }, + { name: "", value: "-", disabled: true }, + { name: "Afghanistan", value: "AF", disabled: false }, + { name: "Åland Islands", value: "AX", disabled: false }, + { name: "Albania", value: "AL", disabled: false }, + { name: "Algeria", value: "DZ", disabled: false }, + { name: "American Samoa", value: "AS", disabled: false }, + { name: "Andorra", value: "AD", disabled: false }, + { name: "Angola", value: "AO", disabled: false }, + { name: "Anguilla", value: "AI", disabled: false }, + { name: "Antarctica", value: "AQ", disabled: false }, + { name: "Antigua and Barbuda", value: "AG", disabled: false }, + { name: "Argentina", value: "AR", disabled: false }, + { name: "Armenia", value: "AM", disabled: false }, + { name: "Aruba", value: "AW", disabled: false }, + { name: "Austria", value: "AT", disabled: false }, + { name: "Azerbaijan", value: "AZ", disabled: false }, + { name: "Bahamas", value: "BS", disabled: false }, + { name: "Bahrain", value: "BH", disabled: false }, + { name: "Bangladesh", value: "BD", disabled: false }, + { name: "Barbados", value: "BB", disabled: false }, + { name: "Belarus", value: "BY", disabled: false }, + { name: "Belgium", value: "BE", disabled: false }, + { name: "Belize", value: "BZ", disabled: false }, + { name: "Benin", value: "BJ", disabled: false }, + { name: "Bermuda", value: "BM", disabled: false }, + { name: "Bhutan", value: "BT", disabled: false }, + { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, + { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, + { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, + { name: "Botswana", value: "BW", disabled: false }, + { name: "Bouvet Island", value: "BV", disabled: false }, + { name: "Brazil", value: "BR", disabled: false }, + { name: "British Indian Ocean Territory", value: "IO", disabled: false }, + { name: "Brunei Darussalam", value: "BN", disabled: false }, + { name: "Bulgaria", value: "BG", disabled: false }, + { name: "Burkina Faso", value: "BF", disabled: false }, + { name: "Burundi", value: "BI", disabled: false }, + { name: "Cambodia", value: "KH", disabled: false }, + { name: "Cameroon", value: "CM", disabled: false }, + { name: "Cape Verde", value: "CV", disabled: false }, + { name: "Cayman Islands", value: "KY", disabled: false }, + { name: "Central African Republic", value: "CF", disabled: false }, + { name: "Chad", value: "TD", disabled: false }, + { name: "Chile", value: "CL", disabled: false }, + { name: "Christmas Island", value: "CX", disabled: false }, + { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, + { name: "Colombia", value: "CO", disabled: false }, + { name: "Comoros", value: "KM", disabled: false }, + { name: "Congo", value: "CG", disabled: false }, + { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, + { name: "Cook Islands", value: "CK", disabled: false }, + { name: "Costa Rica", value: "CR", disabled: false }, + { name: "Côte d'Ivoire", value: "CI", disabled: false }, + { name: "Croatia", value: "HR", disabled: false }, + { name: "Cuba", value: "CU", disabled: false }, + { name: "Curaçao", value: "CW", disabled: false }, + { name: "Cyprus", value: "CY", disabled: false }, + { name: "Czech Republic", value: "CZ", disabled: false }, + { name: "Denmark", value: "DK", disabled: false }, + { name: "Djibouti", value: "DJ", disabled: false }, + { name: "Dominica", value: "DM", disabled: false }, + { name: "Dominican Republic", value: "DO", disabled: false }, + { name: "Ecuador", value: "EC", disabled: false }, + { name: "Egypt", value: "EG", disabled: false }, + { name: "El Salvador", value: "SV", disabled: false }, + { name: "Equatorial Guinea", value: "GQ", disabled: false }, + { name: "Eritrea", value: "ER", disabled: false }, + { name: "Estonia", value: "EE", disabled: false }, + { name: "Ethiopia", value: "ET", disabled: false }, + { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, + { name: "Faroe Islands", value: "FO", disabled: false }, + { name: "Fiji", value: "FJ", disabled: false }, + { name: "Finland", value: "FI", disabled: false }, + { name: "French Guiana", value: "GF", disabled: false }, + { name: "French Polynesia", value: "PF", disabled: false }, + { name: "French Southern Territories", value: "TF", disabled: false }, + { name: "Gabon", value: "GA", disabled: false }, + { name: "Gambia", value: "GM", disabled: false }, + { name: "Georgia", value: "GE", disabled: false }, + { name: "Ghana", value: "GH", disabled: false }, + { name: "Gibraltar", value: "GI", disabled: false }, + { name: "Greece", value: "GR", disabled: false }, + { name: "Greenland", value: "GL", disabled: false }, + { name: "Grenada", value: "GD", disabled: false }, + { name: "Guadeloupe", value: "GP", disabled: false }, + { name: "Guam", value: "GU", disabled: false }, + { name: "Guatemala", value: "GT", disabled: false }, + { name: "Guernsey", value: "GG", disabled: false }, + { name: "Guinea", value: "GN", disabled: false }, + { name: "Guinea-Bissau", value: "GW", disabled: false }, + { name: "Guyana", value: "GY", disabled: false }, + { name: "Haiti", value: "HT", disabled: false }, + { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, + { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, + { name: "Honduras", value: "HN", disabled: false }, + { name: "Hong Kong", value: "HK", disabled: false }, + { name: "Hungary", value: "HU", disabled: false }, + { name: "Iceland", value: "IS", disabled: false }, + { name: "Indonesia", value: "ID", disabled: false }, + { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, + { name: "Iraq", value: "IQ", disabled: false }, + { name: "Ireland", value: "IE", disabled: false }, + { name: "Isle of Man", value: "IM", disabled: false }, + { name: "Israel", value: "IL", disabled: false }, + { name: "Italy", value: "IT", disabled: false }, + { name: "Jamaica", value: "JM", disabled: false }, + { name: "Japan", value: "JP", disabled: false }, + { name: "Jersey", value: "JE", disabled: false }, + { name: "Jordan", value: "JO", disabled: false }, + { name: "Kazakhstan", value: "KZ", disabled: false }, + { name: "Kenya", value: "KE", disabled: false }, + { name: "Kiribati", value: "KI", disabled: false }, + { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, + { name: "Korea, Republic of", value: "KR", disabled: false }, + { name: "Kuwait", value: "KW", disabled: false }, + { name: "Kyrgyzstan", value: "KG", disabled: false }, + { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, + { name: "Latvia", value: "LV", disabled: false }, + { name: "Lebanon", value: "LB", disabled: false }, + { name: "Lesotho", value: "LS", disabled: false }, + { name: "Liberia", value: "LR", disabled: false }, + { name: "Libya", value: "LY", disabled: false }, + { name: "Liechtenstein", value: "LI", disabled: false }, + { name: "Lithuania", value: "LT", disabled: false }, + { name: "Luxembourg", value: "LU", disabled: false }, + { name: "Macao", value: "MO", disabled: false }, + { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, + { name: "Madagascar", value: "MG", disabled: false }, + { name: "Malawi", value: "MW", disabled: false }, + { name: "Malaysia", value: "MY", disabled: false }, + { name: "Maldives", value: "MV", disabled: false }, + { name: "Mali", value: "ML", disabled: false }, + { name: "Malta", value: "MT", disabled: false }, + { name: "Marshall Islands", value: "MH", disabled: false }, + { name: "Martinique", value: "MQ", disabled: false }, + { name: "Mauritania", value: "MR", disabled: false }, + { name: "Mauritius", value: "MU", disabled: false }, + { name: "Mayotte", value: "YT", disabled: false }, + { name: "Mexico", value: "MX", disabled: false }, + { name: "Micronesia, Federated States of", value: "FM", disabled: false }, + { name: "Moldova, Republic of", value: "MD", disabled: false }, + { name: "Monaco", value: "MC", disabled: false }, + { name: "Mongolia", value: "MN", disabled: false }, + { name: "Montenegro", value: "ME", disabled: false }, + { name: "Montserrat", value: "MS", disabled: false }, + { name: "Morocco", value: "MA", disabled: false }, + { name: "Mozambique", value: "MZ", disabled: false }, + { name: "Myanmar", value: "MM", disabled: false }, + { name: "Namibia", value: "NA", disabled: false }, + { name: "Nauru", value: "NR", disabled: false }, + { name: "Nepal", value: "NP", disabled: false }, + { name: "Netherlands", value: "NL", disabled: false }, + { name: "New Caledonia", value: "NC", disabled: false }, + { name: "New Zealand", value: "NZ", disabled: false }, + { name: "Nicaragua", value: "NI", disabled: false }, + { name: "Niger", value: "NE", disabled: false }, + { name: "Nigeria", value: "NG", disabled: false }, + { name: "Niue", value: "NU", disabled: false }, + { name: "Norfolk Island", value: "NF", disabled: false }, + { name: "Northern Mariana Islands", value: "MP", disabled: false }, + { name: "Norway", value: "NO", disabled: false }, + { name: "Oman", value: "OM", disabled: false }, + { name: "Pakistan", value: "PK", disabled: false }, + { name: "Palau", value: "PW", disabled: false }, + { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, + { name: "Panama", value: "PA", disabled: false }, + { name: "Papua New Guinea", value: "PG", disabled: false }, + { name: "Paraguay", value: "PY", disabled: false }, + { name: "Peru", value: "PE", disabled: false }, + { name: "Philippines", value: "PH", disabled: false }, + { name: "Pitcairn", value: "PN", disabled: false }, + { name: "Poland", value: "PL", disabled: false }, + { name: "Portugal", value: "PT", disabled: false }, + { name: "Puerto Rico", value: "PR", disabled: false }, + { name: "Qatar", value: "QA", disabled: false }, + { name: "Réunion", value: "RE", disabled: false }, + { name: "Romania", value: "RO", disabled: false }, + { name: "Russian Federation", value: "RU", disabled: false }, + { name: "Rwanda", value: "RW", disabled: false }, + { name: "Saint Barthélemy", value: "BL", disabled: false }, + { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, + { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, + { name: "Saint Lucia", value: "LC", disabled: false }, + { name: "Saint Martin (French part)", value: "MF", disabled: false }, + { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, + { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, + { name: "Samoa", value: "WS", disabled: false }, + { name: "San Marino", value: "SM", disabled: false }, + { name: "Sao Tome and Principe", value: "ST", disabled: false }, + { name: "Saudi Arabia", value: "SA", disabled: false }, + { name: "Senegal", value: "SN", disabled: false }, + { name: "Serbia", value: "RS", disabled: false }, + { name: "Seychelles", value: "SC", disabled: false }, + { name: "Sierra Leone", value: "SL", disabled: false }, + { name: "Singapore", value: "SG", disabled: false }, + { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, + { name: "Slovakia", value: "SK", disabled: false }, + { name: "Slovenia", value: "SI", disabled: false }, + { name: "Solomon Islands", value: "SB", disabled: false }, + { name: "Somalia", value: "SO", disabled: false }, + { name: "South Africa", value: "ZA", disabled: false }, + { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, + { name: "South Sudan", value: "SS", disabled: false }, + { name: "Spain", value: "ES", disabled: false }, + { name: "Sri Lanka", value: "LK", disabled: false }, + { name: "Sudan", value: "SD", disabled: false }, + { name: "Suriname", value: "SR", disabled: false }, + { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, + { name: "Swaziland", value: "SZ", disabled: false }, + { name: "Sweden", value: "SE", disabled: false }, + { name: "Switzerland", value: "CH", disabled: false }, + { name: "Syrian Arab Republic", value: "SY", disabled: false }, + { name: "Taiwan", value: "TW", disabled: false }, + { name: "Tajikistan", value: "TJ", disabled: false }, + { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, + { name: "Thailand", value: "TH", disabled: false }, + { name: "Timor-Leste", value: "TL", disabled: false }, + { name: "Togo", value: "TG", disabled: false }, + { name: "Tokelau", value: "TK", disabled: false }, + { name: "Tonga", value: "TO", disabled: false }, + { name: "Trinidad and Tobago", value: "TT", disabled: false }, + { name: "Tunisia", value: "TN", disabled: false }, + { name: "Turkey", value: "TR", disabled: false }, + { name: "Turkmenistan", value: "TM", disabled: false }, + { name: "Turks and Caicos Islands", value: "TC", disabled: false }, + { name: "Tuvalu", value: "TV", disabled: false }, + { name: "Uganda", value: "UG", disabled: false }, + { name: "Ukraine", value: "UA", disabled: false }, + { name: "United Arab Emirates", value: "AE", disabled: false }, + { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, + { name: "Uruguay", value: "UY", disabled: false }, + { name: "Uzbekistan", value: "UZ", disabled: false }, + { name: "Vanuatu", value: "VU", disabled: false }, + { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, + { name: "Viet Nam", value: "VN", disabled: false }, + { name: "Virgin Islands, British", value: "VG", disabled: false }, + { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, + { name: "Wallis and Futuna", value: "WF", disabled: false }, + { name: "Western Sahara", value: "EH", disabled: false }, + { name: "Yemen", value: "YE", disabled: false }, + { name: "Zambia", value: "ZM", disabled: false }, + { name: "Zimbabwe", value: "ZW", disabled: false }, + ]; + + private taxSupportedCountryCodes: string[] = [ + "CN", + "FR", + "DE", + "CA", + "GB", + "AU", + "IN", + "AD", + "AR", + "AT", + "BE", + "BO", + "BR", + "BG", + "CL", + "CO", + "CR", + "HR", + "CY", + "CZ", + "DK", + "DO", + "EC", + "EG", + "SV", + "EE", + "FI", + "GE", + "GR", + "HK", + "HU", + "IS", + "ID", + "IQ", + "IE", + "IL", + "IT", + "JP", + "KE", + "KR", + "LV", + "LI", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NO", + "PE", + "PH", + "PL", + "PT", + "RO", + "RU", + "SA", + "RS", + "SG", + "SK", + "SI", + "ZA", + "ES", + "SE", + "CH", + "TW", + "TH", + "TR", + "UA", + "AE", + "UY", + "VE", + "VN", + ]; +} diff --git a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html new file mode 100644 index 00000000000..7add3f6d35d --- /dev/null +++ b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.html @@ -0,0 +1,151 @@ +
    +
    + + + + + {{ "creditCard" | i18n }} + + + + + + {{ "bankAccount" | i18n }} + + + + + + {{ "payPal" | i18n }} + + + + + + {{ "accountCredit" | i18n }} + + + +
    + + +
    +
    + +
    +
    +
    + Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay +
    +
    + +
    +
    +
    +
    + + + + +
    +
    +
    +
    +
    + + + + {{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }} + +
    + + {{ "routingNumber" | i18n }} + + + + {{ "accountNumber" | i18n }} + + + + {{ "accountHolderName" | i18n }} + + + + {{ "bankAccountType" | i18n }} + + + + + + +
    +
    + + +
    +
    + {{ "paypalClickSubmit" | i18n }} +
    +
    + + + + {{ "makeSureEnoughCredit" | i18n }} + + + +
    diff --git a/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.ts b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.ts new file mode 100644 index 00000000000..4dc39334a70 --- /dev/null +++ b/libs/angular/src/billing/components/select-payment-method/select-payment-method.component.ts @@ -0,0 +1,159 @@ +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; + +import { + BillingApiServiceAbstraction, + BraintreeServiceAbstraction, + StripeServiceAbstraction, +} from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { TokenizedPaymentMethod } from "@bitwarden/common/billing/models/domain"; + +@Component({ + selector: "app-select-payment-method", + templateUrl: "./select-payment-method.component.html", +}) +export class SelectPaymentMethodComponent implements OnInit, OnDestroy { + @Input() protected showAccountCredit: boolean = true; + @Input() protected showBankAccount: boolean = true; + @Input() protected showPayPal: boolean = true; + @Input() private startWith: PaymentMethodType = PaymentMethodType.Card; + @Input() protected onSubmit: (tokenizedPaymentMethod: TokenizedPaymentMethod) => Promise; + + private destroy$ = new Subject(); + + protected formGroup = this.formBuilder.group({ + paymentMethod: [this.startWith], + bankInformation: this.formBuilder.group({ + routingNumber: ["", [Validators.required]], + accountNumber: ["", [Validators.required]], + accountHolderName: ["", [Validators.required]], + accountHolderType: ["", [Validators.required]], + }), + }); + protected PaymentMethodType = PaymentMethodType; + + constructor( + private billingApiService: BillingApiServiceAbstraction, + private braintreeService: BraintreeServiceAbstraction, + private formBuilder: FormBuilder, + private stripeService: StripeServiceAbstraction, + ) {} + + async tokenizePaymentMethod(): Promise { + const type = this.selected; + + if (this.usingStripe) { + const clientSecret = await this.billingApiService.createSetupIntent(type); + + if (this.usingBankAccount) { + const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { + accountHolderName: this.formGroup.value.bankInformation.accountHolderName, + routingNumber: this.formGroup.value.bankInformation.routingNumber, + accountNumber: this.formGroup.value.bankInformation.accountNumber, + accountHolderType: this.formGroup.value.bankInformation.accountHolderType, + }); + return { + type, + token, + }; + } + + if (this.usingCard) { + const token = await this.stripeService.setupCardPaymentMethod(clientSecret); + return { + type, + token, + }; + } + } + + if (this.usingPayPal) { + const token = await this.braintreeService.requestPaymentMethod(); + return { + type, + token, + }; + } + + return null; + } + + submit = async () => { + const tokenizedPaymentMethod = await this.tokenizePaymentMethod(); + await this.onSubmit(tokenizedPaymentMethod); + }; + + ngOnInit(): void { + this.stripeService.loadStripe( + { + cardNumber: "#stripe-card-number", + cardExpiry: "#stripe-card-expiry", + cardCvc: "#stripe-card-cvc", + }, + this.startWith === PaymentMethodType.Card, + ); + + if (this.showPayPal) { + this.braintreeService.loadBraintree( + "#braintree-container", + this.startWith === PaymentMethodType.PayPal, + ); + } + + this.formGroup + .get("paymentMethod") + .valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe((type) => { + this.onPaymentMethodChange(type); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.stripeService.unloadStripe(); + if (this.showPayPal) { + this.braintreeService.unloadBraintree(); + } + } + + private onPaymentMethodChange(type: PaymentMethodType): void { + switch (type) { + case PaymentMethodType.Card: { + this.stripeService.mountElements(); + break; + } + case PaymentMethodType.PayPal: { + this.braintreeService.createDropin(); + break; + } + } + } + + private get selected(): PaymentMethodType { + return this.formGroup.value.paymentMethod; + } + + protected get usingAccountCredit(): boolean { + return this.selected === PaymentMethodType.Credit; + } + + protected get usingBankAccount(): boolean { + return this.selected === PaymentMethodType.BankAccount; + } + + protected get usingCard(): boolean { + return this.selected === PaymentMethodType.Card; + } + + protected get usingPayPal(): boolean { + return this.selected === PaymentMethodType.PayPal; + } + + private get usingStripe(): boolean { + return this.usingBankAccount || this.usingCard; + } +} diff --git a/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.html b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.html new file mode 100644 index 00000000000..f338f5b0817 --- /dev/null +++ b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.html @@ -0,0 +1,18 @@ + +

    {{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}

    +
    + + {{ "amountX" | i18n: "1" }} + + $0. + + + {{ "amountX" | i18n: "2" }} + + $0. + + +
    +
    diff --git a/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.ts b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.ts new file mode 100644 index 00000000000..c8abb65d819 --- /dev/null +++ b/libs/angular/src/billing/components/verify-bank-account/verify-bank-account.component.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; + +@Component({ + selector: "app-verify-bank-account", + templateUrl: "./verify-bank-account.component.html", +}) +export class VerifyBankAccountComponent { + @Input() onSubmit?: (amount1: number, amount2: number) => Promise; + @Output() verificationSubmitted = new EventEmitter(); + + protected formGroup = this.formBuilder.group({ + amount1: new FormControl(null, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + amount2: new FormControl(null, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + }); + + constructor(private formBuilder: FormBuilder) {} + + submit = async () => { + if (this.onSubmit) { + await this.onSubmit(this.formGroup.value.amount1, this.formGroup.value.amount2); + } + this.verificationSubmitted.emit(); + }; +} diff --git a/libs/angular/src/billing/images/cards.png b/libs/angular/src/billing/images/cards.png new file mode 100644 index 00000000000..bd43abe54c5 Binary files /dev/null and b/libs/angular/src/billing/images/cards.png differ diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 5f1bf796aa9..ccb7446d863 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,7 +2,24 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { AutofocusDirective, ToastModule } from "@bitwarden/components"; +import { + AddAccountCreditDialogComponent, + ManageTaxInformationComponent, + SelectPaymentMethodComponent, + VerifyBankAccountComponent, +} from "@bitwarden/angular/billing/components"; +import { + AsyncActionsModule, + AutofocusDirective, + ButtonModule, + CheckboxModule, + DialogModule, + FormFieldModule, + RadioButtonModule, + SelectModule, + ToastModule, + TypographyModule, +} from "@bitwarden/components"; import { CalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; @@ -41,6 +58,14 @@ import { IconComponent } from "./vault/components/icon.component"; CommonModule, FormsModule, ReactiveFormsModule, + AsyncActionsModule, + RadioButtonModule, + FormFieldModule, + SelectModule, + ButtonModule, + CheckboxModule, + DialogModule, + TypographyModule, ], declarations: [ A11yInvalidDirective, @@ -70,6 +95,10 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, + AddAccountCreditDialogComponent, + ManageTaxInformationComponent, + SelectPaymentMethodComponent, + VerifyBankAccountComponent, ], exports: [ A11yInvalidDirective, @@ -100,6 +129,10 @@ import { IconComponent } from "./vault/components/icon.component"; UserTypePipe, IfFeatureDirective, FingerprintPipe, + AddAccountCreditDialogComponent, + ManageTaxInformationComponent, + SelectPaymentMethodComponent, + VerifyBankAccountComponent, ], providers: [ CreditCardNumberPipe, diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 17a98498d68..40405b062c6 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,6 +1,7 @@ import { InjectionToken } from "@angular/core"; import { Observable, Subject } from "rxjs"; +import { LogoutReason } from "@bitwarden/auth/common"; import { ClientType } from "@bitwarden/common/enums"; import { AbstractStorageService, @@ -36,7 +37,7 @@ export const MEMORY_STORAGE = new SafeInjectionToken("ME export const SECURE_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); export const STATE_FACTORY = new SafeInjectionToken("STATE_FACTORY"); export const LOGOUT_CALLBACK = new SafeInjectionToken< - (expired: boolean, userId?: string) => Promise + (logoutReason: LogoutReason, userId?: string) => Promise >("LOGOUT_CALLBACK"); export const LOCKED_CALLBACK = new SafeInjectionToken<(userId?: string) => Promise>( "LOCKED_CALLBACK", @@ -53,3 +54,7 @@ export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken< Subject>> >("INTRAPROCESS_MESSAGING_SUBJECT"); export const CLIENT_TYPE = new SafeInjectionToken("CLIENT_TYPE"); + +export const REFRESH_ACCESS_TOKEN_ERROR_CALLBACK = new SafeInjectionToken<() => void>( + "REFRESH_ACCESS_TOKEN_ERROR_CALLBACK", +); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 60f83934af7..048c1829001 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -13,6 +13,7 @@ import { InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, + LogoutReason, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -109,14 +110,20 @@ import { DomainSettingsService, DefaultDomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + BillingApiServiceAbstraction, + BraintreeServiceAbstraction, + OrganizationBillingServiceAbstraction, + PaymentMethodWarningsServiceAbstraction, + StripeServiceAbstraction, +} from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; -import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service"; -import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service"; +import { BraintreeService } from "@bitwarden/common/billing/services/payment-processors/braintree.service"; +import { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; @@ -232,6 +239,7 @@ import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync- import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; +import { ToastService } from "@bitwarden/components"; import { ImportApiService, ImportApiServiceAbstraction, @@ -275,6 +283,7 @@ import { DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, + REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -316,8 +325,12 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: LOGOUT_CALLBACK, useFactory: - (messagingService: MessagingServiceAbstraction) => (expired: boolean, userId?: string) => - Promise.resolve(messagingService.send("logout", { expired: expired, userId: userId })), + (messagingService: MessagingServiceAbstraction) => + async (logoutReason: LogoutReason, userId?: string) => { + return Promise.resolve( + messagingService.send("logout", { logoutReason: logoutReason, userId: userId }), + ); + }, deps: [MessagingServiceAbstraction], }), safeProvider({ @@ -526,6 +539,7 @@ const safeProviders: SafeProvider[] = [ KeyGenerationServiceAbstraction, EncryptService, LogService, + LOGOUT_CALLBACK, ], }), safeProvider({ @@ -579,6 +593,17 @@ const safeProviders: SafeProvider[] = [ StateProvider, ], }), + safeProvider({ + provide: REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, + useFactory: (toastService: ToastService, i18nService: I18nServiceAbstraction) => () => { + toastService.showToast({ + variant: "error", + title: i18nService.t("errorRefreshingAccessToken"), + message: i18nService.t("errorRefreshingAccessTokenDesc"), + }); + }, + deps: [ToastService, I18nServiceAbstraction], + }), safeProvider({ provide: ApiServiceAbstraction, useClass: ApiService, @@ -587,8 +612,10 @@ const safeProviders: SafeProvider[] = [ PlatformUtilsServiceAbstraction, EnvironmentService, AppIdServiceAbstraction, - VaultTimeoutSettingsServiceAbstraction, + REFRESH_ACCESS_TOKEN_ERROR_CALLBACK, + LogService, LOGOUT_CALLBACK, + VaultTimeoutSettingsServiceAbstraction, ], }), safeProvider({ @@ -1190,6 +1217,16 @@ const safeProviders: SafeProvider[] = [ useClass: KdfConfigService, deps: [StateProvider], }), + safeProvider({ + provide: BraintreeServiceAbstraction, + useClass: BraintreeService, + deps: [LogService], + }), + safeProvider({ + provide: StripeServiceAbstraction, + useClass: StripeService, + deps: [LogService], + }), ]; function encryptServiceFactory( diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 4b593d336e1..bf5edbda823 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -15,7 +15,7 @@
diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html index b4dad835eec..9785bf05ab5 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.html @@ -8,6 +8,7 @@ [label]="regionConfig.domain" > diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts index f01873dd3e2..fe41f0a3ac7 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts @@ -1,17 +1,26 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; -import { EMPTY, Subject, from, map, of, switchMap, takeUntil, tap } from "rxjs"; +import { Subject, from, map, of, pairwise, startWith, switchMap, takeUntil, tap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ClientType } from "@bitwarden/common/enums"; import { Environment, EnvironmentService, Region, RegionConfig, } from "@bitwarden/common/platform/abstractions/environment.service"; -import { FormFieldModule, SelectModule } from "@bitwarden/components"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService, FormFieldModule, SelectModule, ToastService } from "@bitwarden/components"; +import { RegistrationSelfHostedEnvConfigDialogComponent } from "./registration-self-hosted-env-config-dialog.component"; + +/** + * Component for selecting the environment to register with in the email verification registration flow. + * Outputs the selected region to the parent component so it can respond as necessary. + */ @Component({ standalone: true, selector: "auth-registration-env-selector", @@ -19,7 +28,7 @@ import { FormFieldModule, SelectModule } from "@bitwarden/components"; imports: [CommonModule, JslibModule, ReactiveFormsModule, FormFieldModule, SelectModule], }) export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy { - @Output() onOpenSelfHostedSettings = new EventEmitter(); + @Output() selectedRegionChange = new EventEmitter(); ServerEnvironmentType = Region; @@ -33,12 +42,24 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy { availableRegionConfigs: RegionConfig[] = this.environmentService.availableRegions(); + private selectedRegionFromEnv: RegionConfig | Region.SelfHosted; + + isDesktopOrBrowserExtension = false; + private destroy$ = new Subject(); constructor( private formBuilder: FormBuilder, private environmentService: EnvironmentService, - ) {} + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private platformUtilsService: PlatformUtilsService, + ) { + const clientType = platformUtilsService.getClientType(); + this.isDesktopOrBrowserExtension = + clientType === ClientType.Desktop || clientType === ClientType.Browser; + } async ngOnInit() { await this.initSelectedRegionAndListenForEnvChanges(); @@ -61,13 +82,17 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy { return regionConfig; }), - tap((selectedRegionInitialValue: RegionConfig | Region.SelfHosted) => { - // This inits the form control with the selected region, but - // it also sets the value to self hosted if the self hosted settings are saved successfully - // in the client specific implementation managed by the parent component. - // It also resets the value to the previously selected region if the self hosted - // settings are closed without saving. We don't emit the event to avoid a loop. - this.selectedRegion.setValue(selectedRegionInitialValue, { emitEvent: false }); + tap((selectedRegionFromEnv: RegionConfig | Region.SelfHosted) => { + // Only set the value if it is different from the current value. + if (selectedRegionFromEnv !== this.selectedRegion.value) { + // Don't emit to avoid triggering the selectedRegion valueChanges subscription + // which could loop back to this code. + this.selectedRegion.setValue(selectedRegionFromEnv, { emitEvent: false }); + } + + // Save this off so we can reset the value to the previously selected region + // if the self hosted settings are closed without saving. + this.selectedRegionFromEnv = selectedRegionFromEnv; }), takeUntil(this.destroy$), ) @@ -77,23 +102,66 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy { private listenForSelectedRegionChanges() { this.selectedRegion.valueChanges .pipe( - switchMap((selectedRegionConfig: RegionConfig | Region.SelfHosted | null) => { - if (selectedRegionConfig === null) { - return of(null); - } + startWith(null), // required so that first user choice is not ignored + pairwise(), + switchMap( + ([prevSelectedRegion, selectedRegion]: [ + RegionConfig | Region.SelfHosted | null, + RegionConfig | Region.SelfHosted | null, + ]) => { + if (selectedRegion === null) { + this.selectedRegionChange.emit(selectedRegion); + return of(null); + } - if (selectedRegionConfig === Region.SelfHosted) { - this.onOpenSelfHostedSettings.emit(); - return EMPTY; - } + if (selectedRegion === Region.SelfHosted) { + return from( + RegistrationSelfHostedEnvConfigDialogComponent.open(this.dialogService), + ).pipe( + tap((result: boolean | undefined) => + this.handleSelfHostedEnvConfigDialogResult(result, prevSelectedRegion), + ), + ); + } - return from(this.environmentService.setEnvironment(selectedRegionConfig.key)); - }), + this.selectedRegionChange.emit(selectedRegion); + return from(this.environmentService.setEnvironment(selectedRegion.key)); + }, + ), takeUntil(this.destroy$), ) .subscribe(); } + private handleSelfHostedEnvConfigDialogResult( + result: boolean | undefined, + prevSelectedRegion: RegionConfig | Region.SelfHosted | null, + ) { + if (result === true) { + this.selectedRegionChange.emit(Region.SelfHosted); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("environmentSaved"), + }); + return; + } + + // Reset the value to the previously selected region or the current env setting + // if the self hosted env settings dialog is closed without saving. + if ( + (result === false || result === undefined) && + prevSelectedRegion !== null && + prevSelectedRegion !== Region.SelfHosted + ) { + this.selectedRegionChange.emit(prevSelectedRegion); + this.selectedRegion.setValue(prevSelectedRegion, { emitEvent: false }); + } else { + this.selectedRegionChange.emit(this.selectedRegionFromEnv); + this.selectedRegion.setValue(this.selectedRegionFromEnv, { emitEvent: false }); + } + } + ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html new file mode 100644 index 00000000000..92c2f9f2f7a --- /dev/null +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html @@ -0,0 +1,107 @@ +
+ + Self-hosted environment + + + {{ "baseUrl" | i18n }} + + {{ "selfHostedBaseUrlHint" | i18n }} + + + + + +

+ {{ "selfHostedCustomEnvHeader" | i18n }} +

+ + + {{ "webVaultUrl" | i18n }} + + + + + {{ "apiUrl" | i18n }} + + + + + {{ "identityUrl" | i18n }} + + + + + {{ "notificationsUrl" | i18n }} + + + + + {{ "iconsUrl" | i18n }} + + +
+ + + {{ "selfHostedEnvFormInvalid" | i18n }} + +
+ + + + + +
+
diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts new file mode 100644 index 00000000000..2bedb4b3583 --- /dev/null +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts @@ -0,0 +1,164 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + ValidatorFn, +} from "@angular/forms"; +import { Subject, firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + DialogService, + FormFieldModule, + LinkModule, + TypographyModule, +} from "@bitwarden/components"; + +/** + * Validator for self-hosted environment settings form. + * It enforces that at least one URL is provided. + */ +function selfHostedEnvSettingsFormValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const formGroup = control as FormGroup; + const baseUrl = formGroup.get("baseUrl")?.value; + const webVaultUrl = formGroup.get("webVaultUrl")?.value; + const apiUrl = formGroup.get("apiUrl")?.value; + const identityUrl = formGroup.get("identityUrl")?.value; + const iconsUrl = formGroup.get("iconsUrl")?.value; + const notificationsUrl = formGroup.get("notificationsUrl")?.value; + + if (baseUrl || webVaultUrl || apiUrl || identityUrl || iconsUrl || notificationsUrl) { + return null; // valid + } else { + return { atLeastOneUrlIsRequired: true }; // invalid + } + }; +} + +/** + * Dialog for configuring self-hosted environment settings. + */ +@Component({ + standalone: true, + selector: "auth-registration-self-hosted-env-config-dialog", + templateUrl: "registration-self-hosted-env-config-dialog.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + ], +}) +export class RegistrationSelfHostedEnvConfigDialogComponent implements OnInit, OnDestroy { + /** + * Opens the dialog. + * @param dialogService - Dialog service. + * @returns Promise that resolves to true if the dialog was closed with a successful result, false otherwise. + */ + static async open(dialogService: DialogService): Promise { + const dialogRef = dialogService.open(RegistrationSelfHostedEnvConfigDialogComponent, { + disableClose: false, + }); + + const dialogResult = await firstValueFrom(dialogRef.closed); + + return dialogResult; + } + + formGroup = this.formBuilder.group( + { + baseUrl: [null], + webVaultUrl: [null], + apiUrl: [null], + identityUrl: [null], + iconsUrl: [null], + notificationsUrl: [null], + }, + { validators: selfHostedEnvSettingsFormValidator() }, + ); + + get baseUrl(): FormControl { + return this.formGroup.get("baseUrl") as FormControl; + } + + get webVaultUrl(): FormControl { + return this.formGroup.get("webVaultUrl") as FormControl; + } + + get apiUrl(): FormControl { + return this.formGroup.get("apiUrl") as FormControl; + } + + get identityUrl(): FormControl { + return this.formGroup.get("identityUrl") as FormControl; + } + + get iconsUrl(): FormControl { + return this.formGroup.get("iconsUrl") as FormControl; + } + + get notificationsUrl(): FormControl { + return this.formGroup.get("notificationsUrl") as FormControl; + } + + showCustomEnv = false; + showErrorSummary = false; + + private destroy$ = new Subject(); + + constructor( + private dialogRef: DialogRef, + private formBuilder: FormBuilder, + private environmentService: EnvironmentService, + ) {} + + ngOnInit() {} + + submit = async () => { + this.showErrorSummary = false; + + if (this.formGroup.invalid) { + this.showErrorSummary = true; + return; + } + + await this.environmentService.setEnvironment(Region.SelfHosted, { + base: this.baseUrl.value, + api: this.apiUrl.value, + identity: this.identityUrl.value, + webVault: this.webVaultUrl.value, + icons: this.iconsUrl.value, + notifications: this.notificationsUrl.value, + }); + + this.dialogRef.close(true); + }; + + async cancel() { + this.dialogRef.close(false); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.component.html b/libs/auth/src/angular/registration/registration-start/registration-start.component.html index 8f64232f9c8..8da2eb76b55 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.component.html +++ b/libs/auth/src/angular/registration/registration-start/registration-start.component.html @@ -1,5 +1,9 @@
+ + {{ "emailAddress" | i18n }} +Note that the self hosted option is not present in the environment selector. -### Self Hosted Example +### US Region - + -### Query Param Example +### EU Region + + + +### Query Params The component accepts two query parameters: `email` and `emailReadonly`. If an email is provided, it will be pre-filled in the email input field. If `emailReadonly` is set to `true`, the email input field will be set to readonly. `emailReadonly` is primarily for the organization invite flow. - + + +## Desktop + +Behavior to note: + +- The self hosted option is present in the environment selector. +- If you go from non-self hosted to self hosted, the terms of service and privacy policy checkbox + will disappear. + +### US Region + + + +### EU Region + + + +### Self Hosted + +Note the fact that the terms of service and privacy policy checkbox is not present when the +environment is self hosted. + + + +## Browser Extension + +Behavior to note: + +- The self hosted option is present in the environment selector. +- If you go from non-self hosted to self hosted, the terms of service and privacy policy checkbox + will disappear. + +### US Region + + + +### EU Region + + + +### Self Hosted + +Note the fact that the terms of service and privacy policy checkbox is not present when the +environment is self hosted. + + diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index 099f086b963..50d1f15182e 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -1,10 +1,30 @@ import { importProvidersFrom } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { ActivatedRoute, Params } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; import { of } from "rxjs"; +import { ClientType } from "@bitwarden/common/enums"; +import { + Environment, + EnvironmentService, + Region, + Urls, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + FormFieldModule, + LinkModule, + SelectModule, + ToastOptions, + ToastService, + TypographyModule, +} from "@bitwarden/components"; import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests"; @@ -15,52 +35,70 @@ export default { component: RegistrationStartComponent, } as Meta; -const decorators = (options: { isSelfHost: boolean; queryParams: Params }) => { +const decorators = (options: { + isSelfHost?: boolean; + queryParams?: Params; + clientType?: ClientType; + defaultRegion?: Region; +}) => { return [ moduleMetadata({ - imports: [RouterTestingModule], + imports: [ + RouterTestingModule, + DialogModule, + ReactiveFormsModule, + FormFieldModule, + SelectModule, + ButtonModule, + LinkModule, + TypographyModule, + AsyncActionsModule, + BrowserAnimationsModule, + ], providers: [ { provide: ActivatedRoute, - useValue: { queryParams: of(options.queryParams) }, - }, - { - provide: PlatformUtilsService, - useValue: { - isSelfHost: () => options.isSelfHost, - } as Partial, + useValue: { queryParams: of(options.queryParams || {}) }, }, ], }), applicationConfig({ - providers: [importProvidersFrom(PreloadedEnglishI18nModule)], + providers: [ + importProvidersFrom(PreloadedEnglishI18nModule), + { + provide: EnvironmentService, + useValue: { + environment$: of({ + getRegion: () => options.defaultRegion || Region.US, + } as Partial), + availableRegions: () => [ + { key: Region.US, domain: "bitwarden.com", urls: {} }, + { key: Region.EU, domain: "bitwarden.eu", urls: {} }, + ], + setEnvironment: (region: Region, urls?: Urls) => Promise.resolve({}), + } as Partial, + }, + { + provide: PlatformUtilsService, + useValue: { + isSelfHost: () => options.isSelfHost || false, + getClientType: () => options.clientType || ClientType.Web, + } as Partial, + }, + { + provide: ToastService, + useValue: { + showToast: (options: ToastOptions) => {}, + } as Partial, + }, + ], }), ]; }; type Story = StoryObj; -export const CloudExample: Story = { - render: (args) => ({ - props: args, - template: ` - - `, - }), - decorators: decorators({ isSelfHost: false, queryParams: {} }), -}; - -export const SelfHostExample: Story = { - render: (args) => ({ - props: args, - template: ` - - `, - }), - decorators: decorators({ isSelfHost: true, queryParams: {} }), -}; - -export const QueryParamsExample: Story = { +export const WebUSRegionExample: Story = { render: (args) => ({ props: args, template: ` @@ -68,7 +106,120 @@ export const QueryParamsExample: Story = { `, }), decorators: decorators({ - isSelfHost: false, + clientType: ClientType.Web, + queryParams: {}, + defaultRegion: Region.US, + }), +}; + +export const WebEURegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Web, + queryParams: {}, + defaultRegion: Region.EU, + }), +}; + +export const WebUSRegionQueryParamsExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Web, + defaultRegion: Region.US, queryParams: { email: "jaredWasHere@bitwarden.com", emailReadonly: "true" }, }), }; + +export const DesktopUSRegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Desktop, + defaultRegion: Region.US, + isSelfHost: false, + }), +}; + +export const DesktopEURegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Desktop, + defaultRegion: Region.EU, + isSelfHost: false, + }), +}; + +export const DesktopSelfHostExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Desktop, + isSelfHost: true, + defaultRegion: Region.SelfHosted, + }), +}; + +export const BrowserExtensionUSRegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Browser, + defaultRegion: Region.US, + isSelfHost: false, + }), +}; + +export const BrowserExtensionEURegionExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Browser, + defaultRegion: Region.EU, + isSelfHost: false, + }), +}; + +export const BrowserExtensionSelfHostExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Browser, + isSelfHost: true, + defaultRegion: Region.SelfHosted, + }), +}; diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts index 8cb40d94524..8c62136d63c 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts @@ -197,8 +197,8 @@ export class UserVerificationFormInputComponent implements ControlValueAccessor, } } - // Don't bother executing secret changes if biometrics verification is active. - if (this.activeClientVerificationOption === ActiveClientVerificationOption.Biometrics) { + // Executing secret changes for all non biometrics verification. Biometrics doesn't have a user entered secret. + if (this.activeClientVerificationOption !== ActiveClientVerificationOption.Biometrics) { this.processSecretChanges(this.secret.value); } diff --git a/libs/auth/src/common/index.ts b/libs/auth/src/common/index.ts index 936666e1a81..43efd7c6387 100644 --- a/libs/auth/src/common/index.ts +++ b/libs/auth/src/common/index.ts @@ -3,5 +3,6 @@ */ export * from "./abstractions"; export * from "./models"; +export * from "./types"; export * from "./services"; export * from "./utilities"; diff --git a/libs/auth/src/common/types/index.ts b/libs/auth/src/common/types/index.ts new file mode 100644 index 00000000000..37ec426fb68 --- /dev/null +++ b/libs/auth/src/common/types/index.ts @@ -0,0 +1 @@ +export * from "./logout-reason.type"; diff --git a/libs/auth/src/common/types/logout-reason.type.ts b/libs/auth/src/common/types/logout-reason.type.ts new file mode 100644 index 00000000000..71fff51064a --- /dev/null +++ b/libs/auth/src/common/types/logout-reason.type.ts @@ -0,0 +1,10 @@ +export type LogoutReason = + | "invalidGrantError" + | "vaultTimeout" + | "invalidSecurityStamp" + | "logoutNotification" + | "keyConnectorError" + | "sessionExpired" + | "accessTokenUnableToBeDecrypted" + | "refreshTokenSecureStorageRetrievalFailure" + | "accountDeleted"; diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index d078051f642..a88dfbb278f 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -70,16 +70,16 @@ export abstract class TokenService { /** * Gets the access token * @param userId - The optional user id to get the access token for; if not provided, the active user is used. - * @returns A promise that resolves with the access token or undefined. + * @returns A promise that resolves with the access token or null. */ - getAccessToken: (userId?: UserId) => Promise; + getAccessToken: (userId?: UserId) => Promise; /** * Gets the refresh token. * @param userId - The optional user id to get the refresh token for; if not provided, the active user is used. - * @returns A promise that resolves with the refresh token or undefined. + * @returns A promise that resolves with the refresh token or null. */ - getRefreshToken: (userId?: UserId) => Promise; + getRefreshToken: (userId?: UserId) => Promise; /** * Sets the API Key Client ID for the active user id in memory or disk based on the given vaultTimeoutAction and vaultTimeout. diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index dd98ce2b446..242a7480958 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -175,7 +175,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { } // At this point of rotating their keys, they should still have their old user key in state - const oldUserKey = await firstValueFrom(this.cryptoService.activeUserKey$); + const oldUserKey = await firstValueFrom(this.cryptoService.userKey$(userId)); const deviceIdentifier = await this.appIdService.getAppId(); const secretVerificationRequest = new SecretVerificationRequest(); diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index f61bce563f3..1527870cb49 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -595,7 +595,7 @@ describe("deviceTrustService", () => { const fakeNewUserKeyData = new Uint8Array(64); fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1); fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey; - cryptoService.activeUserKey$ = of(fakeNewUserKey); + cryptoService.userKey$.mockReturnValue(of(fakeNewUserKey)); }); it("throws an error when a null user id is passed in", async () => { @@ -631,7 +631,9 @@ describe("deviceTrustService", () => { fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1); // Mock the retrieval of a user key that differs from the new one passed into the method - cryptoService.activeUserKey$ = of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey); + cryptoService.userKey$.mockReturnValue( + of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey), + ); appIdService.getAppId.mockResolvedValue("test_device_identifier"); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index 65d1030bd3a..6b81844afb4 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -1,5 +1,7 @@ import { firstValueFrom } from "rxjs"; +import { LogoutReason } from "@bitwarden/auth/common"; + import { ApiService } from "../../abstractions/api.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; @@ -57,7 +59,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private logService: LogService, private organizationService: OrganizationService, private keyGenerationService: KeyGenerationService, - private logoutCallback: (expired: boolean, userId?: string) => Promise, + private logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise, private stateProvider: StateProvider, ) { this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR); @@ -192,7 +194,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { if (this.logoutCallback != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.logoutCallback(false); + this.logoutCallback("keyConnectorError"); } throw new Error("Key Connector error"); } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 9c5dd9fc91f..d7a4c527162 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; +import { LogoutReason } from "@bitwarden/auth/common"; + import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; @@ -9,11 +11,18 @@ import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; import { StorageOptions } from "../../platform/models/domain/storage-options"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; -import { DecodedAccessToken, TokenService, TokenStorageLocation } from "./token.service"; +import { + AccessTokenKey, + DecodedAccessToken, + TokenService, + TokenStorageLocation, +} from "./token.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, @@ -36,6 +45,7 @@ describe("TokenService", () => { let keyGenerationService: MockProxy; let encryptService: MockProxy; let logService: MockProxy; + let logoutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: string]>; const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut; const memoryVaultTimeout: VaultTimeout = 30; @@ -46,6 +56,9 @@ describe("TokenService", () => { const accessTokenJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwibmJmIjoxNzA5MzI0MTExLCJpYXQiOjE3MDkzMjQxMTEsImV4cCI6MTcwOTMyNzcxMSwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl0sImNsaWVudF9pZCI6IndlYiIsInN1YiI6ImVjZTcwYTEzLTcyMTYtNDNjNC05OTc3LWIxMDMwMTQ2ZTFlNyIsImF1dGhfdGltZSI6MTcwOTMyNDEwNCwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoiZXhhbXBsZUBiaXR3YXJkZW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJzc3RhbXAiOiJHWTdKQU82NENLS1RLQkI2WkVBVVlMMldPUVU3QVNUMiIsIm5hbWUiOiJUZXN0IFVzZXIiLCJvcmdvd25lciI6WyI5MmI0OTkwOC1iNTE0LTQ1YTgtYmFkYi1iMTAzMDE0OGZlNTMiLCIzOGVkZTMyMi1iNGI0LTRiZDgtOWUwOS1iMTA3MDExMmRjMTEiLCJiMmQwNzAyOC1hNTgzLTRjM2UtOGQ2MC1iMTA3MDExOThjMjkiLCJiZjkzNGJhMi0wZmQ0LTQ5ZjItYTk1ZS1iMTA3MDExZmM5ZTYiLCJjMGI3Zjc1ZC0wMTVmLTQyYzktYjNhNi1iMTA4MDE3NjA3Y2EiXSwiZGV2aWNlIjoiNGI4NzIzNjctMGRhNi00MWEwLWFkY2ItNzdmMmZlZWZjNGY0IiwianRpIjoiNzUxNjFCRTQxMzFGRjVBMkRFNTExQjhDNEUyRkY4OUEifQ.n7roP8sSbfwcYdvRxZNZds27IK32TW6anorE6BORx_Q"; + const encryptedAccessToken = + "2.rFNYSTJoljn8h6GOSNVYdQ==|4dIp7ONJzC+Kx1ClA+1aIAb7EqCQ4OjnADCYdCPg7BKkdheG+yM62ZiONFk+S6at84M+RnGWWO04aIjinTdJhlhyUmszePNATxIfX60Y+bFKQhlMuCtZpYdEmQDzXVgT43YRbf/6NnN9WzhefLqeMiocwoIJTEpLptb+Zcm7T3MJpkX4dR9w5LUOxUTNFEGd5PlWaI8FBavOkNsrzY5skRK70pvFABET5IDeRlKhi8NwbzvTzkO3SisLRzih+djiz5nEZf0+ujeGAp6P+o7l0mB0sXVsNJzcuE4S9QtHLnx31N6z3mQm5pOgP4EmEOdRIcQGc1p7dL1vXcXtaTJLtfKXoJjJbYT3wplnY9Pf8+2FVxdbM3bRB2yVsnEzgLcf9UchKThQSdOy8+5TO/prDbUt5mDpO4GmRltom5ncda8yJaD3Hw1DO7fa0Xh+kfeByxb1AwBC+GTPfqmo5uqr0J4dZsf9cGlPMTElwR3GYmD60OcQ6iDX36CZZjqqJqBwKSpepDXV39p9G347e6YAAvJenLDKtdjgfWXCMXbkwETbMgYooFDRd60KYsGIXV16UwzJSvczgTY2d+hYb2Cl0lClequaiwcRxLVtW2xau6qoEPjTqJjJi9I0Cs2WNL4LRH96Ir14a3bEtnTvkO1NjN+bQNon+KksaP2BqTbuiAfZbBP/cL4S1Oew4G00PSLZUGV5S1BI0ooJy6e2NLQJlYqfCeKM6RgpvgfOiXlZddVgkkB6lohLjyVvcSZNuKPjs1wZMZ9C76bKb6o39NFK8G3/YScELFf9gkueWjmhcjrs22+xNDn5rxXeedwIkVW9UJVNLc//eGxLfp70y8fNDcyTPRN1UUpqT8+wSz+9ZHl4DLUK0DE2jIveEDke8vi4MK/XLMC/c50rr1NCEuVy6iA3nwiOzVo/GNfeKTpzMcR/D9A0gxkC9GyZ3riSsMQsGNXhZCZLdsFYp0gLiiJxVilMUfyTWaygsNm87GPY3ep3GEHcq/pCuxrpLQQYT3V1j95WJvFxb8dSLiPHb8STR0GOZhe7SquI5LIRmYCFTo+3VBnItYeuin9i2xCIqWz886xIyllHN2BIPILbA1lCOsCsz1BRRGNqtLvmTeVRO8iujsHWBJicVgSI7/dgSJwcdOv2t4TIVtnN1hJkQnz+HZcJ2FYK/VWlo4UQYYoML52sBd1sSz/n8/8hrO2N4X9frHHNCrcxeoyChTKo2cm4rAxHylLbCZYvGt/KIW9x3AFkPBMr7tAc3yq98J0Crna8ukXc3F3uGb5QXLnBi//3zBDN6RCv7ByaFW5G0I+pglBegzeFBqKH8xwfy76B2e2VLFF8rz/r/wQzlumGFypsRhAoGxrkZyzjec/k+RNR0arf7TTX7ymC1cueTnItRDx89veW6WLlF53NpAGqC8GJSp4T2FGIIk01y29j6Ji7GOlQ8BUbyLWYjMfHf3khRzAfr6UC2QgVvKWQTKET4Y/b1nZCnwxeW8wC80GHtYGuarsU+KlsEw4242cjyIN1GobrWaA2GTOedQDEMWUA64McAw5fAvMEEao5DM7i57tMzJHeKfruyMuXYQkBca094vmATjJ/T+kIrWGIcmxCT/Fp2SW1hcxr6Ciwuog84LVfbVlUl2MAj3eC/xqL/5HP6Q3ObD0ld444GV+HSrQUqfIvEIn9gFmalW6TGugyhfROACCogoXbeIr1AyMUNDnl4EWlPl6u7SQvPX+itKyq4qhaK2J0W6f7ElLVQ5GbC2uwARuhXOi7mqEZ5FP0V675C5NPZOl2ZEd6BhmuyhGkmQEtEvw0DCKnbKM7bKMk90Y599DSnuEna4BNFBVjJ7k+BuNhXUKO+iNcDZT0pCQhOKRVLWsaqVff3BsuQ4zMEOVnccJwwAVipwSRyxZi8bF+Wyun6BVI8pz1CBvRMy+6ifmIq2awEL8NnV65hF2jyZDEVwsnrvCyT7MlM8l5C3MhqH/MgMcKqOsUz+P6Jv5sBi4WvojsaHzqxQ6miBHpHhGDpYH5K53LVs36henB/tOUTcg5ZnO4ZM67jjB7Oz7to+QnJsldp5Bdwvi1XD/4jeh/Llezu5/KwwytSHnZG1z6dZA7B8rKwnI+yN2Qnfi70h68jzGZ1xCOFPz9KMorNKP3XLw8x2g9H6lEBXdV95uc/TNw+WTJbvKRawns/DZhM1u/g13lU6JG19cht3dh/DlKRcJpj1AdOAxPiUubTSkhBmdwRj2BTTHrVlF3/9ladTP4s4f6Zj9TtQvR9CREVe7CboGflxDYC+Jww3PU50XLmxQjkuV5MkDAmBVcyFCFOcHhDRoxet4FX9ec0wjNeDpYtkI8B/qUS1Rp+is1jOxr4/ni|pabwMkF/SdYKdDlow4uKxaObrAP0urmv7N7fA9bedec="; + const accessTokenDecoded: DecodedAccessToken = { iss: "http://localhost", nbf: 1709324111, @@ -93,6 +106,7 @@ describe("TokenService", () => { keyGenerationService = mock(); encryptService = mock(); logService = mock(); + logoutCallback = jest.fn(); const supportsSecureStorage = false; // default to false; tests will override as needed tokenService = createTokenService(supportsSecureStorage); @@ -152,7 +166,7 @@ describe("TokenService", () => { expect(result).toEqual(true); }); - it("should return false if no access token exists in memory, disk, or secure storage", async () => { + it("returns false when no access token exists in memory, disk, or secure storage", async () => { // Act const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken)); @@ -162,7 +176,7 @@ describe("TokenService", () => { }); describe("setAccessToken", () => { - it("should throw an error if the access token is null", async () => { + it("throws an error when the access token is null", async () => { // Act const result = tokenService.setAccessToken( null, @@ -173,7 +187,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Access token is required."); }); - it("should throw an error if an invalid token is passed in", async () => { + it("throws an error when an invalid token is passed in", async () => { // Act const result = tokenService.setAccessToken( "invalidToken", @@ -216,7 +230,7 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should set the access token in memory", async () => { + it("set the access token in memory", async () => { // Act await tokenService.setAccessToken( accessTokenJwt, @@ -246,6 +260,14 @@ describe("TokenService", () => { }); describe("Disk storage tests (secure storage supported on platform)", () => { + const accessTokenKey = new SymmetricCryptoKey( + new Uint8Array(64) as CsprngArray, + ) as AccessTokenKey; + + const accessTokenKeyB64 = { + keyB64: + "lI7lSoejJ1HsrTkRs2Ipm0x+YcZMKpgm7WQGCNjAWmFAyGOKossXwBJvvtbxcYDZ0G0XNY8Gp7DBXZV2tWAO5w==", + }; beforeEach(() => { const supportsSecureStorage = true; tokenService = createTokenService(supportsSecureStorage); @@ -259,7 +281,7 @@ describe("TokenService", () => { .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - keyGenerationService.createKey.mockResolvedValue("accessTokenKey" as any); + keyGenerationService.createKey.mockResolvedValue(accessTokenKey); const mockEncryptedAccessToken = "encryptedAccessToken"; @@ -267,6 +289,11 @@ describe("TokenService", () => { encryptedString: mockEncryptedAccessToken, } as any); + // First call resolves to null to simulate no key in secure storage + // then resolves to the key to simulate the key being set in secure storage + // and retrieved successfully to ensure it was set. + secureStorageService.get.mockResolvedValueOnce(null).mockResolvedValue(accessTokenKeyB64); + // Act await tokenService.setAccessToken( accessTokenJwt, @@ -278,7 +305,7 @@ describe("TokenService", () => { // assert that the AccessTokenKey was set in secure storage expect(secureStorageService.save).toHaveBeenCalledWith( accessTokenKeySecureStorageKey, - "accessTokenKey", + accessTokenKey, secureStorageOptions, ); @@ -292,18 +319,85 @@ describe("TokenService", () => { singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock, ).toHaveBeenCalledWith(null); }); + + it("should fallback to disk storage for the access token if the access token cannot be set in secure storage", async () => { + // This tests the scenario where the access token key silently fails to be set in secure storage + + // Arrange: + keyGenerationService.createKey.mockResolvedValue(accessTokenKey); + + // First call resolves to null to simulate no key in secure storage + // and then resolves to no key after it should have been set + secureStorageService.get.mockResolvedValueOnce(null).mockResolvedValue(null); + + // Act + await tokenService.setAccessToken( + accessTokenJwt, + diskVaultTimeoutAction, + diskVaultTimeout, + ); + // Assert + + // assert that we tried to store the AccessTokenKey in secure storage + expect(secureStorageService.save).toHaveBeenCalledWith( + accessTokenKeySecureStorageKey, + accessTokenKey, + secureStorageOptions, + ); + + // assert that we logged the error + expect(logService.error).toHaveBeenCalledWith( + "SetAccessToken: storing encrypted access token in secure storage failed. Falling back to disk storage.", + new Error("New Access token key unable to be retrieved from secure storage."), + ); + + // assert that the access token was put on disk unencrypted + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock, + ).toHaveBeenCalledWith(accessTokenJwt); + }); + + it("should fallback to disk storage for the access token if secure storage errors on trying to get an existing access token key", async () => { + // This tests the scenario for linux users who don't have secure storage configured. + + // Arrange: + keyGenerationService.createKey.mockResolvedValue(accessTokenKey); + + // Mock linux secure storage error + const secureStorageError = "Secure storage error"; + secureStorageService.get.mockRejectedValue(new Error(secureStorageError)); + + // Act + await tokenService.setAccessToken( + accessTokenJwt, + diskVaultTimeoutAction, + diskVaultTimeout, + ); + // Assert + + // assert that we logged the error + expect(logService.error).toHaveBeenCalledWith( + "SetAccessToken: storing encrypted access token in secure storage failed. Falling back to disk storage.", + new Error(secureStorageError), + ); + + // assert that the access token was put on disk unencrypted + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock, + ).toHaveBeenCalledWith(accessTokenJwt); + }); }); }); describe("getAccessToken", () => { - it("should return undefined if no user id is provided and there is no active user in global state", async () => { + it("returns null when no user id is provided and there is no active user in global state", async () => { // Act const result = await tokenService.getAccessToken(); // Assert - expect(result).toBeUndefined(); + expect(result).toBeNull(); }); - it("should return null if no access token is found in memory, disk, or secure storage", async () => { + it("returns null when no access token is found in memory, disk, or secure storage", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -317,11 +411,8 @@ describe("TokenService", () => { describe("Memory storage tests", () => { test.each([ - [ - "should get the access token from memory for the provided user id", - userIdFromAccessToken, - ], - ["should get the access token from memory with no user id provided", undefined], + ["gets the access token from memory when a user id is provided ", userIdFromAccessToken], + ["gets the access token from memory when no user id is provided", undefined], ])("%s", async (_, userId) => { // Arrange singleUserStateProvider @@ -350,11 +441,8 @@ describe("TokenService", () => { describe("Disk storage tests (secure storage not supported on platform)", () => { test.each([ - [ - "should get the access token from disk for the specified user id", - userIdFromAccessToken, - ], - ["should get the access token from disk with no user id specified", undefined], + ["gets the access token from disk when the user id is specified", userIdFromAccessToken], + ["gets the access token from disk when no user id is specified", undefined], ])("%s", async (_, userId) => { // Arrange singleUserStateProvider @@ -387,11 +475,11 @@ describe("TokenService", () => { test.each([ [ - "should get the encrypted access token from disk, decrypt it, and return it when user id is provided", + "gets the encrypted access token from disk, decrypts it, and returns it when a user id is provided", userIdFromAccessToken, ], [ - "should get the encrypted access token from disk, decrypt it, and return it when no user id is provided", + "gets the encrypted access token from disk, decrypts it, and returns it when no user id is provided", undefined, ], ])("%s", async (_, userId) => { @@ -423,11 +511,11 @@ describe("TokenService", () => { test.each([ [ - "should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and a user id is provided", + "falls back and gets the unencrypted access token from disk when there isn't an access token key in secure storage and a user id is provided", userIdFromAccessToken, ], [ - "should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and no user id is provided", + "falls back and gets the unencrypted access token from disk when there isn't an access token key in secure storage and no user id is provided", undefined, ], ])("%s", async (_, userId) => { @@ -455,11 +543,80 @@ describe("TokenService", () => { // Assert expect(result).toEqual(accessTokenJwt); }); + + it("logs the error and logs the user out when the access token key cannot be retrieved from secure storage if the access token is encrypted", async () => { + // This tests the intermittent windows 10/11 scenario in which the access token key was stored successfully in secure storage and the + // access token was encrypted with it and stored on disk successfully. However, on retrieval the access token key isn't able to + // retrieved for whatever reason. + + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, undefined]); + + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]); + + // No access token key set + + // Act + const result = await tokenService.getAccessToken(userIdFromAccessToken); + + // Assert + expect(result).toBeNull(); + + // assert that we logged the error + expect(logService.error).toHaveBeenCalledWith( + "Access token key not found to decrypt encrypted access token. Logging user out.", + ); + + // assert that we logged the user out + expect(logoutCallback).toHaveBeenCalledWith( + "accessTokenUnableToBeDecrypted", + userIdFromAccessToken, + ); + }); + + it("logs the error and logs the user out when secure storage errors on trying to get an access token key", async () => { + // This tests the linux scenario where users might not have secure storage support configured. + + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, undefined]); + + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]); + + // Mock linux secure storage error + const secureStorageError = "Secure storage error"; + secureStorageService.get.mockRejectedValue(new Error(secureStorageError)); + + // Act + const result = await tokenService.getAccessToken(userIdFromAccessToken); + + // Assert + expect(result).toBeNull(); + + // assert that we logged the error + expect(logService.error).toHaveBeenCalledWith( + "Access token key retrieval failed. Unable to decrypt encrypted access token. Logging user out.", + new Error(secureStorageError), + ); + + // assert that we logged the user out + expect(logoutCallback).toHaveBeenCalledWith( + "accessTokenUnableToBeDecrypted", + userIdFromAccessToken, + ); + }); }); }); describe("clearAccessToken", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { + it("throws an error when no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error const result = tokenService.clearAccessToken(); @@ -475,11 +632,11 @@ describe("TokenService", () => { test.each([ [ - "should clear the access token from all storage locations for the provided user id", + "clears the access token from all storage locations when a user id is provided", userIdFromAccessToken, ], [ - "should clear the access token from all storage locations for the global active user", + "clears the access token from all storage locations when there is a global active user", undefined, ], ])("%s", async (_, userId) => { @@ -519,7 +676,7 @@ describe("TokenService", () => { }); describe("decodeAccessToken", () => { - it("should throw an error if no access token provided or retrieved from state", async () => { + it("throws an error when no access token is provided or retrievable from state", async () => { // Access tokenService.getAccessToken = jest.fn().mockResolvedValue(null); @@ -530,7 +687,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Access token not found."); }); - it("should decode the access token", async () => { + it("decodes the access token when a valid one is stored", async () => { // Arrange tokenService.getAccessToken = jest.fn().mockResolvedValue(accessTokenJwt); @@ -544,7 +701,7 @@ describe("TokenService", () => { describe("Data methods", () => { describe("getTokenExpirationDate", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -555,7 +712,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should return null if the decoded access token is null", async () => { + it("returns null when the decoded access token is null", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null); @@ -566,7 +723,7 @@ describe("TokenService", () => { expect(result).toBeNull(); }); - it("should return null if the decoded access token does not have an expiration date", async () => { + it("returns null when the decoded access token does not have an expiration date", async () => { // Arrange const accessTokenDecodedWithoutExp = { ...accessTokenDecoded }; delete accessTokenDecodedWithoutExp.exp; @@ -581,7 +738,7 @@ describe("TokenService", () => { expect(result).toBeNull(); }); - it("should return null if the decoded access token has an non numeric expiration date", async () => { + it("returns null when the decoded access token has a non numeric expiration date", async () => { // Arrange const accessTokenDecodedWithNonNumericExp = { ...accessTokenDecoded, exp: "non-numeric" }; tokenService.decodeAccessToken = jest @@ -595,7 +752,7 @@ describe("TokenService", () => { expect(result).toBeNull(); }); - it("should return the expiration date of the access token", async () => { + it("returns the expiration date of the access token when a valid access token is stored", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); @@ -608,7 +765,7 @@ describe("TokenService", () => { }); describe("tokenSecondsRemaining", () => { - it("should return 0 if the tokenExpirationDate is null", async () => { + it("returns 0 when the tokenExpirationDate is null", async () => { // Arrange tokenService.getTokenExpirationDate = jest.fn().mockResolvedValue(null); @@ -619,7 +776,7 @@ describe("TokenService", () => { expect(result).toEqual(0); }); - it("should return the number of seconds remaining until the token expires", async () => { + it("returns the number of seconds remaining until the token expires", async () => { // Arrange // Lock the time to ensure a consistent test environment // otherwise we have flaky issues with set system time date and the Date.now() call. @@ -644,7 +801,7 @@ describe("TokenService", () => { jest.useRealTimers(); }); - it("should return the number of seconds remaining until the token expires, considering an offset", async () => { + it("returns the number of seconds remaining until the token expires when given an offset", async () => { // Arrange // Lock the time to ensure a consistent test environment // otherwise we have flaky issues with set system time date and the Date.now() call. @@ -672,7 +829,7 @@ describe("TokenService", () => { }); describe("tokenNeedsRefresh", () => { - it("should return true if token is within the default refresh threshold (5 min)", async () => { + it("returns true when the token is within the default refresh threshold (5 min)", async () => { // Arrange const tokenSecondsRemaining = 60; tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining); @@ -684,7 +841,7 @@ describe("TokenService", () => { expect(result).toEqual(true); }); - it("should return false if token is outside the default refresh threshold (5 min)", async () => { + it("returns false when the token is outside the default refresh threshold (5 min)", async () => { // Arrange const tokenSecondsRemaining = 600; tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining); @@ -696,7 +853,7 @@ describe("TokenService", () => { expect(result).toEqual(false); }); - it("should return true if token is within the specified refresh threshold", async () => { + it("returns true when the token is within the specified refresh threshold", async () => { // Arrange const tokenSecondsRemaining = 60; tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining); @@ -708,7 +865,7 @@ describe("TokenService", () => { expect(result).toEqual(true); }); - it("should return false if token is outside the specified refresh threshold", async () => { + it("returns false when the token is outside the specified refresh threshold", async () => { // Arrange const tokenSecondsRemaining = 600; tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining); @@ -722,7 +879,7 @@ describe("TokenService", () => { }); describe("getUserId", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -733,7 +890,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should throw an error if the decoded access token is null", async () => { + it("throws an error when the decoded access token is null", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null); @@ -744,7 +901,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No user id found"); }); - it("should throw an error if the decoded access token has a non-string user id", async () => { + it("throws an error when the decoded access token has a non-string user id", async () => { // Arrange const accessTokenDecodedWithNonStringSub = { ...accessTokenDecoded, sub: 123 }; tokenService.decodeAccessToken = jest @@ -758,7 +915,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No user id found"); }); - it("should return the user id from the decoded access token", async () => { + it("returns the user id from the decoded access token when a valid access token is stored", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); @@ -771,7 +928,7 @@ describe("TokenService", () => { }); describe("getUserIdFromAccessToken", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -782,7 +939,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should throw an error if the decoded access token is null", async () => { + it("throws an error when the decoded access token is null", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null); @@ -793,7 +950,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No user id found"); }); - it("should throw an error if the decoded access token has a non-string user id", async () => { + it("throws an error when the decoded access token has a non-string user id", async () => { // Arrange const accessTokenDecodedWithNonStringSub = { ...accessTokenDecoded, sub: 123 }; tokenService.decodeAccessToken = jest @@ -807,7 +964,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No user id found"); }); - it("should return the user id from the decoded access token", async () => { + it("returns the user id from the decoded access token when a valid access token is stored", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); @@ -820,7 +977,7 @@ describe("TokenService", () => { }); describe("getEmail", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -831,7 +988,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should throw an error if the decoded access token is null", async () => { + it("throws an error when the decoded access token is null", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null); @@ -842,7 +999,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No email found"); }); - it("should throw an error if the decoded access token has a non-string email", async () => { + it("throws an error when the decoded access token has a non-string email", async () => { // Arrange const accessTokenDecodedWithNonStringEmail = { ...accessTokenDecoded, email: 123 }; tokenService.decodeAccessToken = jest @@ -856,7 +1013,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No email found"); }); - it("should return the email from the decoded access token", async () => { + it("returns the email from the decoded access token when a valid access token is stored", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); @@ -869,7 +1026,7 @@ describe("TokenService", () => { }); describe("getEmailVerified", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -880,7 +1037,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should throw an error if the decoded access token is null", async () => { + it("throws an error when the decoded access token is null", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null); @@ -891,7 +1048,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No email verification found"); }); - it("should throw an error if the decoded access token has a non-boolean email_verified", async () => { + it("throws an error when the decoded access token has a non-boolean email_verified", async () => { // Arrange const accessTokenDecodedWithNonBooleanEmailVerified = { ...accessTokenDecoded, @@ -908,7 +1065,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No email verification found"); }); - it("should return the email_verified from the decoded access token", async () => { + it("returns the email_verified from the decoded access token when a valid access token is stored", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); @@ -921,7 +1078,7 @@ describe("TokenService", () => { }); describe("getName", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -932,7 +1089,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should return null if the decoded access token is null", async () => { + it("returns null when the decoded access token is null", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null); @@ -943,7 +1100,7 @@ describe("TokenService", () => { expect(result).toBeNull(); }); - it("should return null if the decoded access token has a non-string name", async () => { + it("returns null when the decoded access token has a non-string name", async () => { // Arrange const accessTokenDecodedWithNonStringName = { ...accessTokenDecoded, name: 123 }; tokenService.decodeAccessToken = jest @@ -957,7 +1114,7 @@ describe("TokenService", () => { expect(result).toBeNull(); }); - it("should return the name from the decoded access token", async () => { + it("returns the name from the decoded access token when a valid access token is stored", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); @@ -970,7 +1127,7 @@ describe("TokenService", () => { }); describe("getIssuer", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -981,7 +1138,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should throw an error if the decoded access token is null", async () => { + it("throws an error when the decoded access token is null", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null); @@ -992,7 +1149,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No issuer found"); }); - it("should throw an error if the decoded access token has a non-string iss", async () => { + it("throws an error when the decoded access token has a non-string iss", async () => { // Arrange const accessTokenDecodedWithNonStringIss = { ...accessTokenDecoded, iss: 123 }; tokenService.decodeAccessToken = jest @@ -1006,7 +1163,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("No issuer found"); }); - it("should return the issuer from the decoded access token", async () => { + it("returns the issuer from the decoded access token when a valid access token is stored", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded); @@ -1019,7 +1176,7 @@ describe("TokenService", () => { }); describe("getIsExternal", () => { - it("should throw an error if the access token cannot be decoded", async () => { + it("throws an error when the access token cannot be decoded", async () => { // Arrange tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error")); @@ -1030,7 +1187,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Failed to decode access token: Mock error"); }); - it("should return false if the amr (Authentication Method Reference) claim does not contain 'external'", async () => { + it("returns false when the amr (Authentication Method Reference) claim does not contain 'external'", async () => { // Arrange const accessTokenDecodedWithoutExternalAmr = { ...accessTokenDecoded, @@ -1047,7 +1204,7 @@ describe("TokenService", () => { expect(result).toEqual(false); }); - it("should return true if the amr (Authentication Method Reference) claim contains 'external'", async () => { + it("returns true when the amr (Authentication Method Reference) claim contains 'external'", async () => { // Arrange const accessTokenDecodedWithExternalAmr = { ...accessTokenDecoded, @@ -1073,7 +1230,7 @@ describe("TokenService", () => { const refreshTokenSecureStorageKey = `${userIdFromAccessToken}${refreshTokenPartialSecureStorageKey}`; describe("setRefreshToken", () => { - it("should throw an error if no user id is provided", async () => { + it("throws an error when no user id is provided", async () => { // Act // note: don't await here because we want to test the error const result = (tokenService as any).setRefreshToken( @@ -1113,7 +1270,7 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should set the refresh token in memory for the specified user id", async () => { + it("sets the refresh token in memory when given a user id", async () => { // Act await (tokenService as any).setRefreshToken( refreshToken, @@ -1130,7 +1287,7 @@ describe("TokenService", () => { }); describe("Disk storage tests (secure storage not supported on platform)", () => { - it("should set the refresh token in disk for the specified user id", async () => { + it("sets the refresh token in disk when given a user id", async () => { // Act await (tokenService as any).setRefreshToken( refreshToken, @@ -1152,7 +1309,7 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should set the refresh token in secure storage, null out data on disk or in memory, and set a flag to indicate the token has been migrated for the specified user id", async () => { + it("sets the refresh token in secure storage, removes data on disk or in memory, and sets a flag to indicate the token has been migrated when given a user id", async () => { // Arrange: // For testing purposes, let's assume that the token is already in disk and memory singleUserStateProvider @@ -1163,6 +1320,9 @@ describe("TokenService", () => { .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) .stateSubject.next([userIdFromAccessToken, refreshToken]); + // We immediately call to get the refresh token from secure storage after setting it to ensure it was set. + secureStorageService.get.mockResolvedValue(refreshToken); + // Act await (tokenService as any).setRefreshToken( refreshToken, @@ -1187,18 +1347,166 @@ describe("TokenService", () => { singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock, ).toHaveBeenCalledWith(null); }); + + it("tries to set the refresh token in secure storage then falls back to disk storage when the refresh token cannot be read back out of secure storage", async () => { + // Arrange: + // We immediately call to get the refresh token from secure storage after setting it to ensure it was set. + // So, set it to return null to mock a failure to set the refresh token in secure storage. + // This mocks the windows 10/11 intermittent issue where the token is not set in secure storage successfully. + secureStorageService.get.mockResolvedValue(null); + + // Act + await (tokenService as any).setRefreshToken( + refreshToken, + diskVaultTimeoutAction, + diskVaultTimeout, + userIdFromAccessToken, + ); + // Assert + + // assert that the refresh token was set in secure storage + expect(secureStorageService.save).toHaveBeenCalledWith( + refreshTokenSecureStorageKey, + refreshToken, + secureStorageOptions, + ); + + // assert that we tried to set the refresh token in secure storage, but it failed, so we reverted to disk storage + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock, + ).toHaveBeenCalledWith(refreshToken); + }); + + it("tries to set the refresh token in secure storage, throws an error, then falls back to disk storage when secure storage isn't supported", async () => { + // Arrange: + // Mock the secure storage service to throw an error when trying to save the refresh token + // to simulate linux scenarios where a secure storage provider isn't configured. + secureStorageService.save.mockRejectedValue(new Error("Secure storage not supported")); + + // Act + await (tokenService as any).setRefreshToken( + refreshToken, + diskVaultTimeoutAction, + diskVaultTimeout, + userIdFromAccessToken, + ); + // Assert + + // assert that the refresh token was set in secure storage + expect(secureStorageService.save).toHaveBeenCalledWith( + refreshTokenSecureStorageKey, + refreshToken, + secureStorageOptions, + ); + + // assert that we tried to set the refresh token in secure storage, but it failed, so we reverted to disk storage + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock, + ).toHaveBeenCalledWith(refreshToken); + }); + + it("returns the unencrypted access token when secure storage retrieval fails but the access token is still pre-migration", async () => { + // This tests the linux scenario where users might not have secure storage support configured. + + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, undefined]); + + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + + // Mock linux secure storage error + const secureStorageError = "Secure storage error"; + secureStorageService.get.mockRejectedValue(new Error(secureStorageError)); + + // Act + const result = await tokenService.getAccessToken(userIdFromAccessToken); + + // Assert + // assert that we returned the unencrypted, pre-migration access token + expect(result).toBe(accessTokenJwt); + + // assert that we did not log an error or log the user out + expect(logService.error).not.toHaveBeenCalled(); + + expect(logoutCallback).not.toHaveBeenCalled(); + }); + + it("does not error and fallback to disk storage when passed a null value for the refresh token", async () => { + // Arrange + secureStorageService.get.mockResolvedValue(null); + + // Act + await (tokenService as any).setRefreshToken( + null, + diskVaultTimeoutAction, + diskVaultTimeout, + userIdFromAccessToken, + ); + + // Assert + expect(secureStorageService.save).toHaveBeenCalledWith( + refreshTokenSecureStorageKey, + null, + secureStorageOptions, + ); + + expect(logService.error).not.toHaveBeenCalled(); + + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock, + ).toHaveBeenCalledWith(null); + + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock, + ).toHaveBeenCalledWith(null); + }); + + it("logs the error and logs the user out when the access token cannot be decrypted", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, undefined]); + + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, encryptedAccessToken]); + + secureStorageService.get.mockResolvedValue(accessTokenKeyB64); + encryptService.decryptToUtf8.mockRejectedValue(new Error("Decryption error")); + + // Act + const result = await tokenService.getAccessToken(userIdFromAccessToken); + + // Assert + expect(result).toBeNull(); + + // assert that we logged the error + expect(logService.error).toHaveBeenCalledWith( + "Failed to decrypt access token", + new Error("Decryption error"), + ); + + // assert that we logged the user out + expect(logoutCallback).toHaveBeenCalledWith( + "accessTokenUnableToBeDecrypted", + userIdFromAccessToken, + ); + }); }); }); describe("getRefreshToken", () => { - it("should return undefined if no user id is provided and there is no active user in global state", async () => { + it("returns null when no user id is provided and there is no active user in global state", async () => { // Act const result = await (tokenService as any).getRefreshToken(); // Assert - expect(result).toBeUndefined(); + expect(result).toBeNull(); }); - it("should return null if no refresh token is found in memory, disk, or secure storage", async () => { + it("returns null when no refresh token is found in memory, disk, or secure storage", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -1211,7 +1519,7 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should get the refresh token from memory with no user id specified (uses global active user)", async () => { + it("gets the refresh token from memory when no user id is specified (uses global active user)", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1233,7 +1541,7 @@ describe("TokenService", () => { expect(result).toEqual(refreshToken); }); - it("should get the refresh token from memory for the specified user id", async () => { + it("gets the refresh token from memory when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1251,7 +1559,7 @@ describe("TokenService", () => { }); describe("Disk storage tests (secure storage not supported on platform)", () => { - it("should get the refresh token from disk with no user id specified", async () => { + it("gets the refresh token from disk when no user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1272,7 +1580,7 @@ describe("TokenService", () => { expect(result).toEqual(refreshToken); }); - it("should get the refresh token from disk for the specified user id", async () => { + it("gets the refresh token from disk when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1295,7 +1603,7 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should get the refresh token from secure storage when no user id is specified and the migration flag is set to true", async () => { + it("gets the refresh token from secure storage when no user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1318,7 +1626,7 @@ describe("TokenService", () => { expect(result).toEqual(refreshToken); }); - it("should get the refresh token from secure storage when user id is specified and the migration flag set to true", async () => { + it("gets the refresh token from secure storage when a user id is specified", async () => { // Arrange singleUserStateProvider @@ -1337,7 +1645,7 @@ describe("TokenService", () => { expect(result).toEqual(refreshToken); }); - it("should fallback and get the refresh token from disk when user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => { + it("falls back and gets the refresh token from disk when a user id is specified even if the platform supports secure storage", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1357,7 +1665,7 @@ describe("TokenService", () => { expect(secureStorageService.get).not.toHaveBeenCalled(); }); - it("should fallback and get the refresh token from disk when no user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => { + it("falls back and gets the refresh token from disk when no user id is specified even if the platform supports secure storage", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1381,11 +1689,80 @@ describe("TokenService", () => { // assert that secure storage was not called expect(secureStorageService.get).not.toHaveBeenCalled(); }); + + it("returns null when the refresh token is not found in memory, on disk, or in secure storage", async () => { + // Arrange + secureStorageService.get.mockResolvedValue(null); + + // Act + const result = await tokenService.getRefreshToken(userIdFromAccessToken); + + // Assert + expect(result).toBeNull(); + }); + + it("returns null and logs when the refresh token is not found in secure storage when it should be", async () => { + // This scenario mocks the case where we have intermittent windows 10/11 issues w/ secure storage not + // returning the refresh token when it should be there. + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, undefined]); + + singleUserStateProvider + .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, undefined]); + + secureStorageService.get.mockResolvedValue(null); + + // Act + const result = await tokenService.getRefreshToken(userIdFromAccessToken); + + // Assert + expect(result).toBeNull(); + + expect(logService.error).toHaveBeenCalledWith( + "Refresh token not found in secure storage. Access token will fail to refresh upon expiration or manual refresh.", + ); + }); + + it("logs out when retrieving the refresh token out of secure storage errors", async () => { + // This scenario mocks the case where linux users don't have secure storage configured. + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, undefined]); + + singleUserStateProvider + .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, undefined]); + + const secureStorageSvcMockErrorMsg = "Secure storage retrieval error"; + + secureStorageService.get.mockRejectedValue(new Error(secureStorageSvcMockErrorMsg)); + + // Act + const result = await tokenService.getRefreshToken(userIdFromAccessToken); + + // Assert + expect(result).toBeNull(); + + // expect that we logged an error and logged the user out + expect(logService.error).toHaveBeenCalledWith( + `Failed to retrieve refresh token from secure storage`, + new Error(secureStorageSvcMockErrorMsg), + ); + + expect(logoutCallback).toHaveBeenCalledWith( + "refreshTokenSecureStorageRetrievalFailure", + userIdFromAccessToken, + ); + }); }); }); describe("clearRefreshToken", () => { - it("should throw an error if no user id is provided", async () => { + it("throws an error when no user id is provided", async () => { // Act // note: don't await here because we want to test the error const result = (tokenService as any).clearRefreshToken(); @@ -1399,7 +1776,7 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should clear the refresh token from all storage locations for the specified user id", async () => { + it("clears the refresh token from all storage locations when given a user id", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY) @@ -1433,7 +1810,7 @@ describe("TokenService", () => { const clientId = "clientId"; describe("setClientId", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { + it("throws an error when no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error const result = tokenService.setClientId(clientId, VaultTimeoutAction.Lock, null); @@ -1470,7 +1847,7 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should set the client id in memory when there is an active user in global state", async () => { + it("sets the client id in memory when there is an active user in global state", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -1486,7 +1863,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(clientId); }); - it("should set the client id in memory for the specified user id", async () => { + it("sets the client id in memory when given a user id", async () => { // Act await tokenService.setClientId( clientId, @@ -1504,7 +1881,7 @@ describe("TokenService", () => { }); describe("Disk storage tests", () => { - it("should set the client id in disk when there is an active user in global state", async () => { + it("sets the client id in disk when there is an active user in global state", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -1519,7 +1896,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(clientId); }); - it("should set the client id in disk for the specified user id", async () => { + it("sets the client id on disk when given a user id", async () => { // Act await tokenService.setClientId( clientId, @@ -1537,14 +1914,14 @@ describe("TokenService", () => { }); describe("getClientId", () => { - it("should return undefined if no user id is provided and there is no active user in global state", async () => { + it("returns undefined when no user id is provided and there is no active user in global state", async () => { // Act const result = await tokenService.getClientId(); // Assert expect(result).toBeUndefined(); }); - it("should return null if no client id is found in memory or disk", async () => { + it("returns null when no client id is found in memory or disk", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -1557,7 +1934,7 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should get the client id from memory with no user id specified (uses global active user)", async () => { + it("gets the client id from memory when no user id is specified (uses global active user)", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) @@ -1580,7 +1957,7 @@ describe("TokenService", () => { expect(result).toEqual(clientId); }); - it("should get the client id from memory for the specified user id", async () => { + it("gets the client id from memory when given a user id", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) @@ -1599,7 +1976,7 @@ describe("TokenService", () => { }); describe("Disk storage tests", () => { - it("should get the client id from disk with no user id specified", async () => { + it("gets the client id from disk when no user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) @@ -1620,7 +1997,7 @@ describe("TokenService", () => { expect(result).toEqual(clientId); }); - it("should get the client id from disk for the specified user id", async () => { + it("gets the client id from disk when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) @@ -1639,7 +2016,7 @@ describe("TokenService", () => { }); describe("clearClientId", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { + it("throws an error when no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error const result = (tokenService as any).clearClientId(); @@ -1647,7 +2024,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot clear client id."); }); - it("should clear the client id from memory and disk for the specified user id", async () => { + it("clears the client id from memory and disk when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) @@ -1669,7 +2046,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(null); }); - it("should clear the client id from memory and disk for the global active user", async () => { + it("clears the client id from memory and disk when there is a global active user", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY) @@ -1702,7 +2079,7 @@ describe("TokenService", () => { const clientSecret = "clientSecret"; describe("setClientSecret", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { + it("throws an error when no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error const result = tokenService.setClientSecret( @@ -1747,7 +2124,7 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should set the client secret in memory when there is an active user in global state", async () => { + it("sets the client secret in memory when there is an active user in global state", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -1767,7 +2144,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(clientSecret); }); - it("should set the client secret in memory for the specified user id", async () => { + it("sets the client secret in memory when a user id is specified", async () => { // Act await tokenService.setClientSecret( clientSecret, @@ -1785,7 +2162,7 @@ describe("TokenService", () => { }); describe("Disk storage tests", () => { - it("should set the client secret in disk when there is an active user in global state", async () => { + it("sets the client secret on disk when there is an active user in global state", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -1805,7 +2182,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(clientSecret); }); - it("should set the client secret in disk for the specified user id", async () => { + it("sets the client secret on disk when a user id is specified", async () => { // Act await tokenService.setClientSecret( clientSecret, @@ -1824,14 +2201,14 @@ describe("TokenService", () => { }); describe("getClientSecret", () => { - it("should return undefined if no user id is provided and there is no active user in global state", async () => { + it("returns undefined when no user id is provided and there is no active user in global state", async () => { // Act const result = await tokenService.getClientSecret(); // Assert expect(result).toBeUndefined(); }); - it("should return null if no client secret is found in memory or disk", async () => { + it("returns null when no client secret is found in memory or disk", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -1844,7 +2221,7 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should get the client secret from memory with no user id specified (uses global active user)", async () => { + it("gets the client secret from memory when no user id is specified (uses global active user)", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) @@ -1867,7 +2244,7 @@ describe("TokenService", () => { expect(result).toEqual(clientSecret); }); - it("should get the client secret from memory for the specified user id", async () => { + it("gets the client secret from memory when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) @@ -1886,7 +2263,7 @@ describe("TokenService", () => { }); describe("Disk storage tests", () => { - it("should get the client secret from disk with no user id specified", async () => { + it("gets the client secret from disk when no user id specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) @@ -1907,7 +2284,7 @@ describe("TokenService", () => { expect(result).toEqual(clientSecret); }); - it("should get the client secret from disk for the specified user id", async () => { + it("gets the client secret from disk when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) @@ -1926,7 +2303,7 @@ describe("TokenService", () => { }); describe("clearClientSecret", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { + it("throws an error when no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error const result = (tokenService as any).clearClientSecret(); @@ -1934,7 +2311,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot clear client secret."); }); - it("should clear the client secret from memory and disk for the specified user id", async () => { + it("clears the client secret from memory and disk when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) @@ -1958,7 +2335,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(null); }); - it("should clear the client secret from memory and disk for the global active user", async () => { + it("clears the client secret from memory and disk when there is a global active user", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY) @@ -1990,7 +2367,7 @@ describe("TokenService", () => { }); describe("setTokens", () => { - it("should call to set all passed in tokens after deriving user id from the access token", async () => { + it("calls to set all tokens after deriving user id from the access token when called with valid params", async () => { // Arrange const refreshToken = "refreshToken"; // specific vault timeout actions and vault timeouts don't change this test so values don't matter. @@ -2042,7 +2419,7 @@ describe("TokenService", () => { ); }); - it("should not try to set client id and client secret if they are not passed in", async () => { + it("does not try to set client id and client secret when they are not passed in", async () => { // Arrange const refreshToken = "refreshToken"; const vaultTimeoutAction = VaultTimeoutAction.Lock; @@ -2076,7 +2453,7 @@ describe("TokenService", () => { expect(tokenService.setClientSecret).not.toHaveBeenCalled(); }); - it("should throw an error if the access token is invalid", async () => { + it("throws an error when the access token is invalid", async () => { // Arrange const accessToken = "invalidToken"; const refreshToken = "refreshToken"; @@ -2095,7 +2472,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("JWT must have 3 parts"); }); - it("should throw an error if the access token is missing", async () => { + it("throws an error when the access token is missing", async () => { // Arrange const accessToken: string = null; const refreshToken = "refreshToken"; @@ -2150,7 +2527,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("Vault Timeout Action is required."); }); - it("should not throw an error if the refresh token is missing and it should just not set it", async () => { + it("does not throw an error or set the refresh token when the refresh token is missing", async () => { // Arrange const refreshToken: string = null; const vaultTimeoutAction = VaultTimeoutAction.Lock; @@ -2166,7 +2543,7 @@ describe("TokenService", () => { }); describe("clearTokens", () => { - it("should call to clear all tokens for the specified user id", async () => { + it("calls to clear all tokens when given a specified user id", async () => { // Arrange const userId = "userId" as UserId; @@ -2187,7 +2564,7 @@ describe("TokenService", () => { expect((tokenService as any).clearClientSecret).toHaveBeenCalledWith(userId); }); - it("should call to clear all tokens for the active user id", async () => { + it("calls to clear all tokens when there is an active user", async () => { // Arrange const userId = "userId" as UserId; @@ -2210,7 +2587,7 @@ describe("TokenService", () => { expect((tokenService as any).clearClientSecret).toHaveBeenCalledWith(userId); }); - it("should not call to clear all tokens if no user id is provided and there is no active user in global state", async () => { + it("does not call to clear all tokens when no user id is provided and there is no active user in global state", async () => { // Arrange tokenService.clearAccessToken = jest.fn(); (tokenService as any).clearRefreshToken = jest.fn(); @@ -2228,7 +2605,7 @@ describe("TokenService", () => { describe("Two Factor Token methods", () => { describe("setTwoFactorToken", () => { - it("should set the email and two factor token when there hasn't been a previous record (initializing the record)", async () => { + it("sets the email and two factor token when there hasn't been a previous record (initializing the record)", async () => { // Arrange const email = "testUser@email.com"; const twoFactorToken = "twoFactorTokenForTestUser"; @@ -2240,7 +2617,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith({ [email]: twoFactorToken }); }); - it("should set the email and two factor token when there is an initialized value already (updating the existing record)", async () => { + it("sets the email and two factor token when there is an initialized value already (updating the existing record)", async () => { // Arrange const email = "testUser@email.com"; const twoFactorToken = "twoFactorTokenForTestUser"; @@ -2263,7 +2640,7 @@ describe("TokenService", () => { }); describe("getTwoFactorToken", () => { - it("should return the two factor token for the given email", async () => { + it("returns the two factor token when given an email", async () => { // Arrange const email = "testUser"; const twoFactorToken = "twoFactorTokenForTestUser"; @@ -2282,7 +2659,7 @@ describe("TokenService", () => { expect(result).toEqual(twoFactorToken); }); - it("should not return the two factor token for an email that doesn't exist", async () => { + it("does not return the two factor token when given an email that doesn't exist", async () => { // Arrange const email = "testUser"; const initialTwoFactorTokenRecord: Record = { @@ -2300,7 +2677,7 @@ describe("TokenService", () => { expect(result).toEqual(undefined); }); - it("should return null if there is no two factor token record", async () => { + it("returns null when there is no two factor token record", async () => { // Arrange globalStateProvider .getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL) @@ -2315,7 +2692,7 @@ describe("TokenService", () => { }); describe("clearTwoFactorToken", () => { - it("should clear the two factor token for the given email when a record exists", async () => { + it("clears the two factor token for the given email when a record exists", async () => { // Arrange const email = "testUser"; const twoFactorToken = "twoFactorTokenForTestUser"; @@ -2336,7 +2713,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith({}); }); - it("should initialize the record if it doesn't exist and delete the value", async () => { + it("initializes the record and deletes the value when the record doesn't exist", async () => { // Arrange const email = "testUser"; @@ -2355,7 +2732,7 @@ describe("TokenService", () => { const mockSecurityStamp = "securityStamp"; describe("setSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { + it("throws an error deletes the value no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error const result = tokenService.setSecurityStamp(mockSecurityStamp); @@ -2363,7 +2740,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); }); - it("should set the security stamp in memory when there is an active user in global state", async () => { + it("sets the security stamp in memory when there is an active user in global state", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -2378,7 +2755,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(mockSecurityStamp); }); - it("should set the security stamp in memory for the specified user id", async () => { + it("sets the security stamp in memory when a user id is specified", async () => { // Act await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); @@ -2390,7 +2767,7 @@ describe("TokenService", () => { }); describe("getSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { + it("throws an error when no user id is provided and there is no active user in global state", async () => { // Act // note: don't await here because we want to test the error const result = tokenService.getSecurityStamp(); @@ -2398,7 +2775,7 @@ describe("TokenService", () => { await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); }); - it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { + it("returns the security stamp from memory when no user id is specified (uses global active user)", async () => { // Arrange globalStateProvider .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) @@ -2415,7 +2792,7 @@ describe("TokenService", () => { expect(result).toEqual(mockSecurityStamp); }); - it("should return the security stamp from memory for the specified user id", async () => { + it("returns the security stamp from memory when a user id is specified", async () => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) @@ -2601,6 +2978,7 @@ describe("TokenService", () => { keyGenerationService, encryptService, logService, + logoutCallback, ); } }); diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 203d95429ee..38d0a77b52f 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -1,7 +1,7 @@ import { Observable, combineLatest, firstValueFrom, map } from "rxjs"; import { Opaque } from "type-fest"; -import { decodeJwtTokenToJson } from "@bitwarden/auth/common"; +import { LogoutReason, decodeJwtTokenToJson } from "@bitwarden/auth/common"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; @@ -111,7 +111,7 @@ export type DecodedAccessToken = { * A symmetric key for encrypting the access token before the token is stored on disk. * This key should be stored in secure storage. * */ -type AccessTokenKey = Opaque; +export type AccessTokenKey = Opaque; export class TokenService implements TokenServiceAbstraction { private readonly accessTokenKeySecureStorageKey: string = "_accessTokenKey"; @@ -132,6 +132,7 @@ export class TokenService implements TokenServiceAbstraction { private keyGenerationService: KeyGenerationService, private encryptService: EncryptService, private logService: LogService, + private logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise, ) { this.initializeState(); } @@ -145,10 +146,6 @@ export class TokenService implements TokenServiceAbstraction { ]).pipe(map(([disk, memory]) => Boolean(disk || memory))); } - // pivoting to an approach where we create a symmetric key we store in secure storage - // which is used to protect the data before persisting to disk. - // We will also use the same symmetric key to decrypt the data when reading from disk. - private initializeState(): void { this.emailTwoFactorTokenRecordGlobalState = this.globalStateProvider.get( EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, @@ -218,6 +215,14 @@ export class TokenService implements TokenServiceAbstraction { this.getSecureStorageOptions(userId), ); + // We are having intermittent issues with access token keys not saving into secure storage on windows 10/11. + // So, let's add a check to ensure we can read the value after writing it. + const accessTokenKey = await this.getAccessTokenKey(userId); + + if (!accessTokenKey) { + throw new Error("New Access token key unable to be retrieved from secure storage."); + } + return newAccessTokenKey; } @@ -238,6 +243,8 @@ export class TokenService implements TokenServiceAbstraction { } // First see if we have an accessTokenKey in secure storage and return it if we do + // Note: retrieving/saving data from/to secure storage on linux will throw if the + // distro doesn't have a secure storage provider let accessTokenKey: AccessTokenKey = await this.getAccessTokenKey(userId); if (!accessTokenKey) { @@ -255,15 +262,13 @@ export class TokenService implements TokenServiceAbstraction { } private async decryptAccessToken( + accessTokenKey: AccessTokenKey, encryptedAccessToken: EncString, - userId: UserId, ): Promise { - const accessTokenKey = await this.getAccessTokenKey(userId); - if (!accessTokenKey) { - // If we don't have an accessTokenKey, then that means we don't have an access token as it hasn't been set yet - // and we have to return null here to properly indicate the user isn't logged in. - return null; + throw new Error( + "decryptAccessToken: Access token key required. Cannot decrypt access token.", + ); } const decryptedAccessToken = await this.encryptService.decryptToUtf8( @@ -297,17 +302,32 @@ export class TokenService implements TokenServiceAbstraction { // store the access token directly. Instead, we encrypt with accessTokenKey and store that // in secure storage. - const encryptedAccessToken: EncString = await this.encryptAccessToken(accessToken, userId); + try { + const encryptedAccessToken: EncString = await this.encryptAccessToken( + accessToken, + userId, + ); - // Save the encrypted access token to disk - await this.singleUserStateProvider - .get(userId, ACCESS_TOKEN_DISK) - .update((_) => encryptedAccessToken.encryptedString); + // Save the encrypted access token to disk + await this.singleUserStateProvider + .get(userId, ACCESS_TOKEN_DISK) + .update((_) => encryptedAccessToken.encryptedString); - // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 - // 2024-02-20: Remove access token from memory so that we migrate to encrypt the access token over time. - // Remove this call to remove the access token from memory after 3 releases. - await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); + // TODO: PM-6408 + // 2024-02-20: Remove access token from memory so that we migrate to encrypt the access token over time. + // Remove this call to remove the access token from memory after 3 months. + await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); + } catch (error) { + this.logService.error( + `SetAccessToken: storing encrypted access token in secure storage failed. Falling back to disk storage.`, + error, + ); + + // Fall back to disk storage for unecrypted access token + await this.singleUserStateProvider + .get(userId, ACCESS_TOKEN_DISK) + .update((_) => accessToken); + } return; } @@ -376,11 +396,11 @@ export class TokenService implements TokenServiceAbstraction { await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); } - async getAccessToken(userId?: UserId): Promise { + async getAccessToken(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { - return undefined; + return null; } // Try to get the access token from memory @@ -399,10 +419,41 @@ export class TokenService implements TokenServiceAbstraction { } if (this.platformSupportsSecureStorage) { - const accessTokenKey = await this.getAccessTokenKey(userId); + let accessTokenKey: AccessTokenKey; + try { + accessTokenKey = await this.getAccessTokenKey(userId); + } catch (error) { + if (EncString.isSerializedEncString(accessTokenDisk)) { + this.logService.error( + "Access token key retrieval failed. Unable to decrypt encrypted access token. Logging user out.", + error, + ); + await this.logoutCallback("accessTokenUnableToBeDecrypted", userId); + return null; + } + + // If the access token key is not found, but the access token is unencrypted then + // this indicates that this is the pre-migration state where the access token + // was stored unencrypted on disk. We can return the access token as is. + // Note: this is likely to only be hit for linux users who don't + // have a secure storage provider configured. + return accessTokenDisk; + } if (!accessTokenKey) { - // We know this is an unencrypted access token because we don't have an access token key + if (EncString.isSerializedEncString(accessTokenDisk)) { + // The access token is encrypted but we don't have the key to decrypt it for + // whatever reason so we have to log the user out. + this.logService.error( + "Access token key not found to decrypt encrypted access token. Logging user out.", + ); + + await this.logoutCallback("accessTokenUnableToBeDecrypted", userId); + + return null; + } + + // We know this is an unencrypted access token return accessTokenDisk; } @@ -410,17 +461,18 @@ export class TokenService implements TokenServiceAbstraction { const encryptedAccessTokenEncString = new EncString(accessTokenDisk as EncryptedString); const decryptedAccessToken = await this.decryptAccessToken( + accessTokenKey, encryptedAccessTokenEncString, - userId, ); return decryptedAccessToken; } catch (error) { - // If an error occurs during decryption, return null for logout. + // If an error occurs during decryption, logout and then return null. // We don't try to recover here since we'd like to know // if access token and key are getting out of sync. - this.logService.error( - `Failed to decrypt access token: ${error?.message ?? "Unknown error."}`, - ); + this.logService.error(`Failed to decrypt access token`, error); + + await this.logoutCallback("accessTokenUnableToBeDecrypted", userId); + return null; } } @@ -456,21 +508,49 @@ export class TokenService implements TokenServiceAbstraction { ); switch (storageLocation) { - case TokenStorageLocation.SecureStorage: - await this.saveStringToSecureStorage( - userId, - this.refreshTokenSecureStorageKey, - refreshToken, - ); + case TokenStorageLocation.SecureStorage: { + try { + await this.saveStringToSecureStorage( + userId, + this.refreshTokenSecureStorageKey, + refreshToken, + ); - // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 - // 2024-02-20: Remove refresh token from memory and disk so that we migrate to secure storage over time. - // Remove these 2 calls to remove the refresh token from memory and disk after 3 releases. - await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); - await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null); + // Check if the refresh token was able to be saved to secure storage by reading it + // immediately after setting it. This is needed due to intermittent silent failures on Windows 10/11. + const refreshTokenSecureStorage = await this.getStringFromSecureStorage( + userId, + this.refreshTokenSecureStorageKey, + ); + + // Only throw if the refresh token was not saved to secure storage + // If we only check for a nullish value out of secure storage without considering the input value, + // then we would end up falling back to disk storage if the input value was null. + if (refreshToken !== null && !refreshTokenSecureStorage) { + throw new Error("Refresh token failed to save to secure storage."); + } + + // TODO: PM-6408 + // 2024-02-20: Remove refresh token from memory and disk so that we migrate to secure storage over time. + // Remove these 2 calls to remove the refresh token from memory and disk after 3 months. + await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); + await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null); + } catch (error) { + // This case could be hit for both Linux users who don't have secure storage configured + // or for Windows users who have intermittent issues with secure storage. + this.logService.error( + `SetRefreshToken: storing refresh token in secure storage failed. Falling back to disk storage.`, + error, + ); + + // Fall back to disk storage for refresh token + await this.singleUserStateProvider + .get(userId, REFRESH_TOKEN_DISK) + .update((_) => refreshToken); + } return; - + } case TokenStorageLocation.Disk: await this.singleUserStateProvider .get(userId, REFRESH_TOKEN_DISK) @@ -485,11 +565,11 @@ export class TokenService implements TokenServiceAbstraction { } } - async getRefreshToken(userId?: UserId): Promise { + async getRefreshToken(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { - return undefined; + return null; } // pre-secure storage migration: @@ -507,17 +587,30 @@ export class TokenService implements TokenServiceAbstraction { const refreshTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, REFRESH_TOKEN_DISK); if (refreshTokenDisk != null) { + // This handles the scenario pre-secure storage migration where the refresh token was stored on disk. return refreshTokenDisk; } if (this.platformSupportsSecureStorage) { - const refreshTokenSecureStorage = await this.getStringFromSecureStorage( - userId, - this.refreshTokenSecureStorageKey, - ); + try { + const refreshTokenSecureStorage = await this.getStringFromSecureStorage( + userId, + this.refreshTokenSecureStorageKey, + ); - if (refreshTokenSecureStorage != null) { - return refreshTokenSecureStorage; + if (refreshTokenSecureStorage != null) { + return refreshTokenSecureStorage; + } + + this.logService.error( + "Refresh token not found in secure storage. Access token will fail to refresh upon expiration or manual refresh.", + ); + } catch (error) { + // This case will be hit for Linux users who don't have secure storage configured. + + this.logService.error(`Failed to retrieve refresh token from secure storage`, error); + + await this.logoutCallback("refreshTokenSecureStorageRetrievalFailure", userId); } } diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 7561023a277..85640519ec3 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -140,6 +140,10 @@ export class UserVerificationService implements UserVerificationServiceAbstracti * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) */ async verifyUser(verification: Verification): Promise { + if (verification == null) { + throw new Error("Verification is required."); + } + const [userId, email] = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])), ); diff --git a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts index 063b3c370b0..117b318768e 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,3 +1,9 @@ +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { TokenizedPaymentMethodRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-method.request"; +import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { PaymentInformationResponse } from "@bitwarden/common/billing/models/response/payment-information.response"; + import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; @@ -13,23 +19,50 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: SubscriptionCancellationRequest, ) => Promise; + cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; + createClientOrganization: ( providerId: string, request: CreateClientOrganizationRequest, ) => Promise; + + createSetupIntent: (paymentMethodType: PaymentMethodType) => Promise; + getBillingStatus: (id: string) => Promise; + getOrganizationBillingMetadata: ( organizationId: string, ) => Promise; + getOrganizationSubscription: ( organizationId: string, ) => Promise; + getPlans: () => Promise>; + + getProviderPaymentInformation: (providerId: string) => Promise; + getProviderSubscription: (providerId: string) => Promise; + updateClientOrganization: ( providerId: string, organizationId: string, request: UpdateClientOrganizationRequest, ) => Promise; + + updateProviderPaymentMethod: ( + providerId: string, + request: TokenizedPaymentMethodRequest, + ) => Promise; + + updateProviderTaxInformation: ( + providerId: string, + request: ExpandedTaxInfoUpdateRequest, + ) => Promise; + + verifyProviderBankAccount: ( + providerId: string, + request: VerifyBankAccountRequest, + ) => Promise; } diff --git a/libs/common/src/billing/abstractions/index.ts b/libs/common/src/billing/abstractions/index.ts new file mode 100644 index 00000000000..08a7a28fd9c --- /dev/null +++ b/libs/common/src/billing/abstractions/index.ts @@ -0,0 +1,7 @@ +export * from "./account/billing-account-profile-state.service"; +export * from "./billilng-api.service.abstraction"; +export * from "./organization-billing.service"; +export * from "./payment-method-warnings-service.abstraction"; +export * from "./payment-processors/braintree.service.abstraction"; +export * from "./payment-processors/stripe.service.abstraction"; +export * from "./provider-billing.service.abstraction"; diff --git a/libs/common/src/billing/abstractions/payment-processors/braintree.service.abstraction.ts b/libs/common/src/billing/abstractions/payment-processors/braintree.service.abstraction.ts new file mode 100644 index 00000000000..9391ab25f54 --- /dev/null +++ b/libs/common/src/billing/abstractions/payment-processors/braintree.service.abstraction.ts @@ -0,0 +1,28 @@ +export abstract class BraintreeServiceAbstraction { + /** + * Utilizes the Braintree SDK to create a [Braintree drop-in]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html} instance attached to the container ID specified as part of the {@link loadBraintree} method. + */ + createDropin: () => void; + + /** + * Loads the Bitwarden dropin.js script in the element of the current page. + * This script attaches the Braintree SDK to the window. + * @param containerId - The ID of the HTML element where the Braintree drop-in will be loaded at. + * @param autoCreateDropin - Specifies whether the Braintree drop-in should be created when dropin.js loads. + */ + loadBraintree: (containerId: string, autoCreateDropin: boolean) => void; + + /** + * Invokes the Braintree [requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} method + * in order to generate a payment method token using the active Braintree drop-in. + */ + requestPaymentMethod: () => Promise; + + /** + * Removes the following elements from the of the current page: + * - The Bitwarden dropin.js script + * - Any