diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 17a1cb5720e..db60ad6a93b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -91,6 +91,7 @@ libs/common/spec @bitwarden/team-platform-dev libs/common/src/state-migrations @bitwarden/team-platform-dev libs/platform @bitwarden/team-platform-dev libs/storage-core @bitwarden/team-platform-dev +libs/storage-test-utils @bitwarden/team-platform-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 45d57bbe202..ac314a4c33a 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -71,7 +71,6 @@ jobs: - name: Get Node Version id: retrieve-node-version - working-directory: ./ run: | NODE_NVMRC=$(cat .nvmrc) NODE_VERSION=${NODE_NVMRC/v/''} diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 31a16dc9a6d..2d7be2e186e 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -55,6 +55,8 @@ jobs: name: Release runs-on: ubuntu-22.04 needs: setup + permissions: + contents: write steps: - name: Download all Release artifacts if: ${{ inputs.release_type != 'Dry Run' }} diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index b3c3fe5d250..5ce0da4cb4b 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -24,6 +24,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + permissions: + contents: write outputs: release_version: ${{ steps.version.outputs.version }} release_channel: ${{ steps.release_channel.outputs.channel }} diff --git a/.nvmrc b/.nvmrc index 9a2a0e219c9..53d1c14db37 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 85a535f2476..6c7f7f0fccd 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "لقد حالت سياسة المؤسسة دون استيراد العناصر إلى خزانتك الشخصية." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "النطاقات", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 5da9db4359c..ddf82f37d2f 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Bir təşkilat siyasəti, elementlərin fərdi seyfinizə köçürülməsini əngəllədi." }, + "restrictCardTypeImport": { + "message": "Kart element növləri daxilə köçürülə bilmir" + }, + "restrictCardTypeImportDesc": { + "message": "1 və ya daha çox təşkilat tərəfindən təyin edilən bir siyasət, kartların seyfinizə köçürülməsini əngəlləyir." + }, "domainsTitle": { "message": "Domenlər", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "PIN ilə kilid açma təyini" }, - "unlockBiometricSet": { - "message": "Biometrik ilə kilidi aç ayarı" + "unlockWithBiometricSet": { + "message": "Kilidi biometriklə aç ayarı" }, "authenticating": { "message": "Kimlik doğrulama" diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 7ec00e7432d..3944569df94 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Дамены", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 32c566db779..53e896e6a72 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Политика на организацията забранява да внасяте елементи в личния си трезор." }, + "restrictCardTypeImport": { + "message": "Картовите елементи не могат да бъдат внесени" + }, + "restrictCardTypeImportDesc": { + "message": "Политика, зададена от 1 или повече организации, не позволява да внасяте карти в трезорите си." + }, "domainsTitle": { "message": "Домейни", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Зададен е ПИН код за отключване" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Отключване с биометричен набор" }, "authenticating": { "message": "Удостоверяване" diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 4eb485f1861..29e0e3800c8 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index b1a2cfc3f6d..7c575cdb72b 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index c67496fef54..ff3226255e3 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Una política d'organització ha bloquejat la importació d'elements a la vostra caixa forta individual." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Dominis", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Desbloqueja amb conjunt biomètric" }, "authenticating": { "message": "S'està autenticant" diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 2679ab063af..52c548a93b0 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Zásady organizace zablokovaly importování položek do Vašeho osobního trezoru." }, + "restrictCardTypeImport": { + "message": "Nelze importovat typy položek karty" + }, + "restrictCardTypeImportDesc": { + "message": "Zásady nastavené 1 nebo více organizací Vám brání v importu karet do Vašeho trezoru." + }, "domainsTitle": { "message": "Domény", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "PIN pro odemknutí byl nastaven" }, - "unlockBiometricSet": { - "message": "Odemknout sadu biometriky" + "unlockWithBiometricSet": { + "message": "Odemknout pomocí biometrie" }, "authenticating": { "message": "Ověřování" diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 9940ed173ed..c060d81bcfc 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index d6680f4190a..1b79fc0ecf9 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "En organisationspolitik hindrer import af emner til den individuelle boks." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domæner", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Godkender" diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 8449eb7b966..418c15dc3a0 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1923,7 +1923,7 @@ "message": "SSH-Schlüssel" }, "typeNote": { - "message": "Note" + "message": "Notiz" }, "newItemHeader": { "message": "Neue $TYPE$", @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Eine Organisationsrichtlinie hat das Importieren von Einträgen in deinen persönlichen Tresor deaktiviert." }, + "restrictCardTypeImport": { + "message": "Karten-Eintragstypen können nicht importiert werden" + }, + "restrictCardTypeImportDesc": { + "message": "Eine von einer oder mehreren Organisationen festgelegte Richtlinie verhindert, dass du Karten in deinen Tresor importieren kannst." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Entsperr-PIN festgelegt" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Mit Biometrie entsperren eingerichtet" }, "authenticating": { "message": "Authentifizierung" @@ -5411,7 +5417,7 @@ "message": "Du hast keine Berechtigung, diese Seite anzuzeigen. Versuche dich mit einem anderen Konto anzumelden." }, "wasmNotSupported": { - "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "message": "WebAssembly wird von deinem Browser nicht unterstützt oder ist nicht aktiviert. WebAssembly wird benötigt, um die Bitwarden-App nutzen zu können.", "description": "'WebAssembly' is a technical term and should not be translated." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 9d023585108..acd7cac0c9b 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Μια οργανωτική πολιτική έχει αποτρέψει την εισαγωγή στοιχείων στο προσωπικό θησαυ/κιο σας." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Τομείς", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Ταυτοποίηση" diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index b6a8d1834b4..ce5787c46bd 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2919,6 +2919,9 @@ "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Updated master password" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 00488aaf275..ffca2486ff4 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organisation policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organisations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 796a51f8bba..02d2ba35060 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organisation policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organisations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 40e20fa2d5e..250aa3430e0 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -887,7 +887,7 @@ "message": "Sigue los pasos de abajo para terminar de iniciar sesión." }, "followTheStepsBelowToFinishLoggingInWithSecurityKey": { - "message": "Follow the steps below to finish logging in with your security key." + "message": "Sigue los siguientes pasos de abajo para terminar de iniciar sesión con tu clave de seguridad." }, "restartRegistration": { "message": "Reiniciar registro" @@ -1076,14 +1076,14 @@ "description": "Aria label for the new item button in notification bar confirmation message when error is prompted" }, "notificationEditTooltip": { - "message": "Edit before saving", + "message": "Editar antes de guardar", "description": "Tooltip and Aria label for edit button on cipher item" }, "newNotification": { "message": "Nueva notificación" }, "labelWithNotification": { - "message": "$LABEL$: New notification", + "message": "$LABEL$: Nueva notificación", "description": "Label for the notification with a new login suggestion.", "placeholders": { "label": { @@ -1093,15 +1093,15 @@ } }, "notificationLoginSaveConfirmation": { - "message": "saved to Bitwarden.", + "message": "guardado en Bitwarden.", "description": "Shown to user after item is saved." }, "notificationLoginUpdatedConfirmation": { - "message": "updated in Bitwarden.", + "message": "actualizado en Bitwarden.", "description": "Shown to user after item is updated." }, "selectItemAriaLabel": { - "message": "Select $ITEMTYPE$, $ITEMNAME$", + "message": "Seleccionar $ITEMTYPE$, $ITEMNAME$", "description": "Used by screen readers. $1 is the item type (like vault or folder), $2 is the selected item name.", "placeholders": { "itemType": { @@ -1113,27 +1113,27 @@ } }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "Guardar como nuevo inicio de sesión", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "Actualizar inicio de sesión", "description": "Button text for updating an existing login entry." }, "unlockToSave": { - "message": "Unlock to save this login", + "message": "Desbloquea para guardar este inicio de sesión", "description": "User prompt to take action in order to save the login they just entered." }, "saveLogin": { - "message": "Save login", + "message": "Guardar inicio de sesión", "description": "Prompt asking the user if they want to save their login details." }, "updateLogin": { - "message": "Update existing login", + "message": "Actualizar inicio de sesión existente", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "Inicio de sesión guardado", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { @@ -1141,7 +1141,7 @@ "description": "Message displayed when login details are successfully updated." }, "loginUpdateTaskSuccess": { - "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "message": "¡Buen trabajo! Has dado los pasos para que tú y $ORGANIZATION$ seáis más seguros.", "placeholders": { "organization": { "content": "$1" @@ -1150,7 +1150,7 @@ "description": "Shown to user after login is updated." }, "loginUpdateTaskSuccessAdditional": { - "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "message": "Gracias por hacer $ORGANIZATION$ más seguro. Tienes $TASK_COUNT$ contraseñas más que actualizar.", "placeholders": { "organization": { "content": "$1" @@ -1162,7 +1162,7 @@ "description": "Shown to user after login is updated." }, "nextSecurityTaskAction": { - "message": "Change next password", + "message": "Cambiar siguiente contraseña", "description": "Message prompting user to undertake completion of another security task." }, "saveFailure": { @@ -1170,7 +1170,7 @@ "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "¡Oh no! No pudimos guardar esto. Intenta introducir los datos manualmente.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -1366,7 +1366,7 @@ "message": "Característica no disponible" }, "legacyEncryptionUnsupported": { - "message": "Legacy encryption is no longer supported. Please contact support to recover your account." + "message": "La encriptación antigua ya no está soportada. Por favor, contacta con soporte para recuperar tu cuenta." }, "premiumMembership": { "message": "Membresía Premium" @@ -1411,7 +1411,7 @@ "message": "Comprar Premium" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "Puedes comprar el Premium desde los ajustes de tu cuenta en la aplicación web de Bitwarden." }, "premiumCurrentMember": { "message": "¡Eres un miembro Premium!" @@ -1474,14 +1474,14 @@ } }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "No volver a preguntar en este dispositivo durante 30 días" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "Selecciona otro método", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "Usa tu código de recuperación" }, "insertU2f": { "message": "Inserta tu llave de seguridad en el puerto USB de tu equipo. Si tiene un botón, púlsalo." @@ -1493,10 +1493,10 @@ "message": "Autenticar WebAuthn" }, "readSecurityKey": { - "message": "Read security key" + "message": "Leer clave de seguridad" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "Esperando interacción de la clave de seguridad..." }, "loginUnavailable": { "message": "Entrada no disponible" @@ -1511,7 +1511,7 @@ "message": "Opciones de la autenticación en dos pasos" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "Selecciona un método de inicio de sesión en dos pasos" }, "recoveryCodeDesc": { "message": "¿Has perdido el acceso a todos tus métodos de autenticación en dos pasos? Utiliza tu código de recuperación para deshabilitar todos los métodos de autenticación en dos pasos de tu cuenta." @@ -1600,13 +1600,13 @@ "message": "Sugerencias de autocompletar" }, "autofillSpotlightTitle": { - "message": "Easily find autofill suggestions" + "message": "Encuentra fácilmente sugerencias de autocompletado" }, "autofillSpotlightDesc": { - "message": "Turn off your browser's autofill settings, so they don't conflict with Bitwarden." + "message": "Desactiva los ajustes de autocompletado de tu navegador para que no entren en conflicto con Bitwarden." }, "turnOffBrowserAutofill": { - "message": "Turn off $BROWSER$ autofill", + "message": "Desactivar autocompletado de $BROWSER$", "placeholders": { "browser": { "content": "$1", @@ -1615,22 +1615,22 @@ } }, "turnOffAutofill": { - "message": "Turn off autofill" + "message": "Desactivar autocompletado" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Mostrar sugerencias de autocompletado en campos de formulario" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "Mostrar identidades como sugerencias" }, "showInlineMenuCardsLabel": { "message": "Mostrar tarjetas como sugerencias" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Mostrar sugerencias cuando el icono esté seleccionado" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "Se aplica a todas las cuentas a las que se haya iniciado sesión." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Desactive la configuración del gestor de contraseñas del navegador para evitar conflictos." @@ -1693,13 +1693,13 @@ "message": "Abrir caja fuerte en la barra lateral" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Autocompletar el último inicio de sesión usado para el sitio web actual" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Autocompletar la última tarjeta usada para el sitio web actual" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Autocompletar la última identidad usada para el sitio web actual" }, "commandGeneratePasswordDesc": { "message": "Generar y copiar una nueva contraseña aleatoria al portapapeles." @@ -1923,7 +1923,7 @@ "message": "Llave SSH" }, "typeNote": { - "message": "Note" + "message": "Nota" }, "newItemHeader": { "message": "Nuevo $TYPE$", @@ -1962,7 +1962,7 @@ "message": "Borrar historial del generador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Si continúas, todas las entradas se eliminarán permanentemente del historial del generador. ¿Estás seguro de que quieres continuar?" }, "back": { "message": "Atrás" @@ -2027,7 +2027,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Dominio base (recomendado)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -2157,7 +2157,7 @@ "message": "Establece tu código PIN para desbloquear Bitwarden. Tus ajustes de PIN se reiniciarán si alguna vez cierras tu sesión completamente de la aplicación." }, "setPinCode": { - "message": "You can use this PIN to unlock Bitwarden. Your PIN will be reset if you ever fully log out of the application." + "message": "Puedes usar este PIN para desbloquear Bitwarden. Tu PIN se reiniciará si alguna vez cierras la sesión de la aplicación por completo." }, "pinRequired": { "message": "Código PIN requerido." @@ -2208,7 +2208,7 @@ "message": "Usar esta contraseña" }, "useThisPassphrase": { - "message": "Use this passphrase" + "message": "Usar esta frase de contraseña" }, "useThisUsername": { "message": "Usar este nombre de usuario" @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Una política organizacional ha bloqueado la importación de elementos a su caja fuerte personal." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "Una política establecida en 1 o más organizaciones te impide importar tarjetas a tus cajas fuertes." + }, "domainsTitle": { "message": "Dominios", "description": "A category title describing the concept of web domains" @@ -2532,7 +2538,7 @@ } }, "atRiskPassword": { - "message": "At-risk password" + "message": "Contraseña en riesgo" }, "atRiskPasswords": { "message": "Contraseñas de riesgo" @@ -2560,7 +2566,7 @@ } }, "atRiskPasswordsDescMultiOrgPlural": { - "message": "Your organizations are requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "Tus organizaciones te están solicitando que cambies las $COUNT$ contraseñas porque están en riesgo.", "placeholders": { "count": { "content": "$1", @@ -2569,7 +2575,7 @@ } }, "atRiskChangePrompt": { - "message": "Your password for this site is at-risk. $ORGANIZATION$ has requested that you change it.", + "message": "La contraseña para este sitio está en riesgo. $ORGANIZATION$ ha solicitado que la cambies.", "placeholders": { "organization": { "content": "$1", @@ -2579,7 +2585,7 @@ "description": "Notification body when a login triggers an at-risk password change request and the change password domain is known." }, "atRiskNavigatePrompt": { - "message": "$ORGANIZATION$ wants you to change this password because it is at-risk. Navigate to your account settings to change the password.", + "message": "$ORGANIZATION$ quiere que cambies esta contraseña porque está en riesgo. Navega a los ajustes de tu cuenta para cambiar la contraseña.", "placeholders": { "organization": { "content": "$1", @@ -2589,10 +2595,10 @@ "description": "Notification body when a login triggers an at-risk password change request and no change password domain is provided." }, "reviewAndChangeAtRiskPassword": { - "message": "Review and change one at-risk password" + "message": "Revisa y cambia una contraseña en riesgo" }, "reviewAndChangeAtRiskPasswordsPlural": { - "message": "Review and change $COUNT$ at-risk passwords", + "message": "Revisa y cambia $COUNT$ contraseñas en riesgo", "placeholders": { "count": { "content": "$1", @@ -2607,7 +2613,7 @@ "message": "Update your settings so you can quickly autofill your passwords and generate new ones" }, "reviewAtRiskLogins": { - "message": "Review at-risk logins" + "message": "Revisar inicios de sesión en riesgo" }, "reviewAtRiskPasswords": { "message": "Revisar contraseñas de riesgo" @@ -2620,7 +2626,7 @@ "message": "Illustration of a list of logins that are at-risk." }, "generatePasswordSlideDesc": { - "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", + "message": "Genera rápidamente una contraseña segura y única con el menú de autocompletado de Bitwarden en el sitio en riesgo.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAltPeriod": { @@ -2677,7 +2683,7 @@ "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ visualizaciones restantes", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2708,11 +2714,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCountReached": { - "message": "Max access count reached", + "message": "Número máximo de accesos alcanzado", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "Ocultar texto por defecto" }, "expired": { "message": "Caducado" @@ -2759,7 +2765,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "¿Estás seguro de que quieres eliminar permanentemente este Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -2770,7 +2776,7 @@ "message": "Fecha de eliminación" }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "El Send se borrará permanentemente en esta fecha.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -2792,7 +2798,7 @@ "message": "Personalizado" }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Añade una contraseña opcional para que los destinatarios accedan a este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -2815,15 +2821,15 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "¡Send creado con éxito!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "El Send estará disponible a cualquiera que tenga el enlace durante la próxima hora.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "El Send estará disponible a cualquier que tenga el enlace durante las próximas $HOURS$ horas.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2833,11 +2839,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "El Send estará disponible a cualquier que tenga el enlace durante el próximo día.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "El Send estará disponible a cualquier que tenga el enlace durante los próximos $DAYS$ días.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2847,7 +2853,7 @@ } }, "sendLinkCopied": { - "message": "Send link copied", + "message": "Enlace del Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { @@ -2985,7 +2991,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "$HOURS$ hora(s) y $MINUTES$ minuto(s) como máximo.", "placeholders": { "hours": { "content": "$1", @@ -3162,7 +3168,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Usa $RECOMMENDED$ palabras o más para generar una frase de contraseña segura.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3207,7 +3213,7 @@ "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Elige un dominio que esté soportado por el servicio seleccionado", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -3263,7 +3269,7 @@ } }, "forwaderInvalidOperation": { - "message": "$SERVICENAME$ refused your request. Please contact your service provider for assistance.", + "message": "$SERVICENAME$ ha rechazado tu solicitud. Por favor, contacta con tu proveedor del servicio para obtener asistencia.", "description": "Displayed when the user is forbidden from using the API by the forwarding service.", "placeholders": { "servicename": { @@ -3273,7 +3279,7 @@ } }, "forwaderInvalidOperationWithMessage": { - "message": "$SERVICENAME$ refused your request: $ERRORMESSAGE$", + "message": "$SERVICENAME$ ha rechazado tu solicitud $ERRORMESSAGE$", "description": "Displayed when the user is forbidden from using the API by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3542,10 +3548,10 @@ "message": "Se requiere aprobación del dispositivo. Seleccione una opción de aprobación a continuación:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Aprobación del dispositivo requerida" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Selecciona una opción de aprobación abajo" }, "rememberThisDevice": { "message": "Recordar este dispositivo" @@ -3615,22 +3621,22 @@ "message": "Falta el correo electrónico del usuario" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Correo electrónico del usuario activo no encontrado. Cerrando sesión." }, "deviceTrusted": { "message": "Dispositivo de confianza" }, "trustOrganization": { - "message": "Trust organization" + "message": "Confiar en la organización" }, "trust": { - "message": "Trust" + "message": "Confiar" }, "doNotTrust": { - "message": "Do not trust" + "message": "No confiar" }, "organizationNotTrusted": { - "message": "Organization is not trusted" + "message": "La organización no es de confianza" }, "emergencyAccessTrustWarning": { "message": "For the security of your account, only confirm if you have granted emergency access to this user and their fingerprint matches what is displayed in their account" @@ -3642,14 +3648,14 @@ "message": "This organization has an Enterprise policy that will enroll you in account recovery. Enrollment will allow organization administrators to change your password. Only proceed if you recognize this organization and the fingerprint phrase displayed below matches the organization's fingerprint." }, "trustUser": { - "message": "Trust user" + "message": "Confiar con el usuario" }, "sendsTitleNoItems": { - "message": "Send sensitive information safely", + "message": "Envía información sensible de forma segura", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsBodyNoItems": { - "message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.", + "message": "Comparte archivos y datos de forma segura con cualquiera, en cualquier plataforma. Tu información permanecerá encriptada de extremo a extremo, limitando su exposición.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3814,7 +3820,7 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "Desbloquea tu cuenta, se abre en una nueva ventana", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { @@ -3822,7 +3828,7 @@ "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "Tiempo restante antes de que el TOTP actual expire", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -3846,11 +3852,11 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Nuevo inicio de sesión", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "Añadir nuevo elemento de inicio de sesión a la caja fuerte, se abre en una nueva ventana", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { @@ -4045,10 +4051,10 @@ "message": "Clave de acceso" }, "accessing": { - "message": "Accessing" + "message": "Accediendo" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "¡Sesión iniciada!" }, "passkeyNotCopied": { "message": "La clave de acceso no se copiará" @@ -4060,7 +4066,7 @@ "message": "Verificación requerida por el sitio inicial. Esta característica aún no está implementada para cuentas sin contraseña maestra." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "¿Iniciar sesión con clave de acceso?" }, "passkeyAlreadyExists": { "message": "Ya existe una clave de acceso para esta aplicación." @@ -4072,10 +4078,10 @@ "message": "No tiene un inicio de sesión que coincida para este sitio." }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "No hay inicios de sesión coincidentes para este sitio" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Busca o guarda la clave de acceso como nuevo inicio de sesión" }, "confirm": { "message": "Confirmar" @@ -4087,10 +4093,10 @@ "message": "Guardar clave de acceso como nuevo inicio de sesión" }, "chooseCipherForPasskeySave": { - "message": "Choose a login to save this passkey to" + "message": "Elige un inicio de sesión al que guardar esta clave de acceso" }, "chooseCipherForPasskeyAuth": { - "message": "Choose a passkey to log in with" + "message": "Elige una clave de acceso para iniciar sesión en" }, "passkeyItem": { "message": "Elemento de clave de acceso" @@ -4239,11 +4245,11 @@ "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "¿Continuar a los ajustes del navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "¿Continuar al Centro de Ayuda?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { @@ -4319,7 +4325,7 @@ "message": "Sugerencias de autocompletado" }, "itemSuggestions": { - "message": "Suggested items" + "message": "Elementos sugeridos" }, "autofillSuggestionsTip": { "message": "Guarda un elemento de inicio de sesión para este sitio para autocompletar" @@ -4384,7 +4390,7 @@ } }, "viewItemTitleWithField": { - "message": "View item - $ITEMNAME$ - $FIELD$", + "message": "Ver elemento - $ITEMNAME$ - $FIELD$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4408,7 +4414,7 @@ } }, "autofillTitleWithField": { - "message": "Autofill - $ITEMNAME$ - $FIELD$", + "message": "Autocompletar - $ITEMNAME$ - $FIELD$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4422,7 +4428,7 @@ } }, "copyFieldValue": { - "message": "Copy $FIELD$, $VALUE$", + "message": "Copiar $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -4439,7 +4445,7 @@ "message": "No hay valores para copiar" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Asignar a colecciones" }, "copyEmail": { "message": "Copiar correo electrónico" @@ -4569,25 +4575,25 @@ } }, "downloadBitwarden": { - "message": "Download Bitwarden" + "message": "Descargar Bitwarden" }, "downloadBitwardenOnAllDevices": { - "message": "Download Bitwarden on all devices" + "message": "Descargar Bitwarden en todos los dispositivos" }, "getTheMobileApp": { - "message": "Get the mobile app" + "message": "Obtén la aplicación móvil" }, "getTheMobileAppDesc": { "message": "Access your passwords on the go with the Bitwarden mobile app." }, "getTheDesktopApp": { - "message": "Get the desktop app" + "message": "Obtén la aplicación de escritorio" }, "getTheDesktopAppDesc": { "message": "Access your vault without a browser, then set up unlock with biometrics to expedite unlocking in both the desktop app and browser extension." }, "downloadFromBitwardenNow": { - "message": "Download from bitwarden.com now" + "message": "Descarga desde bitwarden.com ahora" }, "getItOnGooglePlay": { "message": "Consíguela en Google Play" @@ -4611,10 +4617,10 @@ "message": "Filter vault" }, "filterApplied": { - "message": "One filter applied" + "message": "Un filtro aplicado" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ filtros aplicados", "placeholders": { "count": { "content": "$1", @@ -4648,10 +4654,10 @@ "message": "Credenciales de inicio de sesión" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Clave de autenticador" }, "autofillOptions": { - "message": "Autofill options" + "message": "Opciones de autocompletado" }, "websiteUri": { "message": "Página web (URI)" @@ -4667,7 +4673,7 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Sitio web añadido" }, "addWebsite": { "message": "Añadir página web" @@ -4710,7 +4716,7 @@ "message": "Tarjeta caducada" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Si la has renovado, actualiza la información de la tarjeta" }, "cardDetails": { "message": "Datos de la tarjeta" @@ -4740,7 +4746,7 @@ "message": "Datos" }, "passkeys": { - "message": "Passkeys", + "message": "Claves de acceso", "description": "A section header for a list of passkeys." }, "passwords": { @@ -4755,13 +4761,13 @@ "message": "Asignar" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "Solo los miembros de la organización con acceso a estas colecciones podrán ver el elemento." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Solo los miembros de la organización con acceso a estas colecciones podrán ver los elementos." }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Has seleccionado $TOTAL_COUNT$ elementos. No puedes actualizar $READONLY_COUNT$ de los elementos porque no tienes permisos de edición.", "placeholders": { "total_count": { "content": "$1", @@ -4785,10 +4791,10 @@ "message": "Etiqueta de campo" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Usa campos de texto para datos como preguntas de seguridad" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "Usa campos ocultos para datos sensibles como una contraseña" }, "checkBoxHelpText": { "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" @@ -4859,10 +4865,10 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Selecciona colecciones para asignar" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1 elemento será transferido permanentemente a la organización seleccionada. Ya no serás el propietario de este elemento." }, "personalItemsTransferWarningPlural": { "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", @@ -4896,13 +4902,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Colecciones asignadas correctamente" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "No has seleccionado nada." }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "Elementos movidos a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4911,7 +4917,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "Elemento movido a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4955,13 +4961,13 @@ "message": "Acciones de cuenta" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "Mostrar número de sugerencias de autocompletado de inicios de sesión en el icono de la extensión" }, "showQuickCopyActions": { "message": "Show quick copy actions on Vault" }, "systemDefault": { - "message": "System default" + "message": "Predeterminado del sistema" }, "enterprisePolicyRequirementsApplied": { "message": "Enterprise policy requirements have been applied to this setting" @@ -5003,10 +5009,10 @@ "message": "File saved to device. Manage from your device downloads." }, "showCharacterCount": { - "message": "Show character count" + "message": "Mostrar número de caracteres" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "Ocultar número de caracteres" }, "itemsInTrash": { "message": "Elementos en la papelera" @@ -5030,10 +5036,10 @@ "message": "No tiene permiso de editar este elemento" }, "biometricsStatusHelptextUnlockNeeded": { - "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + "message": "El desbloqueo biométrico no está disponible porque primero es necesario desbloquear con PIN o contraseña." }, "biometricsStatusHelptextHardwareUnavailable": { - "message": "Biometric unlock is currently unavailable." + "message": "El desbloqueo biométrico no está disponible actualmente." }, "biometricsStatusHelptextAutoSetupNeeded": { "message": "Biometric unlock is unavailable due to misconfigured system files." @@ -5042,10 +5048,10 @@ "message": "Biometric unlock is unavailable due to misconfigured system files." }, "biometricsStatusHelptextDesktopDisconnected": { - "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + "message": "El desbloqueo biométrico no está disponible porque la aplicación de escritorio de Bitwarden está cerrada." }, "biometricsStatusHelptextNotEnabledInDesktop": { - "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "message": "El desbloqueo biométrico no está disponible porque no está habilitado para $EMAIL$ en la aplicación de escritorio Bitwarden.", "placeholders": { "email": { "content": "$1", @@ -5054,19 +5060,19 @@ } }, "biometricsStatusHelptextUnavailableReasonUnknown": { - "message": "Biometric unlock is currently unavailable for an unknown reason." + "message": "El desbloqueo biométrico no está disponible actualmente por una razón desconocida." }, "unlockVault": { - "message": "Unlock your vault in seconds" + "message": "Desbloquea tu caja fuete en segundos" }, "unlockVaultDesc": { "message": "You can customize your unlock and timeout settings to more quickly access your vault." }, "unlockPinSet": { - "message": "Unlock PIN set" + "message": "Desbloqueo con PIN establecido" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Desbloqueo con biometría establecido" }, "authenticating": { "message": "Autenticando" @@ -5255,16 +5261,16 @@ "message": "Introduzca la contraseña" }, "invalidSshKey": { - "message": "The SSH key is invalid" + "message": "La clave SSH es inválida" }, "sshKeyTypeUnsupported": { - "message": "The SSH key type is not supported" + "message": "El tipo de clave SSH no está soportado" }, "importSshKeyFromClipboard": { - "message": "Import key from clipboard" + "message": "Importar clave desde el portapapeles" }, "sshKeyImported": { - "message": "SSH key imported successfully" + "message": "Clave SSH importada con éxito" }, "cannotRemoveViewOnlyCollections": { "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$", @@ -5285,16 +5291,16 @@ "message": "Cambiar contraseña de riesgo" }, "settingsVaultOptions": { - "message": "Vault options" + "message": "Opciones de la caja fuerte" }, "emptyVaultDescription": { "message": "The vault protects more than just your passwords. Store secure logins, IDs, cards and notes securely here." }, "introCarouselLabel": { - "message": "Welcome to Bitwarden" + "message": "Bienvenido a Bitwarden" }, "securityPrioritized": { - "message": "Security, prioritized" + "message": "Seguridad, priorizada" }, "securityPrioritizedBody": { "message": "Save logins, cards, and identities to your secure vault. Bitwarden uses zero-knowledge, end-to-end encryption to protect what’s important to you." @@ -5303,7 +5309,7 @@ "message": "Quick and easy login" }, "quickLoginBody": { - "message": "Set up biometric unlock and autofill to log into your accounts without typing a single letter." + "message": "Configura el desbloqueo biométrico y el autocompletado para iniciar sesión en tus cuentas sin tener que escribir ni una sola letra." }, "secureUser": { "message": "Level up your logins" @@ -5312,25 +5318,25 @@ "message": "Utilice el generador para crear y guardar contraseñas fuertes y únicas para todas sus cuentas." }, "secureDevices": { - "message": "Your data, when and where you need it" + "message": "Tus datos, dónde y cuándo los necesites" }, "secureDevicesBody": { "message": "Guarda contraseñas ilimitadas a través de dispositivos ilimitados con aplicaciones móviles, de navegador y de escritorio de Bitwarden." }, "nudgeBadgeAria": { - "message": "1 notification" + "message": "1 notificación" }, "emptyVaultNudgeTitle": { - "message": "Import existing passwords" + "message": "Importar contraseñas existentes" }, "emptyVaultNudgeBody": { - "message": "Use the importer to quickly transfer logins to Bitwarden without manually adding them." + "message": "Usa el importador para transferir rápidamente inicios de sesión a Bitwarden sin añadirlos manualmente." }, "emptyVaultNudgeButton": { - "message": "Import now" + "message": "Importar ahora" }, "hasItemsVaultNudgeTitle": { - "message": "Welcome to your vault!" + "message": "¡Bienvenido a tu caja fuerte!" }, "hasItemsVaultNudgeBodyOne": { "message": "Autofill items for the current page" @@ -5342,7 +5348,7 @@ "message": "Search your vault for something else" }, "newLoginNudgeTitle": { - "message": "Save time with autofill" + "message": "Ahorra tiempo con el autocompletado" }, "newLoginNudgeBodyOne": { "message": "Include a", @@ -5350,7 +5356,7 @@ "example": "Include a Website so this login appears as an autofill suggestion." }, "newLoginNudgeBodyBold": { - "message": "Website", + "message": "Sitio web", "description": "This is in multiple parts to allow for bold text in the middle of the sentence.", "example": "Include a Website so this login appears as an autofill suggestion." }, @@ -5366,16 +5372,16 @@ "message": "With cards, easily autofill payment forms securely and accurately." }, "newIdentityNudgeTitle": { - "message": "Simplify creating accounts" + "message": "Simplifica la creación de cuentas" }, "newIdentityNudgeBody": { "message": "With identities, quickly autofill long registration or contact forms." }, "newNoteNudgeTitle": { - "message": "Keep your sensitive data safe" + "message": "Mantén tus datos sensibles seguros" }, "newNoteNudgeBody": { - "message": "With notes, securely store sensitive data like banking or insurance details." + "message": "Con las notas, almacena de forma segura datos sensibles como datos bancarios o de seguros." }, "newSshNudgeTitle": { "message": "Developer-friendly SSH access" @@ -5386,20 +5392,20 @@ "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "newSshNudgeBodyTwo": { - "message": "Learn more about SSH agent", + "message": "Más información sobre el agente SSH", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, "generatorNudgeTitle": { - "message": "Quickly create passwords" + "message": "Crea contraseñas rápidamente" }, "generatorNudgeBodyOne": { - "message": "Easily create strong and unique passwords by clicking on", + "message": "Crea fácilmente contraseñas seguras y únicas haciendo clic en", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, "generatorNudgeBodyTwo": { - "message": "to help you keep your logins secure.", + "message": "para ayudarte a mantener tus inicios de sesión seguros.", "description": "Two part message", "example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure." }, @@ -5408,7 +5414,7 @@ "description": "Aria label for the body content of the generator nudge" }, "noPermissionsViewPage": { - "message": "You do not have permissions to view this page. Try logging in with a different account." + "message": "No tienes permisos para ver esta página. Intenta iniciar sesión con otra cuenta." }, "wasmNotSupported": { "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 4046451a567..6893dec3f4f 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 1b04d15f36f..49e87abcf83 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index f07352ca159..a1979d703bf 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "یک سیاست سازمانی، درون ریزی موارد به گاوصندوق فردی شما را مسدود کرده است." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "دامنه‌ها", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "بازکردن قفل کد پین تنظیم شد" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "در حال احراز هویت" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 5723b9b29c5..098e361223a 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organisaatiokäytäntö estää kohteiden tuonnin yksityiseen holviisi." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Verkkotunnukset", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Todennetaan" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index d52eae1b43e..de17e87a57b 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Hinarang ng isang patakaran ng organisasyon ang pag-import ng mga item sa iyong vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index f913e2ab4b3..e1397980675 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Une politique d'organisation a bloqué l'import d'éléments dans votre coffre personel." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domaines", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authentification" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 41766204775..152f4d7236c 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Unha directiva da empresa impide importar entradas á túa caixa forte individual." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Dominios", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Autenticando" diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 7549d77cac0..215aa17988d 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "מדיניות ארגון חסמה ייבוא פריטים אל תוך הכספת האישית שלך." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "דומיינים", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "מאמת" diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 4d766d1090d..64a98de313b 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 972448319b5..69a68e921ae 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organizacijsko pravilo onemogućuje uvoz stavki u tvoj osobni trezor." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domene", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Autentifikacija" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 13d5996469f..c85de0ab542 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "A szervezeti politika blokkolta az elemek importálását az egyedi széfbe." }, + "restrictCardTypeImport": { + "message": "A kártya elem típusokat nem lehet importálni." + }, + "restrictCardTypeImportDesc": { + "message": "Egy vagy több szervezet által beállított szabályzat megakadályozza a kártyák importálását a széfekbe." + }, "domainsTitle": { "message": "Tartomány", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "PIN beállítás feloldása" }, - "unlockBiometricSet": { - "message": "Biometriai beállítások feloldása" + "unlockWithBiometricSet": { + "message": "Feloldás biometrikusan" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 995540036d6..125cd7ceeab 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Sebuah kebijakan organisasi telah menghalangi mengimpor benda-benda ke brankas pribadi Anda." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domain", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "PIN untuk membuka telah diatur" }, - "unlockBiometricSet": { - "message": "Biometrik untuk membuka telah diatur" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Sedang memeriksa keaslian" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 09b92304287..f1c2cd09ca2 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Una politica dell'organizzazione ti impedisce di importare elementi nella tua cassaforte individuale." }, + "restrictCardTypeImport": { + "message": "Impossibile importare elementi di tipo carta" + }, + "restrictCardTypeImportDesc": { + "message": "Non puoi importare carte nelle tue casseforti a causa di una politica impostata da una o più organizzazioni." + }, "domainsTitle": { "message": "Domini", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Sblocca PIN impostato" }, - "unlockBiometricSet": { - "message": "Sblocco biometrico" + "unlockWithBiometricSet": { + "message": "Sblocca con i dati biometrici" }, "authenticating": { "message": "Autenticazione" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 9b96507cb2c..fd256961ba6 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "組織のポリシーにより、個々の保管庫へのアイテムのインポートがブロックされました。" }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "ドメイン", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "認証中" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 00fcf43aa67..68a6a877e50 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "დომენები", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "ავთენტიკაცია" diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 2d29efcc89e..b6a8d1834b4 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index da9a4637444..987a2ce79cb 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index a49ca045dd4..41c01431ac2 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "조직 정책으로 인해 개별 보관함으로 항목을 가져오는 것이 차단되었습니다." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "도메인", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "인증 중" diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 6b672696c2f..3c2751d5fbe 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organizacijos politika blokavo elementų importavimą į Jūsų individualią saugyklą." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domenai", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index c0690140cc2..7108fe15a93 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Apvienības nosacījums neļauj ievietot ārējos vienumus savā personīgajā glabātavā." }, + "restrictCardTypeImport": { + "message": "Nevar ievietot karšu vienumu veidus" + }, + "restrictCardTypeImportDesc": { + "message": "Pamatnostādne, ko ir iestatījusi viena vai vairākas apvienības, liedz karšu ievietošanu savās glabātavās." + }, "domainsTitle": { "message": "Domēna vārdi", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Atslēgšanas PIN iestatīts" }, - "unlockBiometricSet": { - "message": "Atslēgt biometrijas kopu" + "unlockWithBiometricSet": { + "message": "Atslēgt ar biometriju" }, "authenticating": { "message": "Autentificē" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 3bbb7e28da6..7708ea4b940 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index fdbd6a9895a..36fdc74f521 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 2d29efcc89e..b6a8d1834b4 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 0ec9268c915..747e37aadd6 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "En organisasjonsretningslinje har blokkert import av gjenstander til ditt individuelle hvelv." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domener", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Autentiserer" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 2d29efcc89e..b6a8d1834b4 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index a4e49771077..af63059ebd3 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organisatiebeleid heeft het importeren van items in je persoonlijke kluis geblokkeerd." }, + "restrictCardTypeImport": { + "message": "Kan kaart item types niet importeren" + }, + "restrictCardTypeImportDesc": { + "message": "Een beleid ingesteld door 1 of meer organisaties voorkomt dat je kaarten naar je kluizen kunt importeren." + }, "domainsTitle": { "message": "Domeinen", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "PIN-code ontgrendelen instellen" }, - "unlockBiometricSet": { - "message": "Biometrische set ontgrendelen" + "unlockWithBiometricSet": { + "message": "Met biometrische set ontgrendelen" }, "authenticating": { "message": "Aan het inloggen" diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 2d29efcc89e..b6a8d1834b4 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 2d29efcc89e..b6a8d1834b4 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index de6dfa2d7ff..dba3a21d160 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -14,7 +14,7 @@ "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { - "message": "Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do Twojego bezpiecznego sejfu." + "message": "Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do bezpiecznego sejfu." }, "inviteAccepted": { "message": "Zaproszenie zostało zaakceptowane" @@ -1923,7 +1923,7 @@ "message": "Klucz SSH" }, "typeNote": { - "message": "Note" + "message": "Notatka" }, "newItemHeader": { "message": "Nowy $TYPE$", @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Polityka organizacji zablokowała importowanie elementów do Twojego sejfu." }, + "restrictCardTypeImport": { + "message": "Nie można importować elementów typu karty" + }, + "restrictCardTypeImportDesc": { + "message": "Polityka ustawiona przez 1 lub więcej organizacji uniemożliwia importowanie kart do sejfów." + }, "domainsTitle": { "message": "Domeny", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Ustaw kod PIN odblokowujący" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Odblokuj za pomocą danych biometrycznych" }, "authenticating": { "message": "Uwierzytelnianie" @@ -5411,7 +5417,7 @@ "message": "Nie masz uprawnień do przeglądania tej strony. Spróbuj zalogować się na inne konto." }, "wasmNotSupported": { - "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "message": "Zestaw WebAssembly nie jest obsługiwany w przeglądarce lub nie jest włączony. Do korzystania z aplikacji Bitwarden wymagany jest zestaw WebAssembre.", "description": "'WebAssembly' is a technical term and should not be translated." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 542c2be0a0b..4405d1c59df 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "A política da organização bloqueou a importação de itens para o seu cofre." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domínios", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Autenticando" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 103dc0351da..ba791785b0e 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Uma política da organização bloqueou a importação de itens para o seu cofre individual." }, + "restrictCardTypeImport": { + "message": "Não é possível importar tipos de itens de cartão" + }, + "restrictCardTypeImportDesc": { + "message": "Uma política definida por 1 ou mais organizações impede-o de importar cartões para os seus cofres." + }, "domainsTitle": { "message": "Domínios", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Definição do PIN de desbloqueio" }, - "unlockBiometricSet": { - "message": "Desbloquear conjunto de biometria" + "unlockWithBiometricSet": { + "message": "Desbloquear com conjunto biométrico" }, "authenticating": { "message": "A autenticar" diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 5a21d0886a4..b27a1cbc519 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 130ded8ef0a..b9ffa0afbdc 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Импорт элементов в ваше личное хранилище отключен политикой организации." }, + "restrictCardTypeImport": { + "message": "Невозможно импортировать элементы карт" + }, + "restrictCardTypeImportDesc": { + "message": "Политика, установленная 1 или более организациями, не позволяет импортировать карты в ваши хранилища." + }, "domainsTitle": { "message": "Домены", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Установить PIN--код разблокировки" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Разблокировать с помощью биометрии" }, "authenticating": { "message": "Аутентификация" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 70019afd17c..9f60625ce4c 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 600ab1817b8..8a10ad901e8 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Zásady organizácie zablokovali importovanie položiek do vášho osobného trezoru." }, + "restrictCardTypeImport": { + "message": "Položky typu karta sa nedajú importovať" + }, + "restrictCardTypeImportDesc": { + "message": "Politika nastavená 1 alebo viacerými organizáciami vám bráni v importovaní kariet do vašich trezorov." + }, "domainsTitle": { "message": "Domény", "description": "A category title describing the concept of web domains" @@ -5065,7 +5071,7 @@ "unlockPinSet": { "message": "PIN na odomknutie nastavený" }, - "unlockBiometricSet": { + "unlockWithBiometricSet": { "message": "Odomknutie biometrickými údajmi nastavené" }, "authenticating": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 25bbabfc0c6..2d73e99023c 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 0479276441b..870338e3563 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1923,7 +1923,7 @@ "message": "SSH кључ" }, "typeNote": { - "message": "Note" + "message": "Белешка" }, "newItemHeader": { "message": "Нови $TYPE$", @@ -2157,7 +2157,7 @@ "message": "Поставите свој ПИН код за откључавање Bitwarden-а. Поставке ПИН-а ће се ресетовати ако се икада потпуно одјавите из апликације." }, "setPinCode": { - "message": "You can use this PIN to unlock Bitwarden. Your PIN will be reset if you ever fully log out of the application." + "message": "Можете употребити овај ПИН да би деблокирали Bitwarden. Ваш ПИН ће се ресетовати ако се икада у потпуности одјавите из апликације." }, "pinRequired": { "message": "ПИН је обавезан." @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Политика организације је блокирала увоз ставки у ваш појединачни сеф." }, + "restrictCardTypeImport": { + "message": "Не могу увозити врсте картица" + }, + "restrictCardTypeImportDesc": { + "message": "Политика која је поставила 1 или више организација спречава вас да се увозе картице у сефу." + }, "domainsTitle": { "message": "Домени", "description": "A category title describing the concept of web domains" @@ -2519,7 +2525,7 @@ "message": "Промени" }, "changePassword": { - "message": "Change password", + "message": "Промени лозинку", "description": "Change password button for browser at risk notification on login." }, "changeButtonTitle": { @@ -2532,7 +2538,7 @@ } }, "atRiskPassword": { - "message": "At-risk password" + "message": "Лозинка под ризиком" }, "atRiskPasswords": { "message": "Лозинке под ризиком" @@ -2569,7 +2575,7 @@ } }, "atRiskChangePrompt": { - "message": "Your password for this site is at-risk. $ORGANIZATION$ has requested that you change it.", + "message": "Ваша лозинка за ову страницу је ризична. $ORGANIZATION$ је затражио да је промените.", "placeholders": { "organization": { "content": "$1", @@ -2579,7 +2585,7 @@ "description": "Notification body when a login triggers an at-risk password change request and the change password domain is known." }, "atRiskNavigatePrompt": { - "message": "$ORGANIZATION$ wants you to change this password because it is at-risk. Navigate to your account settings to change the password.", + "message": "$ORGANIZATION$ жели да промените ову лозинку јер је ризична. Идите до поставки вашег налога да бисте променили лозинку.", "placeholders": { "organization": { "content": "$1", @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Постављен ПИН деблокирања" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Откључај биометријом" }, "authenticating": { "message": "Аутентификација" @@ -5411,7 +5417,7 @@ "message": "Немате дозволе за преглед ове странице. Покушајте да се пријавите са другим налогом." }, "wasmNotSupported": { - "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "message": "WebAssembly није подржано или није уапљено на вашем прегледачу. WebAssembly је потребно да би се користила апликација Bitwarden.", "description": "'WebAssembly' is a technical term and should not be translated." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 5af58bc3419..c4b72cc5ea1 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domäner", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 2d29efcc89e..b6a8d1834b4 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 7d291b911be..f7895c8866d 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Domains", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index abde20c43e0..aae4fdd2486 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Bir kuruluş ilkesi, kayıtları kişisel kasanıza içe aktarmayı engelledi." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Alan adları", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Kimlik doğrulanıyor" diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 8f9e8e21f36..ab4fe87a2be 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Політика організації заблокувала імпортування записів до вашого особистого сховища." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Домени", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Розблокування PIN-кодом встановлено" }, - "unlockBiometricSet": { - "message": "Біометричне розблокування налаштовано" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Аутентифікація" diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index b27e419eb30..d02658f9e26 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "Chính sách của tổ chức đã chặn việc nhập các mục vào kho cá nhân của bạn." }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "Các tên miền", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "Authenticating" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index de945e94110..fe70f8abe57 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -300,7 +300,7 @@ "message": "前往帮助中心吗?" }, "continueToHelpCenterDesc": { - "message": "在帮助中心进一步了解如何使用 Bitwarden。" + "message": "访问帮助中心进一步了解如何使用 Bitwarden。" }, "continueToBrowserExtensionStore": { "message": "前往浏览器扩展商店吗?" @@ -2160,7 +2160,7 @@ "message": "您可以使用此 PIN 码解锁 Bitwarden。您的 PIN 码将在您完全注销此应用程序时被重置。" }, "pinRequired": { - "message": "需要 PIN 码。" + "message": "必须填写 PIN 码。" }, "invalidPin": { "message": "无效 PIN 码。" @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "某个组织策略已阻止将项目导入您的个人密码库。" }, + "restrictCardTypeImport": { + "message": "无法导入支付卡项目类型" + }, + "restrictCardTypeImportDesc": { + "message": "由 1 个或多个组织设置的策略阻止您将支付卡导入密码库。" + }, "domainsTitle": { "message": "域名", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "解锁 PIN 设置" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "生物识别解锁设置" }, "authenticating": { "message": "正在验证" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 1614cf3c9ff..e50117419b2 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -2487,6 +2487,12 @@ "personalOwnershipPolicyInEffectImports": { "message": "某個組織原則已禁止您將項目匯入至您的個人密碼庫。" }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." + }, "domainsTitle": { "message": "網域", "description": "A category title describing the concept of web domains" @@ -5065,8 +5071,8 @@ "unlockPinSet": { "message": "Unlock PIN set" }, - "unlockBiometricSet": { - "message": "Unlock biometrics set" + "unlockWithBiometricSet": { + "message": "Unlock with biometrics set" }, "authenticating": { "message": "驗證中" diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 066332b0e23..4f9e1f7414a 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -533,6 +533,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { await this.biometricStateService.setBiometricUnlockEnabled(successful); if (!successful) { await this.biometricStateService.setFingerprintValidated(false); + return; } this.toastService.showToast({ variant: "success", @@ -597,6 +598,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { this.i18nService.t("errorEnableBiometricTitle"), this.i18nService.t("errorEnableBiometricDesc"), ); + setupResult = false; + return; } setupResult = true; } catch (e) { diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index 267a832a671..901d6595fc8 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -1,5 +1,5 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { of } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { @@ -22,6 +22,10 @@ import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + RestrictedCipherType, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { MainContextMenuHandler } from "./main-context-menu-handler"; @@ -69,6 +73,8 @@ describe("context-menu", () => { let logService: MockProxy; let billingAccountProfileStateService: MockProxy; let accountService: MockProxy; + let restricted$: BehaviorSubject; + let restrictedItemTypesService: RestrictedItemTypesService; let removeAllSpy: jest.SpyInstance void]>; let createSpy: jest.SpyInstance< @@ -85,6 +91,10 @@ describe("context-menu", () => { logService = mock(); billingAccountProfileStateService = mock(); accountService = mock(); + restricted$ = new BehaviorSubject([]); + restrictedItemTypesService = { + restricted$, + } as Partial as RestrictedItemTypesService; removeAllSpy = jest .spyOn(chrome.contextMenus, "removeAll") @@ -105,6 +115,7 @@ describe("context-menu", () => { logService, billingAccountProfileStateService, accountService, + restrictedItemTypesService, ); jest.spyOn(MainContextMenuHandler, "remove"); @@ -147,6 +158,24 @@ describe("context-menu", () => { expect(createdMenu).toBeTruthy(); expect(createSpy).toHaveBeenCalledTimes(11); }); + + it("has menu enabled and has premium, but card type is restricted", async () => { + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); + + restricted$.next([{ cipherType: CipherType.Card, allowViewOrgIds: [] }]); + + const createdMenu = await sut.init(); + expect(createdMenu).toBeTruthy(); + expect(createSpy).toHaveBeenCalledTimes(10); + }); + it("has menu enabled, does not have premium, and card type is restricted", async () => { + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); + restricted$.next([{ cipherType: CipherType.Card, allowViewOrgIds: [] }]); + + const createdMenu = await sut.init(); + expect(createdMenu).toBeTruthy(); + expect(createSpy).toHaveBeenCalledTimes(9); + }); }); describe("loadOptions", () => { diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index ad9dc34e501..abfa2465c51 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -25,8 +25,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { InitContextMenuItems } from "./abstractions/main-context-menu-handler"; @@ -157,6 +158,7 @@ export class MainContextMenuHandler { private logService: LogService, private billingAccountProfileStateService: BillingAccountProfileStateService, private accountService: AccountService, + private restrictedItemTypesService: RestrictedItemTypesService, ) {} /** @@ -181,6 +183,10 @@ export class MainContextMenuHandler { this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); + const isCardRestricted = ( + await firstValueFrom(this.restrictedItemTypesService.restricted$) + ).some((rt) => rt.cipherType === CipherType.Card); + for (const menuItem of this.initContextMenuItems) { const { requiresPremiumAccess, @@ -192,6 +198,9 @@ export class MainContextMenuHandler { if (requiresPremiumAccess && !hasPremium) { continue; } + if (menuItem.id.startsWith(AUTOFILL_CARD_ID) && isCardRestricted) { + continue; + } await MainContextMenuHandler.create({ ...otherOptions, contexts: ["all"] }); } diff --git a/apps/browser/src/autofill/content/abstractions/content-message-handler.ts b/apps/browser/src/autofill/content/abstractions/content-message-handler.ts index 8231bd688c9..f413ace9432 100644 --- a/apps/browser/src/autofill/content/abstractions/content-message-handler.ts +++ b/apps/browser/src/autofill/content/abstractions/content-message-handler.ts @@ -1,3 +1,5 @@ +import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; + type ContentMessageWindowData = { command: string; lastpass?: boolean; @@ -5,6 +7,7 @@ type ContentMessageWindowData = { state?: string; data?: string; remember?: boolean; + url?: ExtensionPageUrls; }; type ContentMessageWindowEventParams = { data: ContentMessageWindowData; diff --git a/apps/browser/src/autofill/content/content-message-handler.ts b/apps/browser/src/autofill/content/content-message-handler.ts index 60f093f8c10..c57b2d959f3 100644 --- a/apps/browser/src/autofill/content/content-message-handler.ts +++ b/apps/browser/src/autofill/content/content-message-handler.ts @@ -1,3 +1,4 @@ +import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; import { @@ -18,6 +19,8 @@ const windowMessageHandlers: ContentMessageWindowEventHandlers = { duoResult: ({ data, referrer }: { data: any; referrer: string }) => handleDuoResultMessage(data, referrer), [VaultMessages.OpenAtRiskPasswords]: () => handleOpenAtRiskPasswordsMessage(), + [VaultMessages.OpenBrowserExtensionToUrl]: ({ data }) => + handleOpenBrowserExtensionToUrlMessage(data), }; /** @@ -73,10 +76,15 @@ function handleWebAuthnResultMessage(data: ContentMessageWindowData, referrer: s sendExtensionRuntimeMessage({ command, data: data.data, remember, referrer }); } +/** @deprecated use {@link handleOpenBrowserExtensionToUrlMessage} */ function handleOpenAtRiskPasswordsMessage() { sendExtensionRuntimeMessage({ command: VaultMessages.OpenAtRiskPasswords }); } +function handleOpenBrowserExtensionToUrlMessage({ url }: { url?: ExtensionPageUrls }) { + sendExtensionRuntimeMessage({ command: VaultMessages.OpenBrowserExtensionToUrl, url }); +} + /** * Handles the window message event. * diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 3107b60f475..ac38fe2f894 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -18,13 +18,13 @@ import { } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4ba869768f5..c6d68a9f047 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -6,8 +6,10 @@ import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs"; import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; import { + AuthRequestApiServiceAbstraction, AuthRequestService, AuthRequestServiceAbstraction, + DefaultAuthRequestApiService, DefaultLockService, InternalUserDecryptionOptionsServiceAbstraction, LoginEmailServiceAbstraction, @@ -20,7 +22,6 @@ import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstracti import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -172,7 +173,6 @@ import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordStrengthService, PasswordStrengthServiceAbstraction, @@ -188,8 +188,10 @@ import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vau import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/vault/abstractions/search.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DefaultEndUserNotificationService, @@ -205,6 +207,7 @@ import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-u import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { SearchService } from "@bitwarden/common/vault/services/search.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; @@ -375,6 +378,7 @@ export default class MainBackground { devicesService: DevicesServiceAbstraction; deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestServiceAbstraction; + authRequestApiService: AuthRequestApiServiceAbstraction; accountService: AccountServiceAbstraction; globalStateProvider: GlobalStateProvider; pinService: PinServiceAbstraction; @@ -412,7 +416,7 @@ export default class MainBackground { inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; cipherEncryptionService: CipherEncryptionService; - restrictedItemTypesService: RestrictedItemTypesService; + private restrictedItemTypesService: RestrictedItemTypesService; ipcContentScriptManagerService: IpcContentScriptManagerService; ipcService: IpcService; @@ -813,14 +817,16 @@ export default class MainBackground { this.appIdService, ); + this.authRequestApiService = new DefaultAuthRequestApiService(this.apiService, this.logService); + this.authRequestService = new AuthRequestService( this.appIdService, - this.accountService, this.masterPasswordService, this.keyService, this.encryptService, this.apiService, this.stateProvider, + this.authRequestApiService, ); this.authService = new AuthService( @@ -1045,13 +1051,6 @@ export default class MainBackground { this.sdkService, ); - this.restrictedItemTypesService = new RestrictedItemTypesService( - this.configService, - this.accountService, - this.organizationService, - this.policyService, - ); - this.individualVaultExportService = new IndividualVaultExportService( this.folderService, this.cipherService, @@ -1094,6 +1093,7 @@ export default class MainBackground { this.configService, new WebPushNotificationsApiService(this.apiService, this.appIdService), registration, + this.stateProvider, ); } else { this.webPushConnectionService = new UnsupportedWebPushConnectionService(); @@ -1307,6 +1307,13 @@ export default class MainBackground { this.stateProvider, ); + this.restrictedItemTypesService = new RestrictedItemTypesService( + this.configService, + this.accountService, + this.organizationService, + this.policyService, + ); + this.mainContextMenuHandler = new MainContextMenuHandler( this.stateService, this.autofillSettingsService, @@ -1314,6 +1321,7 @@ export default class MainBackground { this.logService, this.billingAccountProfileStateService, this.accountService, + this.restrictedItemTypesService, ); this.cipherContextMenuHandler = new CipherContextMenuHandler( @@ -1344,7 +1352,7 @@ export default class MainBackground { this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); - this.ipcService = new IpcBackgroundService(this.logService); + this.ipcService = new IpcBackgroundService(this.platformUtilsService, this.logService); this.endUserNotificationService = new DefaultEndUserNotificationService( this.stateProvider, @@ -1687,14 +1695,44 @@ export default class MainBackground { // Set route of the popup before attempting to open it. // If the vault is locked, this won't have an effect as the auth guards will // redirect the user to the login page. - await browserAction.setPopup({ popup: "popup/index.html#/at-risk-passwords" }); + await browserAction.setPopup({ popup: ExtensionPageUrls.AtRiskPasswords }); await this.openPopup(); } finally { // Reset the popup route to the default route so any subsequent // popup openings will not open to the at-risk-passwords page. await browserAction.setPopup({ - popup: "popup/index.html#/", + popup: ExtensionPageUrls.Index, + }); + } + } + + /** + * Opens the popup to the given page + * @default ExtensionPageUrls.Index + */ + async openTheExtensionToPage(url: ExtensionPageUrls = ExtensionPageUrls.Index) { + const isValidUrl = Object.values(ExtensionPageUrls).includes(url); + + // If a non-defined URL is provided, return early. + if (!isValidUrl) { + return; + } + + const browserAction = BrowserApi.getBrowserAction(); + + try { + // Set route of the popup before attempting to open it. + // If the vault is locked, this won't have an effect as the auth guards will + // redirect the user to the login page. + await browserAction.setPopup({ popup: url }); + + await this.openPopup(); + } finally { + // Reset the popup route to the default route so any subsequent + // popup openings will not open to the at-risk-passwords page. + await browserAction.setPopup({ + popup: ExtensionPageUrls.Index, }); } } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index cca17730a22..54fb8326cfb 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -296,6 +296,10 @@ export default class RuntimeBackground { await this.main.openAtRisksPasswordsPage(); this.announcePopupOpen(); break; + case VaultMessages.OpenBrowserExtensionToUrl: + await this.main.openTheExtensionToPage(msg.url); + this.announcePopupOpen(); + break; case "bgUpdateContextMenu": case "editedCipher": case "addedCipher": diff --git a/apps/browser/src/platform/ipc/ipc-background.service.ts b/apps/browser/src/platform/ipc/ipc-background.service.ts index f26d8d680a3..911ca931c70 100644 --- a/apps/browser/src/platform/ipc/ipc-background.service.ts +++ b/apps/browser/src/platform/ipc/ipc-background.service.ts @@ -1,11 +1,13 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { IpcMessage, isIpcMessage, IpcService } from "@bitwarden/common/platform/ipc"; import { - IpcClient, IpcCommunicationBackend, IncomingMessage, OutgoingMessage, + ipcRegisterDiscoverHandler, + IpcClient, } from "@bitwarden/sdk-internal"; import { BrowserApi } from "../browser/browser-api"; @@ -13,7 +15,10 @@ import { BrowserApi } from "../browser/browser-api"; export class IpcBackgroundService extends IpcService { private communicationBackend?: IpcCommunicationBackend; - constructor(private logService: LogService) { + constructor( + private platformUtilsService: PlatformUtilsService, + private logService: LogService, + ) { super(); } @@ -60,11 +65,18 @@ export class IpcBackgroundService extends IpcService { { Web: { id: sender.tab.id }, }, + message.message.topic, ), ); }); await super.initWithClient(new IpcClient(this.communicationBackend)); + + if (this.platformUtilsService.isDev()) { + await ipcRegisterDiscoverHandler(this.client, { + version: await this.platformUtilsService.getApplicationVersion(), + }); + } } catch (e) { this.logService.error("[IPC] Initialization failed", e); } diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx index 017ee20b344..a2725350a8f 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.mdx +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -54,6 +54,9 @@ page looks nice when the extension is popped out. `false`. - `loadingText` - Custom text to be applied to the loading element for screenreaders only. Defaults to "Loading". +- `disablePadding` + - When `true`, disables the padding of the scrollable region inside of `main`. You will need to + add your own padding to the element you place inside of this area. Basic usage example: @@ -169,6 +172,22 @@ When the browser extension is popped out, the "popout" button should not be pass +## With Virtual Scroll + +If you are using a virtual scrolling container inside of the popup page, you'll want to apply the +`bitScrollLayout` directive to the `cdk-virtual-scroll-viewport` element. This tells the virtual +scroll viewport to use the popup page's scroll layout div as the scrolling container. + +See the code in the example below. + + + +### Known Virtual Scroll Issues + +See [Virtual Scrolling](?path=/docs/documentation-virtual-scrolling--docs#known-footgun) for more +information about how to structure virtual scrolling containers with layout components and avoid a +known issue with template construction. + # Other stories ## Centered Content diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index aecbaf673dc..894ab13dd19 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -1,5 +1,4 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { Component, importProvidersFrom } from "@angular/core"; import { RouterModule } from "@angular/router"; @@ -20,6 +19,7 @@ import { NoItemsModule, SearchModule, SectionComponent, + ScrollLayoutDirective, } from "@bitwarden/components"; import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; @@ -39,6 +39,17 @@ import { PopupTabNavigationComponent } from "./popup-tab-navigation.component"; }) class ExtensionContainerComponent {} +@Component({ + selector: "extension-popped-container", + template: ` +
+ +
+ `, + standalone: true, +}) +class ExtensionPoppedContainerComponent {} + @Component({ selector: "vault-placeholder", template: /*html*/ ` @@ -295,6 +306,7 @@ export default { decorators: [ moduleMetadata({ imports: [ + ScrollLayoutDirective, PopupTabNavigationComponent, PopupHeaderComponent, PopupPageComponent, @@ -302,6 +314,7 @@ export default { CommonModule, RouterModule, ExtensionContainerComponent, + ExtensionPoppedContainerComponent, MockBannerComponent, MockSearchComponent, MockVaultSubpageComponent, @@ -312,6 +325,11 @@ export default { MockVaultPagePoppedComponent, NoItemsModule, VaultComponent, + ScrollingModule, + ItemModule, + SectionComponent, + IconButtonModule, + BadgeModule, ], providers: [ { @@ -495,7 +513,21 @@ export const CompactMode: Story = { const compact = canvasEl.querySelector( `#${containerId} [data-testid=popup-layout-scroll-region]`, ); + + if (!compact) { + // eslint-disable-next-line + console.error(`#${containerId} [data-testid=popup-layout-scroll-region] not found`); + return; + } + const label = canvasEl.querySelector(`#${containerId} .example-label`); + + if (!label) { + // eslint-disable-next-line + console.error(`#${containerId} .example-label not found`); + return; + } + const percentVisible = 100 - Math.round((100 * (compact.scrollHeight - compact.clientHeight)) / compact.scrollHeight); @@ -510,9 +542,9 @@ export const PoppedOut: Story = { render: (args) => ({ props: args, template: /* HTML */ ` -
+ -
+ `, }), }; @@ -560,10 +592,9 @@ export const TransparentHeader: Story = { template: /* HTML */ ` - 🤠 Custom Content - + + 🤠 Custom Content + @@ -608,3 +639,56 @@ export const WidthOptions: Story = { `, }), }; + +export const WithVirtualScrollChild: Story = { + render: (args) => ({ + props: { ...args, data: Array.from(Array(20).keys()) }, + template: /* HTML */ ` + + + + + + @defer (on immediate) { + + + + + + + + + + + + + + + + + + + + } + + + + `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index 2313b942a38..b53ef6e97eb 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -1,29 +1,39 @@
+
+
-
- -
+
diff --git a/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index fc2b6590992..f28de2ce3dd 100644 --- a/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -60,6 +60,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected maxWidth: "md" | "3xl"; protected hasLoggedInAccount: boolean = false; protected hideFooter: boolean; + protected hideCardWrapper: boolean = false; protected theme: string; protected logo = Icons.ExtensionBitwardenLogo; @@ -137,6 +138,10 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { if (firstChildRouteData["hideIcon"] !== undefined) { this.hideIcon = Boolean(firstChildRouteData["hideIcon"]); } + + if (firstChildRouteData["hideCardWrapper"] !== undefined) { + this.hideCardWrapper = Boolean(firstChildRouteData["hideCardWrapper"]); + } } private listenForServiceDataChanges() { @@ -177,6 +182,10 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { this.showReadonlyHostname = data.showReadonlyHostname; } + if (data.hideCardWrapper !== undefined) { + this.hideCardWrapper = data.hideCardWrapper; + } + if (data.showAcctSwitcher !== undefined) { this.showAcctSwitcher = data.showAcctSwitcher; } @@ -214,6 +223,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { this.showLogo = null; this.maxWidth = null; this.hideFooter = null; + this.hideCardWrapper = null; } ngOnDestroy() { diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 80ada61f868..2b625678b89 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -381,14 +381,6 @@ app-root { } } -// Adds padding on each side of the content if opened in a tab -@media only screen and (min-width: 601px) { - header, - main { - padding: 0 calc((100% - 500px) / 2); - } -} - main:not(popup-page main) { position: absolute; top: 44px; diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index 6fc4793f5c0..63ede7ba357 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -6,7 +6,6 @@ import { MockProxy, mock } from "jest-mock-extended"; import { of, BehaviorSubject } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -21,6 +20,7 @@ import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { ButtonModule, NoItemsModule } from "@bitwarden/components"; import { NewSendDropdownComponent, diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts index 18482706272..3c3270e557c 100644 --- a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from "@angular/common"; import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { map, switchMap } from "rxjs"; +import { combineLatest, map, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { AnchorLinkDirective, CalloutModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -16,10 +17,26 @@ import { I18nPipe } from "@bitwarden/ui-common"; }) export class AtRiskPasswordCalloutComponent { private taskService = inject(TaskService); + private cipherService = inject(CipherService); private activeAccount$ = inject(AccountService).activeAccount$.pipe(getUserId); protected pendingTasks$ = this.activeAccount$.pipe( - switchMap((userId) => this.taskService.pendingTasks$(userId)), - map((tasks) => tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential)), + switchMap((userId) => + combineLatest([ + this.taskService.pendingTasks$(userId), + this.cipherService.cipherViews$(userId), + ]), + ), + map(([tasks, ciphers]) => + tasks.filter((t) => { + const associatedCipher = ciphers.find((c) => c.id === t.cipherId); + + return ( + t.type === SecurityTaskType.UpdateAtRiskCredential && + associatedCipher && + !associatedCipher.isDeleted + ); + }), + ), ); } diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts index dae00ba6c2b..eaa10aba624 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts @@ -203,6 +203,20 @@ describe("AtRiskPasswordsComponent", () => { expect(items).toHaveLength(1); expect(items[0].name).toBe("Item 1"); }); + + it("should not show tasks associated with deleted ciphers", async () => { + mockCiphers$.next([ + { + id: "cipher", + organizationId: "org", + name: "Item 1", + isDeleted: true, + } as CipherView, + ]); + + const items = await firstValueFrom(component["atRiskItems$"]); + expect(items).toHaveLength(0); + }); }); describe("pageDescription$", () => { @@ -245,6 +259,19 @@ describe("AtRiskPasswordsComponent", () => { type: SecurityTaskType.UpdateAtRiskCredential, } as SecurityTask, ]); + mockCiphers$.next([ + { + id: "cipher", + organizationId: "org", + name: "Item 1", + } as CipherView, + { + id: "cipher2", + organizationId: "org2", + name: "Item 2", + } as CipherView, + ]); + const description = await firstValueFrom(component["pageDescription$"]); expect(description).toBe("atRiskPasswordsDescMultiOrgPlural"); }); diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts index dc6712aa23f..1bfb65a15cc 100644 --- a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts +++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts @@ -155,32 +155,35 @@ export class AtRiskPasswordsComponent implements OnInit { (t) => t.type === SecurityTaskType.UpdateAtRiskCredential && t.cipherId != null && - ciphers[t.cipherId] != null, + ciphers[t.cipherId] != null && + !ciphers[t.cipherId].isDeleted, ) .map((t) => ciphers[t.cipherId!]), ), ); - protected pageDescription$ = this.activeUserData$.pipe( - switchMap(({ tasks, userId }) => { - const orgIds = new Set(tasks.map((t) => t.organizationId)); + protected pageDescription$ = combineLatest([this.activeUserData$, this.atRiskItems$]).pipe( + switchMap(([{ userId }, atRiskCiphers]) => { + const orgIds = new Set( + atRiskCiphers.filter((c) => c.organizationId).map((c) => c.organizationId), + ) as Set; if (orgIds.size === 1) { const [orgId] = orgIds; return this.organizationService.organizations$(userId).pipe( getOrganizationById(orgId), map((org) => this.i18nService.t( - tasks.length === 1 + atRiskCiphers.length === 1 ? "atRiskPasswordDescSingleOrg" : "atRiskPasswordsDescSingleOrgPlural", org?.name, - tasks.length, + atRiskCiphers.length, ), ), ); } - return of(this.i18nService.t("atRiskPasswordsDescMultiOrgPlural", tasks.length)); + return of(this.i18nService.t("atRiskPasswordsDescMultiOrgPlural", atRiskCiphers.length)); }), ); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts index cda055176e8..9564aeadc09 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts @@ -7,7 +7,6 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -19,6 +18,7 @@ import { StateProvider } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { PasswordRepromptService } from "@bitwarden/vault"; 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 a55bba622e4..87d13d4d18a 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 @@ -89,10 +89,7 @@ - + + + + diff --git a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts new file mode 100644 index 00000000000..3240b8d707a --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/account-recovery-dialog.component.ts @@ -0,0 +1,146 @@ +import { CommonModule } from "@angular/common"; +import { Component, Inject, ViewChild } from "@angular/core"; +import { switchMap } from "rxjs"; + +import { InputPasswordComponent, InputPasswordFlow } from "@bitwarden/auth/angular"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { + AsyncActionsModule, + ButtonModule, + CalloutModule, + DIALOG_DATA, + DialogConfig, + DialogModule, + DialogRef, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { OrganizationUserResetPasswordService } from "../../services/organization-user-reset-password/organization-user-reset-password.service"; + +/** + * Encapsulates a few key data inputs needed to initiate an account recovery + * process for the organization user in question. + */ +export type AccountRecoveryDialogData = { + /** + * The organization user's full name + */ + name: string; + + /** + * The organization user's email address + */ + email: string; + + /** + * The `organizationUserId` for the user + */ + organizationUserId: string; + + /** + * The organization's `organizationId` + */ + organizationId: OrganizationId; +}; + +export const AccountRecoveryDialogResultType = { + Ok: "ok", +} as const; + +export type AccountRecoveryDialogResultType = + (typeof AccountRecoveryDialogResultType)[keyof typeof AccountRecoveryDialogResultType]; + +/** + * Used in a dialog for initiating the account recovery process against a + * given organization user. An admin will access this form when they want to + * reset a user's password and log them out of sessions. + */ +@Component({ + standalone: true, + selector: "app-account-recovery-dialog", + templateUrl: "account-recovery-dialog.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CalloutModule, + CommonModule, + DialogModule, + I18nPipe, + InputPasswordComponent, + ], +}) +export class AccountRecoveryDialogComponent { + @ViewChild(InputPasswordComponent) + inputPasswordComponent: InputPasswordComponent | undefined = undefined; + + masterPasswordPolicyOptions$ = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)), + ); + + inputPasswordFlow = InputPasswordFlow.ChangePasswordDelegation; + + get loggedOutWarningName() { + return this.dialogData.name != null ? this.dialogData.name : this.i18nService.t("thisUser"); + } + + constructor( + @Inject(DIALOG_DATA) protected dialogData: AccountRecoveryDialogData, + private accountService: AccountService, + private dialogRef: DialogRef, + private i18nService: I18nService, + private policyService: PolicyService, + private resetPasswordService: OrganizationUserResetPasswordService, + private toastService: ToastService, + ) {} + + handlePrimaryButtonClick = async () => { + if (!this.inputPasswordComponent) { + throw new Error("InputPasswordComponent is not initialized"); + } + + const passwordInputResult = await this.inputPasswordComponent.submit(); + if (!passwordInputResult) { + return; + } + + await this.resetPasswordService.resetMasterPassword( + passwordInputResult.newPassword, + this.dialogData.email, + this.dialogData.organizationUserId, + this.dialogData.organizationId, + ); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("resetPasswordSuccess"), + }); + + this.dialogRef.close(AccountRecoveryDialogResultType.Ok); + }; + + /** + * Strongly typed helper to open an `AccountRecoveryDialogComponent` + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param dialogConfig Configuration for the dialog + */ + static open = ( + dialogService: DialogService, + dialogConfig: DialogConfig< + AccountRecoveryDialogData, + DialogRef + >, + ) => { + return dialogService.open( + AccountRecoveryDialogComponent, + dialogConfig, + ); + }; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/account-recovery/index.ts b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/index.ts new file mode 100644 index 00000000000..e15fb7b40ef --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/account-recovery/index.ts @@ -0,0 +1 @@ +export * from "./account-recovery-dialog.component"; diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 80f0745f6d5..961d5482d8a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -13,6 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { DIALOG_DATA, DialogConfig, @@ -47,7 +48,7 @@ export type ResetPasswordDialogData = { /** * The organization's `organizationId` */ - organizationId: string; + organizationId: OrganizationId; }; // FIXME: update to use a const object instead of a typescript enum @@ -56,16 +57,18 @@ export enum ResetPasswordDialogResult { Ok = "ok", } +/** + * Used in a dialog for initiating the account recovery process against a + * given organization user. An admin will access this form when they want to + * reset a user's password and log them out of sessions. + * + * @deprecated Use the `AccountRecoveryDialogComponent` instead. + */ @Component({ selector: "app-reset-password", templateUrl: "reset-password.component.html", standalone: false, }) -/** - * Used in a dialog for initiating the account recovery process against a - * given organization user. An admin will access this form when they want to - * reset a user's password and log them out of sessions. - */ export class ResetPasswordComponent implements OnInit, OnDestroy { formGroup = this.formBuilder.group({ newPassword: ["", Validators.required], diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 49c57f5e5a6..0247a8c881b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -52,6 +52,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -66,6 +67,10 @@ import { GroupApiService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; import { openEntityEventsDialog } from "../manage/entity-events.component"; +import { + AccountRecoveryDialogComponent, + AccountRecoveryDialogResultType, +} from "./components/account-recovery/account-recovery-dialog.component"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; @@ -243,10 +248,13 @@ export class MembersComponent extends BaseMembersComponent const separateCustomRolePermissionsEnabled$ = this.configService.getFeatureFlag$( FeatureFlag.SeparateCustomRolePermissions, ); - this.showUserManagementControls$ = separateCustomRolePermissionsEnabled$.pipe( + this.showUserManagementControls$ = combineLatest([ + separateCustomRolePermissionsEnabled$, + organization$, + ]).pipe( map( - (separateCustomRolePermissionsEnabled) => - !separateCustomRolePermissionsEnabled || this.organization.canManageUsers, + ([separateCustomRolePermissionsEnabled, organization]) => + !separateCustomRolePermissionsEnabled || organization.canManageUsers, ), ); } @@ -332,10 +340,9 @@ export class MembersComponent extends BaseMembersComponent if ( await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)) ) { - this.organizationUserService - .confirmUser(this.organization, user, publicKey) - .pipe(takeUntilDestroyed()) - .subscribe(); + await firstValueFrom( + this.organizationUserService.confirmUser(this.organization, user, publicKey), + ); } else { const orgKey = await this.keyService.getOrgKey(this.organization.id); const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey); @@ -749,11 +756,44 @@ export class MembersComponent extends BaseMembersComponent } async resetPassword(user: OrganizationUserView) { + const changePasswordRefactorFlag = await this.configService.getFeatureFlag( + FeatureFlag.PM16117_ChangeExistingPasswordRefactor, + ); + + if (changePasswordRefactorFlag) { + if (!user || !user.email || !user.id) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("orgUserDetailsNotFound"), + }); + this.logService.error("Org user details not found when attempting account recovery"); + + return; + } + + const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + email: user.email, + organizationId: this.organization.id as OrganizationId, + organizationUserId: user.id, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + if (result === AccountRecoveryDialogResultType.Ok) { + await this.load(); + } + + return; + } + const dialogRef = ResetPasswordComponent.open(this.dialogService, { data: { name: this.userNamePipe.transform(user), email: user != null ? user.email : null, - organizationId: this.organization.id, + organizationId: this.organization.id as OrganizationId, id: user != null ? user.id : null, }, }); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index ecf4d26eb52..d54e12c0ee7 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -14,7 +14,7 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { Argon2KdfConfig, @@ -96,7 +96,7 @@ export class OrganizationUserResetPasswordService newMasterPassword: string, email: string, orgUserId: string, - orgId: string, + orgId: OrganizationId, ): Promise { const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails( orgId, diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts index 31dfa865005..79efeebca2a 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user/organization-user.service.ts @@ -43,7 +43,7 @@ export class OrganizationUserService { ): Observable { const encryptedCollectionName$ = this.orgKey$(organization).pipe( switchMap((orgKey) => - this.encryptService.encryptString(this.i18nService.t("My Itmes"), orgKey), + this.encryptService.encryptString(this.i18nService.t("myItems"), orgKey), ), ); diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html index 088b5051fb1..e9b7ba39aa5 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html @@ -1,5 +1,3 @@ - -
{{ "permission" | i18n }} diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index b9f3b8c05b7..ada73dd0059 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -9,7 +9,6 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -26,6 +25,7 @@ import { NotificationsService } from "@bitwarden/common/platform/notifications"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService, BiometricStateService } from "@bitwarden/key-management"; diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts index 5539e3b76ea..8c556986225 100644 --- a/apps/web/src/app/auth/core/services/index.ts +++ b/apps/web/src/app/auth/core/services/index.ts @@ -2,6 +2,7 @@ export * from "./change-password"; export * from "./login"; export * from "./login-decryption-options"; export * from "./webauthn-login"; +export * from "./password-management"; export * from "./set-password-jit"; export * from "./registration"; export * from "./two-factor-auth"; diff --git a/apps/web/src/app/auth/core/services/password-management/index.ts b/apps/web/src/app/auth/core/services/password-management/index.ts new file mode 100644 index 00000000000..1444fd024af --- /dev/null +++ b/apps/web/src/app/auth/core/services/password-management/index.ts @@ -0,0 +1 @@ +export * from "./set-initial-password/web-set-initial-password.service"; diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts new file mode 100644 index 00000000000..b90d0624b3f --- /dev/null +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts @@ -0,0 +1,208 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + SetInitialPasswordCredentials, + SetInitialPasswordService, + SetInitialPasswordUserType, +} from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; +import { + FakeUserDecryptionOptions as UserDecryptionOptions, + InternalUserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { AcceptOrganizationInviteService } from "@bitwarden/web-vault/app/auth/organization-invite/accept-organization.service"; +import { RouterService } from "@bitwarden/web-vault/app/core"; + +import { WebSetInitialPasswordService } from "./web-set-initial-password.service"; + +describe("WebSetInitialPasswordService", () => { + let sut: SetInitialPasswordService; + + let apiService: MockProxy; + let encryptService: MockProxy; + let i18nService: MockProxy; + let kdfConfigService: MockProxy; + let keyService: MockProxy; + let masterPasswordApiService: MockProxy; + let masterPasswordService: MockProxy; + let organizationApiService: MockProxy; + let organizationUserApiService: MockProxy; + let userDecryptionOptionsService: MockProxy; + let acceptOrganizationInviteService: MockProxy; + let routerService: MockProxy; + + beforeEach(() => { + apiService = mock(); + encryptService = mock(); + i18nService = mock(); + kdfConfigService = mock(); + keyService = mock(); + masterPasswordApiService = mock(); + masterPasswordService = mock(); + organizationApiService = mock(); + organizationUserApiService = mock(); + userDecryptionOptionsService = mock(); + acceptOrganizationInviteService = mock(); + routerService = mock(); + + sut = new WebSetInitialPasswordService( + apiService, + encryptService, + i18nService, + kdfConfigService, + keyService, + masterPasswordApiService, + masterPasswordService, + organizationApiService, + organizationUserApiService, + userDecryptionOptionsService, + acceptOrganizationInviteService, + routerService, + ); + }); + + it("should instantiate", () => { + expect(sut).not.toBeFalsy(); + }); + + describe("setInitialPassword(...)", () => { + // Mock function parameters + let credentials: SetInitialPasswordCredentials; + let userType: SetInitialPasswordUserType; + let userId: UserId; + + // Mock other function data + let userKey: UserKey; + let userKeyEncString: EncString; + let masterKeyEncryptedUserKey: [UserKey, EncString]; + + let keyPair: [string, EncString]; + let keysRequest: KeysRequest; + + let userDecryptionOptions: UserDecryptionOptions; + let userDecryptionOptionsSubject: BehaviorSubject; + let setPasswordRequest: SetPasswordRequest; + + beforeEach(() => { + // Mock function parameters + credentials = { + newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey, + newServerMasterKeyHash: "newServerMasterKeyHash", + newLocalMasterKeyHash: "newLocalMasterKeyHash", + newPasswordHint: "newPasswordHint", + kdfConfig: DEFAULT_KDF_CONFIG, + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId", + resetPasswordAutoEnroll: false, + }; + userId = "userId" as UserId; + userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; + + // Mock other function data + userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; + userKeyEncString = new EncString("masterKeyEncryptedUserKey"); + masterKeyEncryptedUserKey = [userKey, userKeyEncString]; + + keyPair = ["publicKey", new EncString("privateKey")]; + keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); + + userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true }); + userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions); + userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + + setPasswordRequest = new SetPasswordRequest( + credentials.newServerMasterKeyHash, + masterKeyEncryptedUserKey[1].encryptedString, + credentials.newPasswordHint, + credentials.orgSsoIdentifier, + keysRequest, + credentials.kdfConfig.kdfType, + credentials.kdfConfig.iterations, + ); + }); + + function setupMocks() { + // Mock makeMasterKeyEncryptedUserKey() values + keyService.userKey$.mockReturnValue(of(userKey)); + keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey); + + // Mock keyPair values + keyService.userPrivateKey$.mockReturnValue(of(null)); + keyService.userPublicKey$.mockReturnValue(of(null)); + keyService.makeKeyPair.mockResolvedValue(keyPair); + } + + describe("given the initial password was successfully set", () => { + it("should call routerService.getAndClearLoginRedirectUrl()", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1); + }); + + it("should call acceptOrganizationInviteService.clearOrganizationInvitation()", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(acceptOrganizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes( + 1, + ); + }); + }); + + describe("given the initial password was NOT successfully set (due to some error in setInitialPassword())", () => { + it("should NOT call routerService.getAndClearLoginRedirectUrl()", async () => { + // Arrange + credentials.newMasterKey = null; // will trigger an error in setInitialPassword() + setupMocks(); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow(); + expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled(); + expect(routerService.getAndClearLoginRedirectUrl).not.toHaveBeenCalled(); + }); + + it("should NOT call acceptOrganizationInviteService.clearOrganizationInvitation()", async () => { + // Arrange + credentials.newMasterKey = null; // will trigger an error in setInitialPassword() + setupMocks(); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow(); + expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled(); + expect(acceptOrganizationInviteService.clearOrganizationInvitation).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts new file mode 100644 index 00000000000..41e7e8ad4ab --- /dev/null +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.ts @@ -0,0 +1,83 @@ +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation"; +import { + SetInitialPasswordCredentials, + SetInitialPasswordService, + SetInitialPasswordUserType, +} from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { AcceptOrganizationInviteService } from "@bitwarden/web-vault/app/auth/organization-invite/accept-organization.service"; +import { RouterService } from "@bitwarden/web-vault/app/core"; + +export class WebSetInitialPasswordService + extends DefaultSetInitialPasswordService + implements SetInitialPasswordService +{ + constructor( + protected apiService: ApiService, + protected encryptService: EncryptService, + protected i18nService: I18nService, + protected kdfConfigService: KdfConfigService, + protected keyService: KeyService, + protected masterPasswordApiService: MasterPasswordApiService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected organizationUserApiService: OrganizationUserApiService, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, + private acceptOrganizationInviteService: AcceptOrganizationInviteService, + private routerService: RouterService, + ) { + super( + apiService, + encryptService, + i18nService, + kdfConfigService, + keyService, + masterPasswordApiService, + masterPasswordService, + organizationApiService, + organizationUserApiService, + userDecryptionOptionsService, + ); + } + + override async setInitialPassword( + credentials: SetInitialPasswordCredentials, + userType: SetInitialPasswordUserType, + userId: UserId, + ) { + await super.setInitialPassword(credentials, userType, userId); + + /** + * TODO: Investigate refactoring the following logic in https://bitwarden.atlassian.net/browse/PM-22615 + * --- + * When a user has been invited to an org, they can be accepted into the org in two different ways: + * + * 1) By clicking the email invite link, which triggers the normal AcceptOrganizationComponent flow + * a. This flow sets an org invite in state + * b. However, if the user does not already have an account AND the org has SSO enabled AND the require + * SSO policy enabled, the AcceptOrganizationComponent will send the user to /sso to accelerate + * the user through the SSO JIT provisioning process (see #2 below) + * + * 2) By logging in via SSO, which triggers the JIT provisioning process + * a. This flow does NOT (itself) set an org invite in state + * b. The set initial password process on the server accepts the user into the org after successfully + * setting the password (see server - SetInitialMasterPasswordCommand.cs) + * + * If a user clicks the email link but gets accelerated through the SSO JIT process (see 1b), + * the SSO JIT process will accept the user into the org upon setting their initial password (see 2b), + * at which point we must remember to clear the deep linked URL used for accepting the org invite, as well + * as clear the org invite itself that was originally set in state by the AcceptOrganizationComponent. + */ + await this.routerService.getAndClearLoginRedirectUrl(); + await this.acceptOrganizationInviteService.clearOrganizationInvitation(); + } +} diff --git a/apps/web/src/app/auth/settings/security/device-management.component.spec.ts b/apps/web/src/app/auth/settings/security/device-management.component.spec.ts index d86123f52be..2821d4a6d76 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.spec.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { RouterTestingModule } from "@angular/router/testing"; import { of, Subject } from "rxjs"; -import { AuthRequestApiService } from "@bitwarden/auth/common"; +import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; import { DeviceType } from "@bitwarden/common/enums"; @@ -79,7 +79,7 @@ describe("DeviceManagementComponent", () => { }, }, { - provide: AuthRequestApiService, + provide: AuthRequestApiServiceAbstraction, useValue: { getAuthRequest: jest.fn().mockResolvedValue(mockDeviceResponse), }, diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts index c831d26ea16..854a13faa99 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -4,7 +4,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { firstValueFrom } from "rxjs"; import { LoginApprovalComponent } from "@bitwarden/auth/angular"; -import { AuthRequestApiService } from "@bitwarden/auth/common"; +import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicePendingAuthRequest, @@ -61,7 +61,7 @@ export class DeviceManagementComponent { private toastService: ToastService, private validationService: ValidationService, private messageListener: MessageListener, - private authRequestApiService: AuthRequestApiService, + private authRequestApiService: AuthRequestApiServiceAbstraction, private destroyRef: DestroyRef, ) { void this.initializeDevices(); diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html index 7f093842b6a..7a1ca2cd83d 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html @@ -38,13 +38,17 @@ [loading]="loading && (trialPaymentOptional$ | async)" (click)="orgNameEntrySubmit()" > - {{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }} + {{ + (trialPaymentOptional$ | async) && trialLength > 0 + ? ("startTrial" | i18n) + : ("next" | i18n) + }} { const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$); - if (isTrialPaymentOptional) { + /** Only skip payment if the flag is on AND trialLength > 0 */ + if (isTrialPaymentOptional && this.trialLength > 0) { await this.createOrganizationOnTrial(); } else { await this.conditionallyCreateOrganization(); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 46435981a5e..b6a6ca102d8 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -10,6 +10,7 @@ import { OrganizationUserApiService, CollectionService, } from "@bitwarden/admin-console/common"; +import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { CLIENT_TYPE, @@ -117,6 +118,7 @@ import { WebLoginDecryptionOptionsService, WebTwoFactorAuthDuoComponentService, LinkSsoService, + WebSetInitialPasswordService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; @@ -283,6 +285,24 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, ], }), + safeProvider({ + provide: SetInitialPasswordService, + useClass: WebSetInitialPasswordService, + deps: [ + ApiService, + EncryptService, + I18nServiceAbstraction, + KdfConfigService, + KeyServiceAbstraction, + MasterPasswordApiService, + InternalMasterPasswordServiceAbstraction, + OrganizationApiServiceAbstraction, + OrganizationUserApiService, + InternalUserDecryptionOptionsServiceAbstraction, + AcceptOrganizationInviteService, + RouterService, + ], + }), safeProvider({ provide: AppIdService, useClass: DefaultAppIdService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 783fe6ada0a..615bb545811 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,6 +10,8 @@ import { unauthGuardFn, activeAuthGuard, } from "@bitwarden/angular/auth/guards"; +import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { PasswordHintComponent, RegistrationFinishComponent, @@ -36,6 +38,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { VaultIcons } from "@bitwarden/vault"; @@ -305,6 +308,14 @@ const routes: Routes = [ }, ], }, + { + path: "set-initial-password", + canActivate: [canAccessFeature(FeatureFlag.PM16117_SetInitialPasswordRefactor), authGuard], + component: SetInitialPasswordComponent, + data: { + maxWidth: "lg", + } satisfies AnonLayoutWrapperData, + }, { path: "set-password-jit", component: SetPasswordJitComponent, @@ -347,7 +358,6 @@ const routes: Routes = [ pageSubtitle: { key: "singleSignOnEnterOrgIdentifierText", }, - titleAreaMaxWidth: "md", pageIcon: SsoKeyIcon, } satisfies RouteDataProperties & AnonLayoutWrapperData, children: [ @@ -381,7 +391,6 @@ const routes: Routes = [ pageTitle: { key: "verifyYourIdentity", }, - titleAreaMaxWidth: "md", } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { diff --git a/apps/web/src/app/platform/ipc/web-ipc.service.ts b/apps/web/src/app/platform/ipc/web-ipc.service.ts index 06f3c660218..590c1f36cc4 100644 --- a/apps/web/src/app/platform/ipc/web-ipc.service.ts +++ b/apps/web/src/app/platform/ipc/web-ipc.service.ts @@ -1,17 +1,20 @@ import { inject } from "@angular/core"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { IpcMessage, IpcService, isIpcMessage } from "@bitwarden/common/platform/ipc"; import { IncomingMessage, IpcClient, IpcCommunicationBackend, + ipcRegisterDiscoverHandler, OutgoingMessage, } from "@bitwarden/sdk-internal"; export class WebIpcService extends IpcService { private logService = inject(LogService); + private platformUtilsService = inject(PlatformUtilsService); private communicationBackend?: IpcCommunicationBackend; override async init() { @@ -68,6 +71,12 @@ export class WebIpcService extends IpcService { }); await super.initWithClient(new IpcClient(this.communicationBackend)); + + if (this.platformUtilsService.isDev()) { + await ipcRegisterDiscoverHandler(this.client, { + version: await this.platformUtilsService.getApplicationVersion(), + }); + } } catch (e) { this.logService.error("[IPC] Initialization failed", e); } diff --git a/apps/web/src/app/tools/send/send.component.ts b/apps/web/src/app/tools/send/send.component.ts index 3d42b3182f8..b74a3b80ee3 100644 --- a/apps/web/src/app/tools/send/send.component.ts +++ b/apps/web/src/app/tools/send/send.component.ts @@ -4,7 +4,6 @@ import { Component, NgZone, OnInit, OnDestroy } from "@angular/core"; import { lastValueFrom } from "rxjs"; import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -16,6 +15,7 @@ import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { SendId } from "@bitwarden/common/types/guid"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { DialogRef, DialogService, diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 992c9c26bf3..ef928903a72 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -22,16 +22,12 @@ bitCell bitSortable="name" [fn]="sortByName" - [class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'" + [class]="showExtraColumn ? 'tw-w-3/5' : 'tw-w-full'" > {{ "name" | i18n }} - + {{ "name" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 9d94fb044b5..3793db6f76a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -382,19 +382,22 @@ export class VaultItemsComponent { } if (this.selection.selected.length === 0) { - return true; + return false; } const hasPersonalItems = this.hasPersonalItems(); const uniqueCipherOrgIds = this.getUniqueOrganizationIds(); + const hasEditableCollections = this.allCollections.some((collection) => { + return !collection.readOnly; + }); // Return false if items are from different organizations if (uniqueCipherOrgIds.size > 1) { return false; } - // If all items are personal, return based on personal items - if (uniqueCipherOrgIds.size === 0) { + // If all selected items are personal, return based on personal items + if (uniqueCipherOrgIds.size === 0 && hasEditableCollections) { return hasPersonalItems; } @@ -406,7 +409,11 @@ export class VaultItemsComponent { const collectionNotSelected = this.selection.selected.filter((item) => item.collection).length === 0; - return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected; + return ( + (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && + collectionNotSelected && + hasEditableCollections + ); } /** diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts index 4ce65b9f771..c97b23b1456 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.spec.ts @@ -1,16 +1,19 @@ import { TestBed } from "@angular/core/testing"; -import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs"; import { + AuthRequestServiceAbstraction, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; -import { DeviceResponse } from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; +import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DeviceType } from "@bitwarden/common/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; @@ -41,11 +44,15 @@ describe("VaultBannersService", () => { [userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo, }); const devices$ = new BehaviorSubject([]); + const pendingAuthRequests$ = new BehaviorSubject>([]); + let configService: MockProxy; beforeEach(() => { lastSync$.next(new Date("2024-05-14")); isSelfHost.mockClear(); getEmailVerified.mockClear().mockResolvedValue(true); + configService = mock(); + configService.getFeatureFlag$.mockImplementation(() => of(true)); TestBed.configureTestingModule({ providers: [ @@ -88,6 +95,14 @@ describe("VaultBannersService", () => { provide: DevicesServiceAbstraction, useValue: { getDevices$: () => devices$ }, }, + { + provide: AuthRequestServiceAbstraction, + useValue: { getPendingAuthRequests$: () => pendingAuthRequests$ }, + }, + { + provide: ConfigService, + useValue: configService, + }, ], }); }); @@ -286,31 +301,25 @@ describe("VaultBannersService", () => { describe("PendingAuthRequest", () => { const now = new Date(); - let deviceResponse: DeviceResponse; + let authRequestResponse: AuthRequestResponse; beforeEach(() => { - deviceResponse = new DeviceResponse({ - Id: "device1", - UserId: userId, - Name: "Test Device", - Identifier: "test-device", - Type: DeviceType.Android, - CreationDate: now.toISOString(), - RevisionDate: now.toISOString(), - IsTrusted: false, + authRequestResponse = new AuthRequestResponse({ + id: "authRequest1", + deviceId: "device1", + deviceName: "Test Device", + deviceType: DeviceType.Android, + creationDate: now.toISOString(), + requestApproved: null, }); // Reset devices list, single user state, and active user state before each test - devices$.next([]); + pendingAuthRequests$.next([]); fakeStateProvider.singleUser.states.clear(); fakeStateProvider.activeUser.states.clear(); }); it("shows pending auth request banner when there is a pending request", async () => { - deviceResponse.devicePendingAuthRequest = { - id: "123", - creationDate: now.toISOString(), - }; - devices$.next([new DeviceView(deviceResponse)]); + pendingAuthRequests$.next([new AuthRequestResponse(authRequestResponse)]); service = TestBed.inject(VaultBannersService); @@ -318,8 +327,7 @@ describe("VaultBannersService", () => { }); it("does not show pending auth request banner when there are no pending requests", async () => { - deviceResponse.devicePendingAuthRequest = null; - devices$.next([new DeviceView(deviceResponse)]); + pendingAuthRequests$.next([]); service = TestBed.inject(VaultBannersService); @@ -327,11 +335,7 @@ describe("VaultBannersService", () => { }); it("dismisses pending auth request banner", async () => { - deviceResponse.devicePendingAuthRequest = { - id: "123", - creationDate: now.toISOString(), - }; - devices$.next([new DeviceView(deviceResponse)]); + pendingAuthRequests$.next([new AuthRequestResponse(authRequestResponse)]); service = TestBed.inject(VaultBannersService); diff --git a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts index 17aaf5271ba..dd50c832cc6 100644 --- a/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-banners/services/vault-banners.service.ts @@ -1,10 +1,15 @@ import { Injectable } from "@angular/core"; import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs"; -import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { + AuthRequestServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateProvider, @@ -66,20 +71,33 @@ export class VaultBannersService { private syncService: SyncService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private devicesService: DevicesServiceAbstraction, + private authRequestService: AuthRequestServiceAbstraction, + private configService: ConfigService, ) {} /** Returns true when the pending auth request banner should be shown */ async shouldShowPendingAuthRequestBanner(userId: UserId): Promise { - const devices = await firstValueFrom(this.devicesService.getDevices$()); - const hasPendingRequest = devices.some( - (device) => device.response?.devicePendingAuthRequest != null, - ); - const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes( VisibleVaultBanner.PendingAuthRequest, ); + // TODO: PM-20439 remove feature flag + const browserLoginApprovalFeatureFlag = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval), + ); + if (browserLoginApprovalFeatureFlag === true) { + const pendingAuthRequests = await firstValueFrom( + this.authRequestService.getPendingAuthRequests$(), + ); - return hasPendingRequest && !alreadyDismissed; + return pendingAuthRequests.length > 0 && !alreadyDismissed; + } else { + const devices = await firstValueFrom(this.devicesService.getDevices$()); + const hasPendingRequest = devices.some( + (device) => device.response?.devicePendingAuthRequest != null, + ); + + return hasPendingRequest && !alreadyDismissed; + } } shouldShowPremiumBanner$(userId: UserId): Observable { diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 29ad0ead621..52c4bcef01b 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -36,7 +36,6 @@ import { import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { getOrganizationById, @@ -49,7 +48,6 @@ import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -60,6 +58,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; @@ -85,7 +84,6 @@ import { import { getNestedCollectionTree, getFlatCollectionTree, - getNestedCollectionTree_vNext, } from "../../admin-console/organizations/collections"; import { CollectionDialogAction, @@ -331,15 +329,8 @@ export class VaultComponent implements OnInit, OnDestroy { const filter$ = this.routedVaultFilterService.filter$; const allCollections$ = this.collectionService.decryptedCollections$; - const nestedCollections$ = combineLatest([ - allCollections$, - this.configService.getFeatureFlag$(FeatureFlag.OptimizeNestedTraverseTypescript), - ]).pipe( - map(([collections, shouldOptimize]) => - shouldOptimize - ? getNestedCollectionTree_vNext(collections) - : getNestedCollectionTree(collections), - ), + const nestedCollections$ = allCollections$.pipe( + map((collections) => getNestedCollectionTree(collections)), ); this.searchText$ diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts new file mode 100644 index 00000000000..68a9ca6d099 --- /dev/null +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.spec.ts @@ -0,0 +1,111 @@ +import { fakeAsync, TestBed, tick } from "@angular/core/testing"; + +import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; +import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; + +import { WebBrowserInteractionService } from "./web-browser-interaction.service"; + +describe("WebBrowserInteractionService", () => { + let service: WebBrowserInteractionService; + const postMessage = jest.fn(); + window.postMessage = postMessage; + + const dispatchEvent = (command: string) => { + window.dispatchEvent(new MessageEvent("message", { data: { command } })); + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [WebBrowserInteractionService], + }); + + postMessage.mockClear(); + + service = TestBed.inject(WebBrowserInteractionService); + }); + + describe("extensionInstalled$", () => { + it("posts a message to check for the extension", () => { + service.extensionInstalled$.subscribe(); + + expect(postMessage).toHaveBeenCalledWith({ + command: VaultMessages.checkBwInstalled, + }); + }); + + it("returns false after the timeout", fakeAsync(() => { + service.extensionInstalled$.subscribe((installed) => { + expect(installed).toBe(false); + }); + + tick(1500); + })); + + it("returns true when the extension is installed", (done) => { + service.extensionInstalled$.subscribe((installed) => { + expect(installed).toBe(true); + done(); + }); + + dispatchEvent(VaultMessages.HasBwInstalled); + }); + + it("continues to listen for extension state changes after the first response", fakeAsync(() => { + const results: boolean[] = []; + + service.extensionInstalled$.subscribe((installed) => { + results.push(installed); + }); + + // initial timeout, should emit false + tick(1500); + expect(results[0]).toBe(false); + + // then emit `HasBwInstalled` + dispatchEvent(VaultMessages.HasBwInstalled); + tick(); + expect(results[1]).toBe(true); + })); + }); + + describe("openExtension", () => { + it("posts a message to open the extension", fakeAsync(() => { + service.openExtension().catch(() => {}); + + expect(postMessage).toHaveBeenCalledWith({ + command: VaultMessages.OpenBrowserExtensionToUrl, + }); + + tick(1500); + })); + + it("posts a message with the passed page", fakeAsync(() => { + service.openExtension(ExtensionPageUrls.Index).catch(() => {}); + + expect(postMessage).toHaveBeenCalledWith({ + command: VaultMessages.OpenBrowserExtensionToUrl, + url: ExtensionPageUrls.Index, + }); + + tick(1500); + })); + + it("resolves when the extension opens", async () => { + const openExtensionPromise = service.openExtension().catch(() => { + fail(); + }); + + dispatchEvent(VaultMessages.PopupOpened); + + await openExtensionPromise; + }); + + it("rejects if the extension does not open within the timeout", fakeAsync(() => { + service.openExtension().catch((error) => { + expect(error).toBe("Failed to open the extension"); + }); + + tick(1500); + })); + }); +}); diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.ts new file mode 100644 index 00000000000..46c566140e4 --- /dev/null +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.ts @@ -0,0 +1,76 @@ +import { DestroyRef, inject, Injectable } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { concatWith, filter, fromEvent, map, Observable, race, take, tap, timer } from "rxjs"; + +import { ExtensionPageUrls } from "@bitwarden/common/vault/enums"; +import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; + +/** + * The amount of time in milliseconds to wait for a response from the browser extension. + * NOTE: This value isn't computed by any means, it is just a reasonable timeout for the extension to respond. + */ +const MESSAGE_RESPONSE_TIMEOUT_MS = 1500; + +@Injectable({ + providedIn: "root", +}) +export class WebBrowserInteractionService { + destroyRef = inject(DestroyRef); + + private messages$ = fromEvent(window, "message").pipe( + takeUntilDestroyed(this.destroyRef), + ); + + /** Emits the installation status of the extension. */ + extensionInstalled$ = this.checkForExtension().pipe( + concatWith( + this.messages$.pipe( + filter((event) => event.data.command === VaultMessages.HasBwInstalled), + map(() => true), + ), + ), + ); + + /** Attempts to open the extension, rejects if the extension is not installed or it fails to open. */ + openExtension = (url?: ExtensionPageUrls) => { + return new Promise((resolve, reject) => { + race( + this.messages$.pipe( + filter((event) => event.data.command === VaultMessages.PopupOpened), + map(() => true), + ), + timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), + ) + .pipe(take(1)) + .subscribe((didOpen) => { + if (!didOpen) { + return reject("Failed to open the extension"); + } + + resolve(); + }); + + window.postMessage({ command: VaultMessages.OpenBrowserExtensionToUrl, url }); + }); + }; + + /** Sends a message via the window object to check if the extension is installed */ + private checkForExtension(): Observable { + const checkForExtension$ = race( + this.messages$.pipe( + filter((event) => event.data.command === VaultMessages.HasBwInstalled), + map(() => true), + ), + timer(MESSAGE_RESPONSE_TIMEOUT_MS).pipe(map(() => false)), + ).pipe( + tap({ + subscribe: () => { + window.postMessage({ command: VaultMessages.checkBwInstalled }); + }, + }), + take(1), + ); + + return checkForExtension$; + } +} diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index e1ee9515030..9350bcfc0ff 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Deaktiveer" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Verstekversameling" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Kry Hulp" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Noodtoegang afgekeur" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Persoonlike eienaarskap" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "hierdie gebruiker" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Een of meer organisasiebeleide stel die volgende eise aan die hoofwagwoord:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index fcbdbb57b4a..f8a6dc1288b 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "إيقاف" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "إلغاء الوصول" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "المساعدة" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "هذا المستخدم" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "تمت إعادة تعيين كلمة المرور بنجاح!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index f8c76e729fa..062f0f35ff4 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "İki addımlı girişi qurmaq, Bitwarden hesabınızı birdəfəlik kilidləyə bilər. Geri qaytarma kodu, normal iki addımlı giriş provayderinizi artıq istifadə edə bilmədiyiniz hallarda (məs. cihazınızı itirəndə) hesabınıza müraciət etməyinizə imkan verir. Hesabınıza müraciəti itirsəniz, Bitwarden dəstəyi sizə kömək edə bilməyəcək. Geri qaytarma kodunuzu bir yerə yazmağınızı və ya çap etməyinizi və onu etibarlı bir yerdə saxlamağınızı məsləhət görürük." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Kart element növünü sil" }, - "restrictedItemTypesPolicyDesc": { - "message": "Üzvlərin kart element növünü yaratmasına icazə verilməsin." + "restrictedItemTypePolicyDesc": { + "message": "Üzvlərin kart element növlərini yaratmasına icazə verməyin. Mövcud kartlar avtomatik silinəcək." + }, + "restrictCardTypeImport": { + "message": "Kart element növləri daxilə köçürülə bilmir" + }, + "restrictCardTypeImportDesc": { + "message": "1 və ya daha çox təşkilat tərəfindən təyin edilən bir siyasət, kartların seyfinizə köçürülməsini əngəlləyir." }, "yourSingleUseRecoveryCode": { "message": "İki addımlı giriş provayderinizə müraciəti itirdiyiniz halda, iki addımlı girişi söndürmək üçün təkistifadəlik geri qaytarma kodunu istifadə edə bilərsiniz. Bitwarden tövsiyə edir ki, geri qaytarma kodunuzu bir yerə yazıb güvənli bir yerdə saxlayın." @@ -2218,6 +2224,9 @@ "disable": { "message": "Sıradan çıxart" }, + "orgUserDetailsNotFound": { + "message": "Üzv təfsilatları tapılmadı." + }, "revokeAccess": { "message": "Müraciəti ləğv et" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "İlkin kolleksiya" }, + "myItems": { + "message": "Elementlərim" + }, "getHelp": { "message": "Kömək alın" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Fövqəladə hal müraciəti rədd edildi" }, + "grantorDetailsNotFound": { + "message": "Qrant verən təfsilatları tapılmadı" + }, "passwordResetFor": { "message": "$USER$ üçün parol sıfırlandı. Artıq yeni parol ilə giriş edə bilərsiniz.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Təşkilata data üzərində məcburi sahiblik ver" + }, "personalOwnership": { "message": "Fərdi sahiblik" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Davam etsəniz, $NAME$ üçün hazırkı seans bitəcək, təkrar giriş etməsi tələb olunacaq. Digər cihazlardakı aktiv seanslar, bir saata qədər aktiv qalmağa davam edə bilər.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "bu istifadəçi" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Bir və ya daha çox təşkilat siyasəti, aşağıdakı tələbləri qarşılamaq üçün ana parolu tələb edir:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Bir və ya daha çox təşkilat siyasəti, aşağıdakı tələbləri qarşılamaq üçün ana parolu tələb edir:" + }, "resetPasswordSuccess": { "message": "Parol sıfırlama uğurludur!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Kredit əlavə etmək üçün faktura ünvanı tələb olunur.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 485764d33f1..05c49e30b44 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Уключэнне двухэтапнага ўваходу можа цалкам заблакіраваць доступ да ўліковага запісу Bitwarden. Код аднаўлення дае магчымасць атрымаць доступ да вашага ўліковага запісу ў выпадку, калі вы не можаце скарыстацца звычайным спосабам пастаўшчыка двухэтапнага ўваходу (напрыклад, вы згубілі сваю прыладу). Падтрымка Bitwarden не зможа вам дапамагчы, калі вы згубіце доступ да свайго ўліковага запісу. Мы рэкамендуем вам запісаць або раздрукаваць код аднаўлення і захоўваць яго ў надзейным месцы." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Адключыць" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Адклікаць доступ" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Прадвызначаная калекцыя" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Атрымаць даведку" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Экстранны доступ адхілены" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Пароль скінуты для $USER$. Цяпер вы можаце ўвайсці з дапамогай новага пароля.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Выдаліць асабістае сховішча" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "гэты карыстальнік" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Адна або больш палітык арганізацыі патрабуе, каб асноўны пароль адпавядаў наступным патрабаванням:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Пароль паспяхова скінуты!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 29b076ce2ec..d3c7604f2d7 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Включването на двустепенна идентификация може завинаги да предотврати вписването ви в абонамента към Битуорден. Кодът за възстановяване ще ви позволи да достъпите абонамента дори и да имате проблем с доставчика на двустепенна идентификация (напр. ако изгубите устройството си). Дори и екипът по поддръжката към няма да ви помогне в такъв случай. Силно препоръчваме да отпечатате или запишете кодовете и да ги пазете на надеждно място." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Премахване на елемента за карти" }, - "restrictedItemTypesPolicyDesc": { - "message": "Да не се позволява на членовете да създават елементи от тип „карта“." + "restrictedItemTypePolicyDesc": { + "message": "Да не се разрешава на членовете да създават картови елементи. Съществуващите карти ще бъдат премахнати автоматично." + }, + "restrictCardTypeImport": { + "message": "Картовите елементи не могат да бъдат внесени" + }, + "restrictCardTypeImportDesc": { + "message": "Политика, зададена от 1 или повече организации, не позволява да внасяте карти в трезорите си." }, "yourSingleUseRecoveryCode": { "message": "Вашият еднократен код за възстановяване може да бъде използван, за да изключите двустепенното удостоверяване, в случай че нямате достъп до доставчика си за двустепенно вписване. Битуорден препоръчва да запишете кода си за възстановяване и да го пазите на сигурно място." @@ -2218,6 +2224,9 @@ "disable": { "message": "Изключване" }, + "orgUserDetailsNotFound": { + "message": "Няма намерени подробности за члена." + }, "revokeAccess": { "message": "Отнемане на достъпа" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Стандартна колекция" }, + "myItems": { + "message": "Моите елементи" + }, "getHelp": { "message": "Помощ" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Извънредният достъп е отказан." }, + "grantorDetailsNotFound": { + "message": "Няма намерени подробности за Gantor" + }, "passwordResetFor": { "message": "Паролата на потребителя $USER$ е сменена и той може вече да влезе с новата.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Задължителна собственост на организационните данни" + }, "personalOwnership": { "message": "Индивидуално притежание" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Действието ще прекрати текущата сесия на $NAME$, след което ще се наложи той/тя отново да се впише. Активните сесии на другите устройства може да останат такива до един час.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "този потребител" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Поне една политика на организация има следните изисквания към главната парола:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Поне една политика на организация има следните изисквания към главната парола:" + }, "resetPasswordSuccess": { "message": "Успешна смяна на паролата!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Адресът за таксуване е задължителен за добавянето на средства.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index 8b80336304f..c8539b19f79 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 71f91d57180..7c611e9ad5a 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 6603ce8dbf8..0b5d5e96988 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -502,7 +502,7 @@ "message": "Nom de la carpeta" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Imbriqueu una carpeta afegint el nom de la carpeta principal seguit d'una \"/\". Exemple: Social/Fòrums" }, "deleteFolderPermanently": { "message": "Are you sure you want to permanently delete this folder?" @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Si habiliteu l'inici de sessió en dues passes, pot bloquejar-vos de manera definitiva el compte de Bitwarden. Un codi de recuperació us permet accedir al vostre compte en cas que no pugueu utilitzar el proveïdor d'inici de sessió en dues passes (p. Ex. Perdre el dispositiu). El suport de Bitwarden no podrà ajudar-vos si perdeu l'accés al vostre compte. Us recomanem que escriviu o imprimiu el codi de recuperació i el mantingueu en un lloc segur." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Inhabilita" }, + "orgUserDetailsNotFound": { + "message": "No s'han trobat les dades del membre." + }, "revokeAccess": { "message": "Revoca l'accés" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Col·lecció per defecte" }, + "myItems": { + "message": "Els meus elements" + }, "getHelp": { "message": "Obteniu ajuda" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Accés d’emergència rebutjat" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Restabliment de la contrasenya per a $USER$. Ara podeu iniciar la sessió amb la nova contrasenya.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Suprimeix la caixa forta individual" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "aquest usuari" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Una o més polítiques d’organització requereixen que la vostra contrasenya principal complisca els requisits següents:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "S'ha restablert la contrasenya correctament!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 12731ea382e..8a80664c6c2 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Nastavením dvoufázového přihlášení můžete sami sobě znemožnit přihlášení k Vašemu účtu. Obnovovací kód umožňuje přístup do Vašeho účtu i v případě, pokud již nemůžete použít svůj normální způsob dvoufázového přihlášení (např. ztráta zařízení). Pokud ztratíte přístup ke svému účtu, nebude Vám schopna pomoci ani zákaznická podpora Bitwardenu. Doporučujeme si proto kód pro obnovení zapsat nebo vytisknout a uložit jej na bezpečném místě." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Odebrat typ položky karty" }, - "restrictedItemTypesPolicyDesc": { - "message": "Nedovolí členům vytvářet typy položek karet." + "restrictedItemTypePolicyDesc": { + "message": "Nepovolovat členům vytvářet typy položek karty. Existující karty budou automaticky odebrány." + }, + "restrictCardTypeImport": { + "message": "Nelze importovat typy položek karty" + }, + "restrictCardTypeImportDesc": { + "message": "Zásady nastavené 1 nebo více organizací Vám brání v importu karet do Vašeho trezoru." }, "yourSingleUseRecoveryCode": { "message": "Jednorázový kód pro obnovení lze použít k vypnutí dvoufázového přihlašování v případě, že ztratíte přístup ke svému poskytovateli dvoufázového přihlašování. Bitwarden doporučuje, abyste si kód pro obnovení zapsali a uložili na bezpečném místě." @@ -2218,6 +2224,9 @@ "disable": { "message": "Vypnout" }, + "orgUserDetailsNotFound": { + "message": "Podrobnosti o členovi nebyly nalezeny" + }, "revokeAccess": { "message": "Zrušit přístup" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Výchozí sbírka" }, + "myItems": { + "message": "Moje položky" + }, "getHelp": { "message": "Získat nápovědu" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Nouzový přístup byl odmítnut" }, + "grantorDetailsNotFound": { + "message": "Údaje o zadavateli nebyly nalezeny" + }, "passwordResetFor": { "message": "Pro $USER$ bylo obnoveno heslo. Nyní se můžete přihlásit pomocí nového hesla.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Vynutit vlastnictví dat organizace" + }, "personalOwnership": { "message": "Odebrat osobní trezor" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Pokračováním odhlásíte $NAME$ z aktuální relace, což znamená, že se bude muset znovu přihlásit. Aktivní relace na jiných zařízeních mohou zůstat aktivní až po dobu jedné hodiny.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "tento uživatel" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Jedna nebo více zásad organizace vyžaduje, aby hlavní heslo splňovalo následující požadavky:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Jedna nebo více zásad organizace vyžaduje, aby hlavní heslo splňovalo následující požadavky:" + }, "resetPasswordSuccess": { "message": "Heslo bylo úspěšně resetováno!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Pro přidání kreditu je vyžadována fakturační adresa.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index bf319005c5e..b8e3ccd5d25 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index ac5ed3c0a16..1f288af356f 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Opsætning af totrins-login kan permanent låse en bruger ude af sin Bitwarden-konto. En gendannelseskode muliggør kontoadgang, såfremt den normale totrins-loginudbyder ikke længere kan bruges (f.eks. ved tab af en enhed). Bitwarden-supporten kan ikke hjælpe ved mistet kontoadgang. Det anbefales at nedskrive/udskrive gendannelseskoden samt gemme denne et sikkert sted." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Deaktivér" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Ophæv adgang" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Standardsamling" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Få hjælp" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Nødadgang nægtet" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Adgangskode nulstillet for $USER$. Du kan nu logge ind med den nye adgangskode.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Fjern individuel boks" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "denne bruger" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Én eller flere organisationspolitikker kræver, at hovedadgangskoden opfylder flg. krav:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Adgangskode nulstillet!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 41a583b1d87..c065f26f98d 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Durch die Aktivierung der Zwei-Faktor-Authentifizierung kannst du dich dauerhaft aus deinem Bitwarden-Konto aussperren. Ein Wiederherstellungscode ermöglicht es dir, auf dein Konto zuzugreifen, falls du deinen normalen Zwei-Faktor-Anbieter nicht mehr verwenden kannst (z.B. wenn du dein Gerät verlierst). Der Bitwarden-Support kann dir nicht helfen, wenn du den Zugang zu deinem Konto verlierst. Wir empfehlen dir, den Wiederherstellungscode aufzuschreiben oder auszudrucken und an einem sicheren Ort aufzubewahren." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Karten-Eintragstyp entfernen" }, - "restrictedItemTypesPolicyDesc": { - "message": "Mitgliedern das Erstellen von Karten-Eintragstypen nicht erlauben." + "restrictedItemTypePolicyDesc": { + "message": "Mitgliedern nicht erlauben, Karten-Eintragstypen zu erstellen. Vorhandene Karten werden automatisch entfernt." + }, + "restrictCardTypeImport": { + "message": "Karten-Eintragstypen können nicht importiert werden" + }, + "restrictCardTypeImportDesc": { + "message": "Eine von einer oder mehreren Organisationen festgelegte Richtlinie verhindert, dass du Karten in deinen Tresor importieren kannst." }, "yourSingleUseRecoveryCode": { "message": "Dein einmal benutzbarer Wiederherstellungscode kann benutzt werden, um die Zwei-Faktor-Authentifizierung auszuschalten, wenn du Zugang zu deinen Zwei-Faktor-Anbietern verlierst. Bitwarden empfiehlt dir, den Wiederherstellungscode aufzuschreiben und an einem sicheren Ort aufzubewahren." @@ -2218,6 +2224,9 @@ "disable": { "message": "Deaktivieren" }, + "orgUserDetailsNotFound": { + "message": "Mitgliederdetails nicht gefunden." + }, "revokeAccess": { "message": "Zugriff widerrufen" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Standardsammlung" }, + "myItems": { + "message": "Meine Einträge" + }, "getHelp": { "message": "Hilfe erhalten" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Notfallzugriff abgelehnt" }, + "grantorDetailsNotFound": { + "message": "Angaben zur gewährenden Person nicht gefunden" + }, "passwordResetFor": { "message": "Passwort für $USER$ zurückgesetzt. Du kannst dich jetzt mit dem neuen Passwort anmelden.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Eigentumsrechte an Unternehmensdaten erzwingen" + }, "personalOwnership": { "message": "Persönlichen Tresor entfernen" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Wenn du fortfährst, wird $NAME$ aus seiner aktuellen Sitzung abgemeldet und muss sich erneut anmelden. Aktive Sitzungen auf anderen Geräten können bis zu einer Stunde weiterhin aktiv bleiben.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "dieser Benutzer" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Eine oder mehrere Organisationsrichtlinien erfordern, dass dein Master-Passwort die folgenden Anforderungen erfüllt:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Eine oder mehrere Organisationsrichtlinien erfordern, dass dein Master-Passwort die folgenden Anforderungen erfüllt:" + }, "resetPasswordSuccess": { "message": "Passwort erfolgreich zurückgesetzt!" }, @@ -7615,7 +7645,7 @@ "description": "Notifies that a service account has been updated" }, "typeOrSelectProjects": { - "message": "Type or select projects", + "message": "Projektnamen eingeben oder auswählen", "description": "Instructions for selecting projects for a service account" }, "newSaTypeToFilter": { @@ -7702,7 +7732,7 @@ "description": "Title for the section displaying access tokens." }, "createAccessToken": { - "message": "Create access token", + "message": "Zugriffstoken erstellen", "description": "Button label for creating a new access token." }, "expires": { @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Rechnungsadresse erforderlich, um Guthaben hinzuzufügen.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index de75b5c538b..b8efb914a34 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Η ενεργοποίηση σύνδεσης δύο βημάτων μπορεί να κλειδώσει οριστικά το λογαριασμό σας από το Bitwarden. Ένας κωδικός ανάκτησης σάς επιτρέπει να έχετε πρόσβαση στον λογαριασμό σας σε περίπτωση που δεν μπορείτε πλέον να χρησιμοποιήσετε τη σύνδεση δύο βημάτων (π. χ. χάνετε τη συσκευή σας). Η υποστήριξη πελατών του Bitwarden δεν θα είναι σε θέση να σας βοηθήσει αν χάσετε την πρόσβαση στο λογαριασμό σας. Συνιστούμε να γράψετε ή να εκτυπώσετε τον κωδικό ανάκτησης και να τον φυλάξετε σε ασφαλές μέρος." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Απενεργοποίηση" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Ανάκληση πρόσβασης" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Προεπιλεγμένη Συλλογή" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Ζητήστε Βοήθεια" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Η πρόσβαση έκτακτης ανάγκης απορρίφθηκε" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Επαναφορά κωδικού πρόσβασης για το $USER$. Τώρα μπορείτε να συνδεθείτε χρησιμοποιώντας το νέο κωδικό πρόσβασης.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Προσωπική Ιδιοκτησία" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "αυτός ο χρήστης" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Σε μία ή περισσότερες πολιτικές του οργανισμού απαιτείται ο κύριος κωδικός να πληρεί τις ακόλουθες απαιτήσεις:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Επιτυχία επαναφοράς κωδικού!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index efd54434a45..a7b8241ed34 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2225,6 +2225,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3273,6 +3276,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -6059,6 +6065,9 @@ "add": { "message": "Add" }, + "masterPasswordSuccessfullySet": { + "message": "Master password successfully set" + }, "updatedMasterPassword": { "message": "Master password saved" }, diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index f749cf65f54..bf15dd133f9 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organisations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organisation data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organisation policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organisation policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 0be4e496f74..d290ea148ad 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Enabling two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (e.g. you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organisations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Disable" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organisation data ownership" + }, "personalOwnership": { "message": "Personal Ownership" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organisation policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organisation policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 0da9c976af1..49924ab03fe 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Ebligi du-paŝan ensaluton povas konstante elŝlosi vin el via Bitwarden-konto. Rekuperiga kodo permesas vin aliri vian konton, se vi ne plu povas uzi vian normalan du-paŝan ensalutan provizanton (ekz. vi perdas Bitwarden-subteno ne povos helpi vin se vi perdos aliron al via konto. Ni rekomendas al vi skribi aŭ presi la reakiran kodon kaj konservi ĝin en sekura loko. " }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Malŝalti" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Implicita kolekto" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Akiri helpon" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Krizaliro malakceptita" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Pravaloriziĝis la pasvorto de $USER$. Vi nun povas saluti per la nova pasvorto.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Persona Posedo" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "ĉi tiu uzanto" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 02a4ec0de38..253507109a8 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -108,7 +108,7 @@ "message": "At-risk passwords" }, "requestPasswordChange": { - "message": "Request password change" + "message": "Solicitar cambio de contraseña" }, "totalPasswords": { "message": "Total de contraseñas" @@ -1495,7 +1495,7 @@ "message": "Usa un Yubikey para acceder a tu cuenta. Funciona con YubiKey 4, 4 Nano, 4C y dispositivos NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Introduce un código generado por Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Habilitar la autenticación en dos pasos puede impedirte acceder permanentemente a tu cuenta de Bitwarden. Un código de recuperación te permite acceder a la cuenta en caso de que no puedas usar más tu proveedor de autenticación en dos pasos (ej. si pierdes tu dispositivo). El soporte de Bitwarden no será capaz de asistirte si pierdes acceso a tu cuenta. Te recomendamos que escribas o imprimas este código y lo guardes en un lugar seguro." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Desactivar" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revocar el acceso" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Colección por defecto" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Consigue ayuda" }, @@ -4125,7 +4137,7 @@ } }, "freeTrialEndPromptTomorrowNoOrgName": { - "message": "Tu prueba gratuita finaliza mañana." + "message": "Tu prueba gratuita termina mañana." }, "freeTrialEndPromptToday": { "message": "$ORGANIZATION$, your free trial ends today.", @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Acceso de emergencia rechazado" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Restablecimiento de contraseña para $USER$. Ahora puede iniciar sesión usando la nueva contraseña.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Propiedad personal" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "este usuario" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Una o más políticas de organización requieren la contraseña maestra para cumplir con los siguientes requisitos:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "¡Contraseña restablecida!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index bf766409fbd..04b51e7c23b 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Kaheastmelise kinnitamine aktiveerimine võib luua olukorra, kus sul on võimatu oma Bitwardeni kontosse sisse logida. Näiteks kui kaotad oma nutiseadme. Taastamise kood võimaldab aga kontole ligi pääseda ka olukorras, kus kaheastmelist kinnitamist ei ole võimalik läbi viia. Sellistel juhtudel ei saa ka Bitwardeni klienditugi sinu kontole ligipääsu taastada. Selle tõttu soovitame taastekoodi välja printida ja seda turvalises kohas hoida." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Sinu ühekordseid taastamise koode saab kasutada selleks, et lülitada kahe-astmeline sisselogimine välja juhul, kui sa oled kaotanud juurdepääsu oma kahe-astmelise sisselogimise viisidele. Bitwarden soovitab sul kirjutada üles taastamise koodid ja hoiustada neid ohutus kohas." @@ -2218,6 +2224,9 @@ "disable": { "message": "Keela" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Tühistada ligipääsu luba" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Vaikekogumik" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Klienditugi" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Hädaolukorra ligipääsust keelduti" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "$USER$ parool on lähtestatud. Saad nüüd uue parooliga sisse logida.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Personaalne salvestamine" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "see kasutaja" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Üks või enam organisatsiooni eeskirja nõuavad, et ülemparool vastaks nendele nõudmistele:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Parool on edukalt lähtestatud!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 4c520de1762..30f3f5488d7 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Bi urratseko saio hasiera gaitzeak betirako blokea dezake Bitwarden kontura sartzea. Berreskuratze-kode baten bidez, zure kontura sar zaitezke, bi urratseko saio hasierako hornitzailea erabili ezin baduzu (adb. gailua galtzen baduzu). Bitwarden-ek ezingo dizu lagundu zure konturako sarbidea galtzen baduzu. Berreskuratze-kodea idatzi edo inprimatzea eta leku seguruan edukitzea gomendatzen dugu." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Desgaitu" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Sarbidea ezeztatu" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Bilduma lehenetsia" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Jaso laguntza" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Larrialdiko sarbidea ukatua" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "$USER$-(r)entzat pasahitza berrezartzea. Orain, saioa abiaraz dezakezu pasahitz berria erabiliz.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Ezabatu kutxa gotor pertsonala" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "erabiltzaile hau" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Erakundeko politika batek edo gehiagok pasahitz nagusia behar dute baldintza hauek betetzeko:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Pasahitza berrezarria!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 2be7366e75e..99cdefe5695 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "راه‌اندازی ورود دو مرحله‌ای می‌تواند برای همیشه حساب Bitwarden شما را قفل کند. یک کد بازیابی به شما امکان می‌دهد در صورتی که دیگر نمی‌توانید از ارائه‌دهنده‌ی ورود دو مرحله‌ای معمولی خود استفاده کنید (به عنوان مثال: دستگاه خود را گم می‌کنید) به حساب خود دسترسی پیدا کنید. اگر دسترسی به حساب خود را از دست بدهید، پشتیبانی Bitwarden نمی‌تواند به شما کمک کند. توصیه می‌کنیم کد بازیابی را یادداشت یا چاپ کنید و آن را در مکانی امن نگهداری کنید." }, - "restrictedItemTypesPolicy": { - "message": "حذف نوع مورد کارت" + "restrictedItemTypePolicy": { + "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "اجازه نده اعضا نوع مورد کارت ایجاد کنند." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "کد بازیابی یک‌بار مصرف شما می‌تواند در صورت از دست دادن دسترسی به سرویس ورود دو مرحله‌ای، برای غیرفعال کردن آن استفاده شود. Bitwarden توصیه می‌کند این کد را یادداشت کرده و در جای امنی نگهداری کنید." @@ -2218,6 +2224,9 @@ "disable": { "message": "خاموش کردن" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "لغو دسترسی" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "مجموعه پیش‌فرض" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "کمک گرفتن" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "دسترسی اضطراری رد شد" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "کلمه عبور برای $USER$ بازنشانی شد. اکنون می‌توانید با استفاده از کلمه عبور جدید وارد شوید.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "حذف گاوصندوق شخصی" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "این کاربر" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "یک یا چند سیاست سازمانی برای تأمین شرایط زیر به کلمه عبور اصلی احتیاج دارد:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "بازیابی رمزعبور با موفقیت انجام شد!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 7b948d6cbc8..9f6afee3713 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Kaksivaiheisen kirjautumisen käyttöönotto voi lukita sinut ulos Bitwarden-tililtäsi pysyvästi. Palautuskoodi mahdollistaa pääsyn tilillesi myös silloin, kun et voi käyttää normaaleja kaksivaiheisen tunnistautumisen vahvistustapoja (esim. kadotat suojausavaimesi tai se varastetaan). Bitwardenin asiakaspalvelukaan ei voi auttaa sinua, jos menetät pääsyn tillesi. Suosittelemme, että kirjoitat palautuskoodin muistiin tai tulostat sen ja säilytät turvallisessa paikassa (esim. kassakaapissa tai pankin tallelokerossa)." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Kertakäyttöisellä palautuskoodillasi voit poistaa kaksivaiheisen kirjautumisen käytöstä, mikäli et voi käyttää kaksivaiheista todennustapaasi. Bitwarden suosittelee kirjoittamaan palautuskoodin ylös ja säilyttämään sen turvallisessa paikassa." @@ -2218,6 +2224,9 @@ "disable": { "message": "Poista käytöstä" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Mitätöi käyttöoikeudet" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Oletuskokoelma" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Hanki apua" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Varmuuskäyttö hylätty." }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Käyttäjän $USER$ salasana on palautettu. Voit nyt kirjautua sisään käyttäen uutta salasanaa.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Poista yksityinen holvi" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "tämä käyttäjä" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Yksi tai useampi organisaatiokäytäntö edellyttää, että pääsalasanasi täyttää seuraavat vaatimukset:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Salasanan palautus onnistui!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 8d8524a070b..4794aef1d99 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Maaaring permanente kang ma-lock out sa account mo dahil sa dalawang-hakbang na pag-log in. Pwede kang gumamit ng code pang-recover sakaling hindi mo na magamit ang normal mong provider ng dalawang-hakbang na pag-log in (halimbawa: nawala ang device mo). Hindi ka matutulungan ng Bitwarden support kung mawalan ka ng access sa account mo. Mainam na isulat o i-print mo ang mga code pang-recover at itago ito sa ligtas na lugar." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Isara" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Tanggalin ang access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default na koleksyon" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Humingi ng tulong" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Hindi tinanggap ang emergency access" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Pag-reset ng password para sa $USER$. Maaari ka na ngayong mag login gamit ang bagong password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Alisin ang indibidwal na vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "ang user na ito" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Ang isa o higit pang mga patakaran sa organisasyon ay nangangailangan ng master password upang matugunan ang mga sumusunod na kinakailangan:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Tagumpay sa pag-reset ng password!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index adc8f91aaf7..3abcdf9a999 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "La configuration d'un système d'authentification à deux facteurs peut définitivement vous verrouiller l'accès à votre compte Bitwarden. Un code de récupération vous permet d'accéder à votre compte dans le cas où vous ne pourriez plus utiliser votre fournisseur normal d'authentification à deux facteurs (exemple : vous perdez votre appareil). L'assistance de Bitwarden ne pourra pas vous aider si vous perdez l'accès à votre compte. Nous vous recommandons de noter ou d'imprimer le code de récupération et de le conserver en lieu sûr." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Supprimer le type d'élément de la carte" }, - "restrictedItemTypesPolicyDesc": { - "message": "Ne pas autoriser les membres à créer des types d'objets de carte." + "restrictedItemTypePolicyDesc": { + "message": "Ne pas autoriser les membres à créer des types d'éléments de carte. Les cartes existantes seront automatiquement supprimées." + }, + "restrictCardTypeImport": { + "message": "Impossible d'importer les types de l'élément de la carte" + }, + "restrictCardTypeImportDesc": { + "message": "Une politique de sécurité définie par 1 organisation ou plus vous empêche d'importer des cartes dans vos coffres." }, "yourSingleUseRecoveryCode": { "message": "Votre code de récupération à usage unique peut être utilisé pour désactiver la connexion en deux étapes si vous perdez l'accès à votre fournisseur de connexion en deux étapes. Bitwarden vous recommande d'écrire le code de récupération et de le conserver dans un endroit sûr." @@ -2218,6 +2224,9 @@ "disable": { "message": "Désactiver" }, + "orgUserDetailsNotFound": { + "message": "Informations sur le membre non trouvés." + }, "revokeAccess": { "message": "Révoquer l'Accès" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Collection par défaut" }, + "myItems": { + "message": "Mes éléments" + }, "getHelp": { "message": "Obtenir de l'aide" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Accès d'urgence refusé." }, + "grantorDetailsNotFound": { + "message": "Détails de la source non trouvés" + }, "passwordResetFor": { "message": "Mot de passe réinitialisé pour $USER$. Vous pouvez maintenant vous connecter en utilisant le nouveau mot de passe.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Forcer la propriété des données de l'organisation" + }, "personalOwnership": { "message": "Supprimer le coffre individuel" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "En poursuivant, $NAME$ sera déconnecté de sa session actuelle, ce qui l'obligera à se reconnecter. Les sessions actives sur d'autres appareils peuvent rester actives pendant encore une heure.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "cet utilisateur" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Une ou plusieurs politiques de sécurité de l'organisation exigent que votre mot de passe principal réponde aux exigences suivantes :" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Une ou plusieurs politiques de sécurité de l'organisation exigent que votre mot de passe principal réponde aux exigences suivantes :" + }, "resetPasswordSuccess": { "message": "Mot de passe réinitialisé avec succès !" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "L'adresse de facturation est requise pour ajouter du crédit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 0603a7e79f0..9ed6e922300 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 49fa7eda4aa..e569d94437e 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "הגדרת כניסה דו־שלבית יכולה לנעול אותך לצמיתות מחוץ לחשבון Bitwarden שלך. קוד שחזור מאפשר לך לגשת לחשבון שלך במקרה שאתה לא יכול להשתמש בספק הכניסה הד־שלבית הרגיל שלך (דוגמה: איבדת את המכשיר שלך). התמיכה של Bitwarden לא תוכל לסייע לך אם תאבד גישה לחשבון שלך. אנו ממליצים שתכתוב או תדפיס את קוד השחזור ותשמור אותו במקום בטוח." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "ניתן להשתמש בקוד השחזור החד־פעמי שלך כדי לכבות כניסה דו־שלבית במקרה שאתה מאבד גישה לספק הכניסה הדו־שלבית שלך. Bitwarden ממליץ לך לרשום את קוד השחזור ולשמור אותו במקום בטוח." @@ -2218,6 +2224,9 @@ "disable": { "message": "כבה" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "בטל גישה" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "אוסף ברירת מחדל" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "קבל עזרה" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "גישת חירום נדחתה" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "הסיסמה אופסה עבור $USER$. אתה יכול כעת להיכנס באמצעות הסיסמה החדשה.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "הסר כספת אישית" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "משתמש זה" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "מדיניות ארגון אחת או יותר דורשת שהסיסמה הראשית תעמוד בדרישות הבאות:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "הצלחת איפוס סיסמה!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 2135be63cc2..f6e2e491f49 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 98f24ef51cd..ef5730fb6b6 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Uključivanje prijave dvostrukom autentifikacijom može ti trajno onemogućiti pristup Bitwarden računu. Kôd za oporavak ti omogućuje pristup računu u slučaju kada više ne možeš koristiti redovnog pružatelja prijave dvostrukom autentifikacijom (npr. izgubiš svoj uređaj). Bitwarden podrška neće ti moći pomoći ako izgubiš pristup svojem računu. Savjetujemo da zapišeš ili ispišeš kôd za oporavak i spremiš ga na sigurno mjesto." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Tvoj jednokratni kôd za oporavak može se koristiti za isključivanje prijave dvostruke autentifikacije u slučaju da izgubiš pristup svom davatelju usluge dvostruke autentifikacije. Bitwarden preporučuje da zapišeš kôd za oporavak i čuvaš ga na sigurnom mjestu." @@ -2218,6 +2224,9 @@ "disable": { "message": "Onemogući" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Opozovi pristup" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Zadana zbirka" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Potraži pomoć" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Pristup u nuždi odbijen" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Lozinka za $USER$ resetirana. Sada se možeš prijaviti novom lozinkom.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Ukloni osobni trezor" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "ovaj korisnik" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Jedno ili više organizacijskih pravila zahtijeva da glavna lozinka ispunjava sljedeće uvjete:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Uspješno ponovno postalvjena lozinka!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 33bdef91d86..4ad900a4bae 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "A kétlépcsős bejelentkezés engedélyezése véglegesen kizárhatja a felhasználót a Bitwarden fiókból. A helyreállítási kód lehetővé teszi a fiókjához való hozzáférést abban az esetben, ha már nem tudjuk használni a szokásos kétlépcsős bejelentkezési szolgáltatást (pl. készülék elvesztése). A Bitwarden támogatás nem tud segíteni abban az esetben, ha elveszítjük a hozzáférést a fiókhoz. Célszerű leírni vagy kinyomtatni a helyreállítási kódot és azt biztonságos helyen tartani." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Kártyaelem típus eltávolítása" }, - "restrictedItemTypesPolicyDesc": { - "message": "Ne engedjük a felhasználóknak a kártyaelem típusok létrehozását." + "restrictedItemTypePolicyDesc": { + "message": "Ne engedjük meg a tagoknak, hogy kártyaelem típusokat hozzanak létre. A meglévő kártyák automatikusan eltávolításra kerülnek." + }, + "restrictCardTypeImport": { + "message": "A kártya elem típusokat nem lehet importálni." + }, + "restrictCardTypeImportDesc": { + "message": "Egy vagy több szervezet által beállított szabályzat megakadályozza a kártyák importálását a széfekbe." }, "yourSingleUseRecoveryCode": { "message": "Az egyszer használatos helyreállítási kóddal kikapcsolhatjuk a kétlépcsős bejelentkezést abban az esetben, ha elveszítjük a hozzáférést a kétlépcsős bejelentkezési szolgáltatóhoz. A Bitwarden azt javasolja, hogy írjuk le a helyreállítási kódot és tartsuk biztonságos helyen." @@ -2218,6 +2224,9 @@ "disable": { "message": "Letiltás" }, + "orgUserDetailsNotFound": { + "message": "A tag adatai nem találhatók." + }, "revokeAccess": { "message": "Hozzáférés visszavonása" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Alapértelmezett gyűjtemény" }, + "myItems": { + "message": "Saját elemek" + }, "getHelp": { "message": "Segítségkérés" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "A vészhelyzeti hozzáférés elutasításra került." }, + "grantorDetailsNotFound": { + "message": "Az adományozó adatai nem találhatók." + }, "passwordResetFor": { "message": "A jelszó alaphelyzetbe került $USER$ részére. Most az új jelszóval lehet bejelentkezni.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "A szervezet adat tulajdonjogának érvényesítése" + }, "personalOwnership": { "message": "Személyes tulajdon" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "A folyamat során $NAME$ kijelentkezése történik az aktuális munkamenetből, ezért vissza kell jelentkezni. Az aktív munkamenetek más eszközökön akár egy órán keresztül is aktívak maradhatnak.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "ez a felhasználó" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Egy vagy több szervezeti rendszabályhoz mesterjelszó szükséges a következő követelmények megfeleléséhez:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Egy vagy több szervezeti rendszabályhoz mesterjelszó szükséges a következő követelmények megfeleléséhez:" + }, "resetPasswordSuccess": { "message": "A jelszó alaphelyzetbe állítása sikeres volt." }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "A jóváírás hozzáadásához szükséges számlázási cím.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 72bb773140d..fffe374b44b 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Dengan mengaktifkan login dua-langkah, Anda bisa terkunci dari akun Bitwarden secara permanen. Jika Anda tidak bisa menggunakan provider login dua-langkah di kondisi normal (misal karena perangkat Anda hilang), Anda bisa menggunakan kode pemulihan untuk mengakses akun tersebut. Bitwarden sama sekali tidak dapat membantu jika Anda kehilangan akses ke akun Anda. Oleh karena itu, kami menyarankan Anda untuk menulis atau mencetak kode pemulihan dan menyimpannya di tempat yang aman." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Nonaktifkan" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Cabut Akses" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Koleksi Default" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Dapatkan Bantuan" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Akses darurat ditolak" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Setel ulang sandi untuk $USER$. Sekarang Anda dapat masuk menggunakan kata sandi baru.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Kepemilikan Pribadi" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "pengguna ini" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Satu atau lebih kebijakan organisasi memerlukan kata sandi utama Anda untuk memenuhi persyaratan berikut:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Ubah kata kunci berhasil!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index c5f665c89e6..a861f368366 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Impostare la verifica in due passaggi potrebbe bloccarti permanentemente fuori dal tuo account Bitwarden. Un codice di recupero ti permette di accedere al tuo account il caso non potessi più usare il tuo solito metodo di verifica in due passaggi (per esempio se perdi il telefono). L'assistenza clienti di Bitwarden non sarà in grado di aiutarti se perdi l'accesso al tuo account. Scrivi o stampa il tuo codice di recupero e conservalo in un luogo sicuro." }, - "restrictedItemTypesPolicy": { - "message": "Rimuovi tipo di carta" + "restrictedItemTypePolicy": { + "message": "Rimuovi l'elemento carta" }, - "restrictedItemTypesPolicyDesc": { - "message": "Non consentire ai membri di salvare le carte." + "restrictedItemTypePolicyDesc": { + "message": "Non consentire ai membri di creare elementi di tipo carta. Le carte esistenti saranno rimosse automaticamente." + }, + "restrictCardTypeImport": { + "message": "Impossibile importare elementi di tipo carta" + }, + "restrictCardTypeImportDesc": { + "message": "Non puoi importare carte nelle tue casseforti a causa di una politica impostata da una o più organizzazioni." }, "yourSingleUseRecoveryCode": { "message": "Puoi usare il codice di recupero monouso se non hai accesso a nessuno dei metodi impostati per l'accesso in due passaggi. Se accedi con un codice, l'accesso in due passaggi sarà disattivato. Conserva il codice in un luogo sicuro e accessibile solo a te." @@ -2218,6 +2224,9 @@ "disable": { "message": "Disattiva" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoca accesso" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Raccolta predefinita" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Ottieni aiuto" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Accesso di emergenza rifiutato" }, + "grantorDetailsNotFound": { + "message": "Dettagli del concedente non trovati" + }, "passwordResetFor": { "message": "Password ripristinata per $USER$. Ora puoi accedere usando la nuova password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Forza la proprietà dei dati dell'organizzazione" + }, "personalOwnership": { "message": "Rimuovi cassaforte individuale" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Se si procede, sarà chiusa la sessione corrente di $NAME$, cui sarà richiesto un nuovo accesso. Le sue sessioni attive su altri dispositivi potrebbero restare attive per un periodo massimo di un'ora.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "questo utente" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Una o più politiche dell'organizzazione richiedono che la tua password principale soddisfi questi requisiti:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Una o più politiche dell'organizzazione richiedono che la tua password principale soddisfi questi requisiti:" + }, "resetPasswordSuccess": { "message": "Password ripristinata!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Indirizzo di fatturazione richiesto per aggiungere credito.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 278110dc8cc..9af2943f1da 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "2段階認証を有効にすると Bitwarden アカウントから永久に閉め出されてしまうことがあります。リカバリーコードがあれば、通常の2段階認証プロバイダを使えなくなったとき (デバイスの紛失等) でもアカウントにアクセスできます。アカウントにアクセスできなくなっても Bitwarden はサポート出来ないため、リカバリーコードを書き出すか印刷し安全な場所で保管しておくことを推奨します。" }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "2段階認証プロバイダーへのアクセスを失った場合は、使い捨てのリカバリーコードを使用して2段階認証をオフにできます。 Bitwarden では、リカバリーコードを書き留めて安全な場所に保管することをお勧めしています。" @@ -2218,6 +2224,9 @@ "disable": { "message": "無効化" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "アクセスを取り消す" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "既定のコレクション" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "ヘルプを参照する" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "緊急アクセスが拒否されました" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "$USER$のパスワードをリセットしました。新しいパスワードでログインできます。", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "個別の保管庫を削除" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "このユーザー\n" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "組織ポリシーの要件を満たすためにマスターパスワードが必要です:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "パスワードをリセットしました" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 51ae044e856..d5ae3e1978c 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 3661600ce58..f422e74a569 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 92f70868868..b400d0ca863 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ನಿಮ್ಮ ಬಿಟ್‌ವಾರ್ಡನ್ ಖಾತೆಯಿಂದ ನಿಮ್ಮನ್ನು ಶಾಶ್ವತವಾಗಿ ಲಾಕ್ ಮಾಡಬಹುದು. ನಿಮ್ಮ ಸಾಮಾನ್ಯ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಪೂರೈಕೆದಾರರನ್ನು ನೀವು ಇನ್ನು ಮುಂದೆ ಬಳಸಲಾಗದಿದ್ದಲ್ಲಿ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಮರುಪಡೆಯುವಿಕೆ ಕೋಡ್ ನಿಮಗೆ ಅನುಮತಿಸುತ್ತದೆ (ಉದಾ. ನಿಮ್ಮ ಸಾಧನವನ್ನು ನೀವು ಕಳೆದುಕೊಳ್ಳುತ್ತೀರಿ). ನಿಮ್ಮ ಖಾತೆಗೆ ನೀವು ಪ್ರವೇಶವನ್ನು ಕಳೆದುಕೊಂಡರೆ ಬಿಟ್‌ವಾರ್ಡನ್ ಬೆಂಬಲವು ನಿಮಗೆ ಸಹಾಯ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. ಮರುಪಡೆಯುವಿಕೆ ಕೋಡ್ ಅನ್ನು ಬರೆಯಲು ಅಥವಾ ಮುದ್ರಿಸಲು ಮತ್ತು ಅದನ್ನು ಸುರಕ್ಷಿತ ಸ್ಥಳದಲ್ಲಿ ಇರಿಸಲು ನಾವು ಶಿಫಾರಸು ಮಾಡುತ್ತೇವೆ." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "ನಿಷ್‌ಕ್ರಿಯೆಗೊಳಿಸಿ" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "ಡೀಫಾಲ್ಟ್ ಸಂಗ್ರಹ" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "ಸಹಾಯ ಪಡೆ" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "ತುರ್ತು ಪ್ರವೇಶವನ್ನು ತಿರಸ್ಕರಿಸಲಾಗಿದೆ" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "$USER$ ಗೆ ಪಾಸ್‌ವರ್ಡ್ ಮರುಹೊಂದಿಸಿ. ನೀವು ಈಗ ಹೊಸ ಪಾಸ್‌ವರ್ಡ್ ಬಳಸಿ ಲಾಗಿನ್ ಮಾಡಬಹುದು.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "ವೈಯಕ್ತಿಕ ಮಾಲೀಕತ್ವ" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "ಈ ಬಳಕೆದಾರ" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "ಒಂದು ಅಥವಾ ಹೆಚ್ಚಿನ ಸಂಸ್ಥೆಯ ನೀತಿಗಳಿಗೆ ಈ ಕೆಳಗಿನ ಅವಶ್ಯಕತೆಗಳನ್ನು ಪೂರೈಸಲು ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅಗತ್ಯವಿದೆ:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "ಪಾಸ್ವರ್ಡ್ ಮರುಹೊಂದಿಸುವ ಯಶಸ್ಸು!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 76b99be0fe8..8293750eed3 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "2단계 로그인을 활성화하면 Bitwarden 계정을 영원히 잠글 수 있습니다. 복구 코드를 사용하면 정상적인 2단계 로그인 제공자를 더 이상 사용할 수 없는 경우(예. 장치를 잃어버렸을 때) 계정에 액세스할 수 있습니다. 계정에 접근하지 못한다면 Bitwarden 지원팀은 어떤 도움도 줄 수 없습니다. 복구 코드를 기록하거나 출력하여 안전한 장소에 보관할 것을 권장합니다." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "비활성화" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "기본 컬렉션" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "문의하기" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "긴급 접근 거절됨." }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "$USER$ 사용자의 비밀번호가 초기화되었습니다. 이제 새로운 비밀번호로 로그인할 수 있습니다.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "개인 소유권" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "이 사용자" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "하나 이상의 단체 정책이 마스터 비밀번호가 다음 사항을 따르도록 요구합니다:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "비밀번호 재설정 성공!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 102057367fd..b17131cf2ef 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Divpakāpju pieteikšanās var pastāvīgi liegt piekļuvi Bitwarden kontam. Atkopšanas kods ļauj piekļūt tam gadījumā, kad vairs nav iespējams izmantot ierasto divpakāpju pieteikšanās nodrošinātāju (piemēram, ir pazaudēta ierīce). Bitwarden atbalsts nevarēs palīdzēt, ja tiks pazaudēta piekļuve kontam. Ir ieteicams, ka atkopšanas kods tiek pierakstīts vai izdrukāts un turēts drošā vietā." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Noņemt karšu vienumu veidu" }, - "restrictedItemTypesPolicyDesc": { - "message": "Neļaut dalībniekiem izveidot karšu vienumu veidus." + "restrictedItemTypePolicyDesc": { + "message": "Neļaut dalībniekiem izveidot karšu vienumu veidus. Esošās kartes tiks automātiski noņemtas." + }, + "restrictCardTypeImport": { + "message": "Nevar ievietot karšu vienumu veidus" + }, + "restrictCardTypeImportDesc": { + "message": "Pamatnostādne, ko ir iestatījusi viena vai vairākas apvienības, liedz karšu ievietošanu savās glabātavās." }, "yourSingleUseRecoveryCode": { "message": "Vienreizējas izmantošanas atkopes kodu var izmantot, lai izslēgtu divpakāpju pieteikšanos gadījumā, ja tiek zaudēta piekļuve savam divpakāpju pieteikšanās nodrošinātājam. Bitwarden iesaka pierakstīt atkopes kodu un glabāt to drošā vietā." @@ -2218,6 +2224,9 @@ "disable": { "message": "Atspējot" }, + "orgUserDetailsNotFound": { + "message": "Informācija par dalībnieku netika atrasta." + }, "revokeAccess": { "message": "Atsaukt piekļuvi" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Noklusējuma krājums" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Saņemt palīdzību" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Ārkārtas piekļuve noraidīta" }, + "grantorDetailsNotFound": { + "message": "Informācijas par piešķīrēju netika atrasta" + }, "passwordResetFor": { "message": "Parole atiestatīta lietotājam $USER$. Tagad var pieteikties ar jauno paroli.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Uzspiest apvienības datu īpašumtiesības" + }, "personalOwnership": { "message": "Personīgās īpašumtiesības" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Turpināšana izrakstīs $NAME$ no pašreizējās sesijas, un pēc tam būs nepieciešams vēlreiz pieteikties. Citās ierīcēs darbojošās sesijas var būt spēkā līdz vienai stundai.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "šo lietotāju" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Vienā vai vairākos apvienības nosacījumos ir norādīts, ka galvenajai parolei ir jāatbilst šādām prasībām:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Vienā vai vairākās apvienības pamatnostādnēs ir norādīts, ka galvenajai parolei ir jāatbilst šādām prasībām:" + }, "resetPasswordSuccess": { "message": "Peroles atiestatīšana bija veiksmīga" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Norēķinu adrese ir nepieciešama, lai pievienot kredītu.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 3442762319d..ef1bb77feb4 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Enabling two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (ex. you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "പ്രവര്‍ത്തന രഹിതമാക്കുക" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default Collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get Help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "വ്യക്തിഗത ഉടമസ്ഥാവകാശം" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 4483b9ebb61..97eb93dd3eb 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 3661600ce58..f422e74a569 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 3e704ca47dc..68cd7725bd5 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Å skru på 2-trinnsinnlogging kan låse deg permanent ut av din Bitwarden-konto. En gjenopprettingskode gir deg tilgang til kontoen din i det tilfellet at du ikke lenger kan bruke din vanlige 2-trinnsinnloggingsleverandør (f.eks. at du mister enheten din). Bitwarden-kundestøtten vil ikke kunne hjelpe deg dersom du mister tilgang til kontoen din. Vi anbefaler at du skriver ned eller skriver ut gjenopprettingskoden og legger den på en trygg plass." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Deaktiver" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Opphev tilgang" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Standardsamling" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Få hjelp" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Nødtilgang avvist" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Passord tilbakestille for $USER$. Du kan nå logge inn ved å bruke det nye passordet.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Personlig eierskap" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "denne brukeren" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "En eller flere av organisasjonens vilkår krever hovedpassordet ditt for å oppfylle følgende krav:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Tilbakestilling av passord vellykket!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 7a4c19a0362..f74848f0aee 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 0e70d503dc5..fc2b6e9593c 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Door aanmelden in twee stappen in te schakelen, kun je jezelf definitief buitensluiten van je Bitwarden-account. Een herstelcode geeft je toegang tot je account in het geval dat je je normale tweestapsaanmelding niet meer kunt gebruiken (bijv. als je je apparaat verliest). De Bitwarden-klantondersteuning kan je niet helpen als je de toegang tot je account verliest. We raden je met klem aan de herstelcode op te schrijven of af te drukken en op een veilige plaats te bewaren." }, - "restrictedItemTypesPolicy": { - "message": "Verwijder kaart item type" + "restrictedItemTypePolicy": { + "message": "Kaart item type verwijderen" }, - "restrictedItemTypesPolicyDesc": { - "message": "Aanmaken van kaart item types niet toestaan voor leden." + "restrictedItemTypePolicyDesc": { + "message": "Leden niet toestaan om kaartitemtypes te maken. Bestaande kaarten wordek automatisch verwijderd." + }, + "restrictCardTypeImport": { + "message": "Kan kaart item types niet importeren" + }, + "restrictCardTypeImportDesc": { + "message": "Een beleid ingesteld door 1 of meer organisaties voorkomt dat je kaarten naar je kluizen kunt importeren." }, "yourSingleUseRecoveryCode": { "message": "Met je herstelcode voor eenmalig gebruik kun je tweestapsaanmelding uitschakelen in het geval dat je toegang verliest tot je tweestapsaanmeldingsprovider. Bitwarden adviseert de herstelcode op te schrijven en op een veilige plaats te bewaren." @@ -2218,6 +2224,9 @@ "disable": { "message": "Uitschakelen" }, + "orgUserDetailsNotFound": { + "message": "Details van lid niet gevonden." + }, "revokeAccess": { "message": "Toegang intrekken" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Standaardverzameling" }, + "myItems": { + "message": "Mijn items" + }, "getHelp": { "message": "Hulp vragen" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Noodtoegang afgewezen" }, + "grantorDetailsNotFound": { + "message": "Details van concessiegever niet gevonden" + }, "passwordResetFor": { "message": "Wachtwoord opnieuw ingesteld voor $USER$. Je kunt nu inloggen met het nieuwe wachtwoord.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Gegevenseigendom van organisatie afdwingen" + }, "personalOwnership": { "message": "Persoonlijk eigendom" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Doorgaan logt de huidige sessie van $NAME$ uit, waarna deze opnieuw moet aanmelden. Actieve sessies op andere apparaten kunnen mogelijk nog één uur actief blijven.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "deze gebruiker" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Een of meer organisatiebeleidseisen stelt de volgende eisen aan je hoofdwachtwoord:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Een of meer organisatiebeleidseisen stelt de volgende eisen aan je hoofdwachtwoord:" + }, "resetPasswordSuccess": { "message": "Wachtwoord opnieuw ingesteld!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Factuuradres vereist voor het toevoegen van krediet.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index dee5e91aa9c..79700ff65e3 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Slå av" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 3661600ce58..f422e74a569 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 7199de0f2bb..13e67f9d33c 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Włączenie logowania dwustopniowego można trwale zablokować konto Bitwarden. Kod odzyskiwania pozwala na dostęp do konta w przypadku, gdy nie będziesz mógł skorzystać ze standardowego dostawcy logowania dwustopniowego (np. w przypadku utraty urządzenia). Pomoc techniczna Bitwarden nie będzie w stanie Ci pomóc, jeśli stracisz dostęp do swojego konta. Zalecamy zapisanie lub wydrukowanie kodu odzyskiwania i przechowywanie go w bezpiecznym miejscu." }, - "restrictedItemTypesPolicy": { - "message": "Usuń typ elementu karty" + "restrictedItemTypePolicy": { + "message": "Usuń elementy typu karty" }, - "restrictedItemTypesPolicyDesc": { - "message": "Nie zezwalaj członkom na tworzenie typów elementów karty." + "restrictedItemTypePolicyDesc": { + "message": "Nie zezwalaj członkom na tworzenie elementów typu karty. Istniejące karty zostaną automatycznie usunięte." + }, + "restrictCardTypeImport": { + "message": "Nie można importować elementów typu karty" + }, + "restrictCardTypeImportDesc": { + "message": "Polityka ustawiona przez 1 lub więcej organizacji uniemożliwia importowanie kart do sejfów." }, "yourSingleUseRecoveryCode": { "message": "Jednorazowy kod odzyskiwania może być użyty do wyłączenia dwuetapowego logowania w przypadku utraty dostępu do dostawcy logowania dwuetapowego. Bitwarden zaleca zapisanie kodu odzyskiwania i przechowywanie go w bezpiecznym miejscu." @@ -2218,6 +2224,9 @@ "disable": { "message": "Wyłącz" }, + "orgUserDetailsNotFound": { + "message": "Nie znaleziono szczegółów użytkownika." + }, "revokeAccess": { "message": "Unieważnij dostęp" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Domyślna kolekcja" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Uzyskaj pomoc" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Dostęp awaryjny został odrzucony" }, + "grantorDetailsNotFound": { + "message": "Nie znaleziono szczegółów dotacji" + }, "passwordResetFor": { "message": "Zresetowałeś hasło użytkownika $USER$. Możesz zalogować się za pomocą nowego hasła.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Wymuś własność danych organizacji" + }, "personalOwnership": { "message": "Własność osobista" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Kontynuowanie spowoduje wylogowanie użytkownika $NAME$ z obecnej sesji i będzie musiał zalogować się ponownie. Aktywne sesje na innych urządzeniach mogą pozostać aktywne przez maksymalnie godzinę.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "ten użytkownik" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Co najmniej jedna zasada organizacji wymaga, aby hasło główne spełniało następujące wymagania:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Co najmniej jedna zasada organizacji wymaga, aby hasło główne spełniało następujące wymagania:" + }, "resetPasswordSuccess": { "message": "Hasło zostało zresetowane!" }, @@ -7615,7 +7645,7 @@ "description": "Notifies that a service account has been updated" }, "typeOrSelectProjects": { - "message": "Type or select projects", + "message": "Wpisz lub wybierz projekty", "description": "Instructions for selecting projects for a service account" }, "newSaTypeToFilter": { @@ -7702,7 +7732,7 @@ "description": "Title for the section displaying access tokens." }, "createAccessToken": { - "message": "Create access token", + "message": "Utwórz token dostępu", "description": "Button label for creating a new access token." }, "expires": { @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Aby dodać środki, wymagany jest adres rozliczeniowy.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index cfd998f2bfd..da4180ee0e6 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "A configuração de login em duas etapas pode bloqueá-lo permanentemente da sua conta no Bitwarden. Um código de recuperação permite que você acesse sua conta no caso de não poder mais usar seu provedor de login em duas etapas normalmente (exemplo: você perde seu dispositivo). O suporte do Bitwarden não será capaz de ajudá-lo se você perder o acesso à sua conta. Recomendamos que você anote ou imprima o código de recuperação e o mantenha em um lugar seguro." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Seu código de recuperação de uso único pode ser usado para desativar o login em duas etapas no caso de você perder acesso ao seu provedor de login em duas etapas. O Bitwarden recomenda que você anote o código de recuperação e o mantenha em um lugar seguro." @@ -2218,6 +2224,9 @@ "disable": { "message": "Desabilitar" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revogar acesso" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Coleção Padrão" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Obter Ajuda" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Acesso de emergência rejeitado" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Redefinição de senha para $USER$. Agora você pode acessar usando a nova senha.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Propriedade Pessoal" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "este usuário" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Uma ou mais políticas da organização exigem que a senha mestra cumpra aos seguintes requisitos:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Senha redefinida com sucesso!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 4b9343a62ca..eb06f4b7390 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "A configuração da verificação de dois passos pode bloquear permanentemente a sua conta Bitwarden. Um código de recuperação permite-lhe aceder à sua conta no caso de já não poder utilizar o seu fornecedor normal de verificação de dois passos (por exemplo, se perder o seu dispositivo). O suporte Bitwarden não poderá ajudá-lo se perder o acesso à sua conta. Recomendamos que anote ou imprima o código de recuperação e o guarde num local seguro." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remover o tipo de item do cartão" }, - "restrictedItemTypesPolicyDesc": { - "message": "Não permitir que os membros criem tipos de itens de cartão." + "restrictedItemTypePolicyDesc": { + "message": "Não permitir que os membros criem tipos de itens de cartão. Os cartões existentes serão automaticamente removidos." + }, + "restrictCardTypeImport": { + "message": "Não é possível importar tipos de itens de cartão" + }, + "restrictCardTypeImportDesc": { + "message": "Uma política definida por 1 ou mais organizações impede-o de importar cartões para os seus cofres." }, "yourSingleUseRecoveryCode": { "message": "O seu código de recuperação de utilização única pode ser utilizado para desativar a verificação de dois passos no caso de perder o acesso ao seu fornecedor de verificação de dois passos. O Bitwarden recomenda que anote o código de recuperação e o guarde num local seguro." @@ -2218,6 +2224,9 @@ "disable": { "message": "Desativar" }, + "orgUserDetailsNotFound": { + "message": "Detalhes do membro não encontrados." + }, "revokeAccess": { "message": "Revogar o acesso" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Coleção predefinida" }, + "myItems": { + "message": "Os meus itens" + }, "getHelp": { "message": "Obter ajuda" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Acesso de emergência rejeitado" }, + "grantorDetailsNotFound": { + "message": "Detalhes do concedente não encontrados" + }, "passwordResetFor": { "message": "Palavra-passe de $USER$ redefinida. Pode agora iniciar sessão utilizando a nova palavra-passe.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Reforçar a propriedade dos dados da organização" + }, "personalOwnership": { "message": "Remover cofre pessoal" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Ao prosseguir, terminará a sessão atual de $NAME$ e terá de iniciar sessão novamente. As sessões ativas noutros dispositivos poderão continuar ativas até uma hora.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "este utilizador" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Uma ou mais políticas da organização exigem que a palavra-passe mestra cumpra os seguintes requisitos:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Uma ou mais políticas da organização exigem que a palavra-passe mestra cumpra os seguintes requisitos:" + }, "resetPasswordSuccess": { "message": "Palavra-passe redefinida com sucesso!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Endereço de faturação necessário para adicionar crédito.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index afec83c9395..569faf4e9a8 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Configurarea unei autentificări în două etape vă poate bloca permanent din contul Bitwarden. Un cod de recuperare vă permite să vă accesați contul în cazul în care nu mai puteți utiliza furnizorul normal de autentificare în două etape (exemplu: vă pierdeți dispozitivul). Serviciul de asistență Bitwarden nu vă va putea ajuta dacă pierdeți accesul la cont. Vă recomandăm să notați sau să imprimați codul de recuperare și să-l păstrați într-un loc sigur." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Dezactivare" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revocare acces" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Colecție implicită" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Obținere ajutor" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Acces de urgență respins" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "S-a resetat parola pentru $USER$. Vă puteți conecta acum cu noua parolă.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Înlăturați seiful personal" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "acest utilizator" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Una sau mai multe politici ale organizației, necesită ca parola principală să îndeplinească următoarele cerințe:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Parolă resetată cu succes!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 95c100551c3..d57bdc8ed45 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "При включении двухэтапной аутентификации вы можете навсегда потерять доступ к вашей учетной записи Bitwarden. Код восстановления позволяет получить доступ к вашему аккаунту в случае, если вы больше не можете использовать свой обычный метод двухэтапной аутентификации (например, при потере устройства). Служба поддержки Bitwarden не сможет вам помочь, если вы потеряете доступ к своему аккаунту. Мы рекомендуем вам записать или распечатать код восстановления и хранить его в надежном месте." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Удалить элемент типа карта" }, - "restrictedItemTypesPolicyDesc": { - "message": "Не разрешать пользователям создавать элемент типа карта." + "restrictedItemTypePolicyDesc": { + "message": "Не разрешать пользователям создавать элементы карт. Существующие карты будут автоматически удалены." + }, + "restrictCardTypeImport": { + "message": "Невозможно импортировать элементы карт" + }, + "restrictCardTypeImportDesc": { + "message": "Политика, установленная 1 или более организациями, не позволяет импортировать карты в ваши хранилища." }, "yourSingleUseRecoveryCode": { "message": "Одноразовый код восстановления можно использовать для отключения двухэтапной аутентификации в случае потери доступа к провайдеру двухэтапной аутентификации. Bitwarden рекомендует записать код восстановления и хранить его в надежном месте." @@ -2218,6 +2224,9 @@ "disable": { "message": "Отключить" }, + "orgUserDetailsNotFound": { + "message": "Данные участника не найдены." + }, "revokeAccess": { "message": "Отозвать доступ" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Коллекция по умолчанию" }, + "myItems": { + "message": "Мои элементы" + }, "getHelp": { "message": "Получить помощь" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "В экстренном доступе отказано" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Сброшен пароль для $USER$. Теперь вы можете войти, используя новый пароль.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Удалить личное хранилище" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "В случае продолжения сессия $NAME$ будет завершена, что потребует повторной авторизации. Сессии на других устройствах могут оставаться активными до одного часа.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "этот пользователь" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Согласно одной или нескольким политикам организации необходимо, чтобы мастер-пароль отвечал следующим требованиям:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Согласно одной или нескольким политикам организации необходимо, чтобы мастер-пароль отвечал следующим требованиям:" + }, "resetPasswordSuccess": { "message": "Пароль успешно сброшен!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Для пополнения счета необходим платежный адрес.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 09705e19ecd..80dd700f848 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index bf2acb5838b..c3f204beaf3 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Zapnutie dvojstupňového prihlásenia vás môže natrvalo vymknúť z vášho Bitwarden účtu. Záchranný kód umožňuje prístup k vášmu kontu v prípade že už nemôžete použiť svoj normálny dvojstupňový spôsob overenia. (napríklad ak stratíte zariadenie) Zákaznícka podpora nebude schopná pomôcť vám ak stratíte prístup k účtu. Preto vám odporúčame zapísať si, alebo si vytlačiť záchranný kód a uložiť ho na bezpečnom mieste." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Odstrániť typ položky pre kartu" }, - "restrictedItemTypesPolicyDesc": { - "message": "Nedovoliť členom vytvárať typy položiek pre karty." + "restrictedItemTypePolicyDesc": { + "message": "Neumožniť členom vytvárať typ položky pre kartu. Existujúce karty budú automaticky odstránené." + }, + "restrictCardTypeImport": { + "message": "Položky typu karta sa nedajú importovať" + }, + "restrictCardTypeImportDesc": { + "message": "Politika nastavená 1 alebo viacerými organizáciami vám bráni v importovaní kariet do vašich trezorov." }, "yourSingleUseRecoveryCode": { "message": "Váš jednorázový záchranný kód sa dá použiť na vypnutie dvojstupňového prihlasovania ak ste stratili pristúp k jeho poskytovateľovi. Bitwarden odporúča, aby ste si záchranný kód zapísali a odložili na bezpečné miesto." @@ -2218,6 +2224,9 @@ "disable": { "message": "Vypnúť" }, + "orgUserDetailsNotFound": { + "message": "Detaily o členovi nenájdené." + }, "revokeAccess": { "message": "Zrušiť prístup" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Predvolená kolekcia" }, + "myItems": { + "message": "Moje položky" + }, "getHelp": { "message": "Získať pomoc" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Núdzový prístup odmietnutý" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Resetovanie hesla pre $USER$. Teraz sa môžete prihlásiť s novým heslom.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Požadovanie vlastníctva údajov organizácie" + }, "personalOwnership": { "message": "Zakázať osobný trezor" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Pokračovaním sa $NAME$ odhlási z aktuálnej relácie a bude sa musieť znova prihlásiť. Aktívne relácie na iných zariadeniach môžu zostať aktívne až jednu hodinu.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "tento používateľ" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Jedno alebo viac pravidiel organizácie požadujú, aby hlavné heslo spĺňalo nasledujúce požiadavky:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Jedno alebo viac pravidiel organizácie požadujú, aby hlavné heslo spĺňalo nasledujúce požiadavky:" + }, "resetPasswordSuccess": { "message": "Heslo bolo úspešne obnovené!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Na pridanie kreditu je potrebná fakturačná adresa.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 66930baf451..5b68d7e21af 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Onemogočeno" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Odvzemi dostop" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Pomoč" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 380122b4f71..c0681d1224f 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/sr_CY/messages.json b/apps/web/src/locales/sr_CY/messages.json index 6e805557c65..837b8c706b8 100644 --- a/apps/web/src/locales/sr_CY/messages.json +++ b/apps/web/src/locales/sr_CY/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Омогућавање пријаве у два корака може вас трајно закључати са вашег Bitwarden-а налога. Код за опоравак омогућава вам приступ вашем налогу у случају да више не можете да користите свог уобичајеног добављача услуге пријављивања у два корака (нпр. ако изгубите уређај). Подршка Bitwarden-а неће вам моћи помоћи ако изгубите приступ свом налогу. Препоручујемо да запишете или одштампате код за опоравак и сачувате га на сигурном месту." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Уклоните тип ставке картице" }, - "restrictedItemTypesPolicyDesc": { - "message": "Не дозволите члановима да креирају тип ставке картице." + "restrictedItemTypePolicyDesc": { + "message": "Не дозволите члановима да креирају врсте предмета картице. Постојеће картице ће се аутоматски уклонити." + }, + "restrictCardTypeImport": { + "message": "Не могу увозити врсте картица" + }, + "restrictCardTypeImportDesc": { + "message": "Политика која је поставила 1 или више организација спречава вас да се увозе картице у сефу." }, "yourSingleUseRecoveryCode": { "message": "Ваш јединствени кôд за опоравак може се користити за искључивање у два корака у случају да изгубите приступ свом двоструком провајдеру пријаве. Bitwarden препоручује да запишете кôд за опоравак и држите га на сигурном месту." @@ -2218,6 +2224,9 @@ "disable": { "message": "Онемогући" }, + "orgUserDetailsNotFound": { + "message": "Детаљи чланова нису пронађени." + }, "revokeAccess": { "message": "Опозови Приступ" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Стандардна колекција" }, + "myItems": { + "message": "Моји предмети" + }, "getHelp": { "message": "Потражи помоћ" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Одбијен хитни приступ" }, + "grantorDetailsNotFound": { + "message": "Детаљи одобравања нису пронађени" + }, "passwordResetFor": { "message": "Ресетовање лозинке за $USER$. Сада се можете пријавити помоћу нове лозинке.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Спровести власништво података о организацији" + }, "personalOwnership": { "message": "Лично власништво" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Ако наставите, објавићете $NAME$ са тренутне сесије, захтевајући их да се поново пријаве. Активне сесије на другим уређајима могу наставити да остају активне до једног сата.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "овај корисник" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Једна или више смерница организације захтевају главну лозинку да би испуњавали следеће захтеве:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "Једна или више смерница организације захтевају главну лозинку да би испуњавали следеће захтеве:" + }, "resetPasswordSuccess": { "message": "Успешно ресетовање лозинке!" }, @@ -7615,7 +7645,7 @@ "description": "Notifies that a service account has been updated" }, "typeOrSelectProjects": { - "message": "Type or select projects", + "message": "Унети или одабрати пројекте", "description": "Instructions for selecting projects for a service account" }, "newSaTypeToFilter": { @@ -7702,7 +7732,7 @@ "description": "Title for the section displaying access tokens." }, "createAccessToken": { - "message": "Create access token", + "message": "Креирати приступни токен", "description": "Button label for creating a new access token." }, "expires": { @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Адреса за наплату је потребна за додавање кредита.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 1a2c3f0ec5a..41813e5edc0 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Att aktivera tvåstegsverifiering kan låsa ute dig från ditt Bitwarden-konto permanent. En återställningskod låter dig komma åt ditt konto om du inte längre kan använda din vanliga metod för tvåstegsverifiering (t.ex. om du förlorar din enhet). Bitwardens kundservice kommer inte att kunna hjälpa dig om du förlorar åtkomst till ditt konto. Vi rekommenderar att du skriver ner eller skriver ut återställningskoden och förvarar den på ett säkert ställe." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Stäng av" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Återkalla åtkomst" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Standardsamling" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Få hjälp" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Nödåtkomst nekad" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Lösenordet för $USER$ återställdes. Du kan nu logga in med det nya lösenordet.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Radera individuellt valv" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "denna användare" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "En eller flera organisationspolicyer kräver att huvudlösenordet uppfyller följande krav:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Lösenordsåterställningen lyckades!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 3661600ce58..f422e74a569 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 5913116b462..c47c6caca85 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Turn off" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Revoke access" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Get help" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index b7f8212c05e..211851f8450 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "İki aşamalı girişi etkinleştirmek, Bitwarden hesabınızı kalıcı olarak kilitleyebilir. Kurtarma kodunuz, iki aşamalı giriş sağlayıcınızı kullanamamanız durumunda hesabınıza erişmenize olanak sağlar (ör. cihazınızı kaybedersiniz). Hesabınıza erişiminizi kaybederseniz Bitwarden destek ekibi size yardımcı olamaz. Kurtarma kodunu not almanızı veya yazdırmanızı ve güvenli bir yerde saklamanızı öneririz." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Kart kaydı türünü kaldır" }, - "restrictedItemTypesPolicyDesc": { - "message": "Üyelerin kart kaydı türü oluşturmasına izin verme." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Devre dışı bırak" }, + "orgUserDetailsNotFound": { + "message": "Üye bilgileri bulunamadı." + }, "revokeAccess": { "message": "Erişimi iptal et" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Varsayılan koleksiyon" }, + "myItems": { + "message": "Kayıtlarım" + }, "getHelp": { "message": "Yardım al" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Acil durum erişimi reddedildi" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "$USER$ için parola sıfırlandı. Artık yeni parola ile giriş yapabilirsiniz.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Kişisel kasayı kaldır" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "bu kullanıcı" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Bir veya daha fazla kuruluş ilkesi gereğince ana parola aşağıdaki gereksinimleri karşılamalıdır:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Parola başarıyla sıfırlandı." }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index b95db61651c..4ddcfa3c463 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Увімкнення двоетапної перевірки може цілком заблокувати доступ до облікового запису Bitwarden. Код відновлення дає вам змогу отримати доступ до свого облікового запису у випадку, якщо ви не можете скористатися провайдером двоетапної перевірки (наприклад, якщо втрачено пристрій). Служба підтримки Bitwarden не зможе допомогти відновити доступ до вашого облікового запису. Ми радимо вам записати чи надрукувати цей код відновлення і зберігати його в надійному місці." }, - "restrictedItemTypesPolicy": { - "message": "Вилучити тип запису \"Картка\"" + "restrictedItemTypePolicy": { + "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Не дозволяти учасникам створювати записи типу \"Картка\"." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Одноразовий код відновлення можна використати для вимкнення двоетапної перевірки у випадку, якщо ви втратите доступ до вашого провайдера двоетапної перевірки. Bitwarden рекомендує вам записати код відновлення і зберігати його в надійному місці." @@ -2218,6 +2224,9 @@ "disable": { "message": "Вимкнути" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Відкликати доступ" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Типова збірка" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Отримати допомогу" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Екстрений доступ відхилено" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Пароль для користувача $USER$ скинуто. Тепер ви можете увійти використовуючи новий пароль.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Вилучити особисте сховище" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "цей користувач" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "Одна або декілька політик організації вимагають дотримання таких вимог для головного пароля:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Пароль успішно скинуто!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 03d8238f9bd..c3bbf855a38 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "Vô hiệu hoá" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "Thu hồi quyền truy cập" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "Default collection" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "Nhận trợ giúp" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "Emergency access rejected" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "Password reset for $USER$. You can now login using the new password.", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "Remove individual vault" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "this user" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "One or more organization policies require the master password to meet the following requirements:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "Password reset success!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index 7cde4854120..550d4d56bdd 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "启用两步登录可能会将您永久锁定在 Bitwarden 账户之外。当您无法使用常规的两步登录提供程序(例如您丢失了设备)时,可以使用恢复代码访问您的账户。如果您失去对您账户的访问,Bitwarden 支持也无法帮助您。我们建议您写下或打印恢复代码,并将其妥善保管。" }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "禁用支付卡项目类型" }, - "restrictedItemTypesPolicyDesc": { - "message": "不允许成员创建支付卡项目类型。" + "restrictedItemTypePolicyDesc": { + "message": "不允许成员创建支付卡项目类型。现有支付卡将自动被移除。" + }, + "restrictCardTypeImport": { + "message": "无法导入支付卡项目类型" + }, + "restrictCardTypeImportDesc": { + "message": "由 1 个或多个组织设置的策略阻止您将支付卡导入密码库。" }, "yourSingleUseRecoveryCode": { "message": "当您无法访问两步登录提供程序时,您的一次性恢复代码可用于停用两步登录。Bitwarden 建议您写下恢复代码,并将其妥善保管。" @@ -2189,7 +2195,7 @@ "message": "需要高级会员" }, "premiumRequiredDesc": { - "message": "此功能需要高级会员资格。" + "message": "使用此功能需要高级会员资格。" }, "youHavePremiumAccess": { "message": "您拥有高级访问权限" @@ -2218,6 +2224,9 @@ "disable": { "message": "停用" }, + "orgUserDetailsNotFound": { + "message": "未找到成员详细信息。" + }, "revokeAccess": { "message": "撤销访问权限" }, @@ -2608,7 +2617,7 @@ "message": "检查泄漏情况" }, "breachUsernameNotFound": { - "message": "在任何已知数据泄漏中找不到 $USERNAME$。", + "message": "没有在已知的数据泄露中发现 $USERNAME$。", "placeholders": { "username": { "content": "$1", @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "默认集合" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "获取帮助" }, @@ -4345,7 +4357,7 @@ "description": "Upper limit of seats to allow through autoscaling" }, "maxSeatCost": { - "message": "最大潜在席位费用" + "message": "最大潜在的席位费用" }, "addSeats": { "message": "添加席位", @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "紧急访问已拒绝" }, + "grantorDetailsNotFound": { + "message": "未找到授予人详细信息" + }, "passwordResetFor": { "message": "$USER$ 的密码已重置。您现在可以使用新密码登录了。", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "强制组织数据所有权" + }, "personalOwnership": { "message": "禁用个人密码库" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "继续操作会将 $NAME$ 登出当前会话,要求他们重新登录。在其他设备上的活动会话可能继续活动长达一个小时。", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "此用户" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "一个或多个组织策略要求主密码满足以下要求:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "一个或多个组织策略要求主密码满足以下要求:" + }, "resetPasswordSuccess": { "message": "密码重置成功!" }, @@ -6681,7 +6711,7 @@ "message": "上面的 1 个字段需要您注意。" }, "fieldRequiredError": { - "message": "$FIELDNAME$ 必填。", + "message": "必须填写 $FIELDNAME$。", "placeholders": { "fieldname": { "content": "$1", @@ -8436,7 +8466,7 @@ } }, "notFound": { - "message": "$RESOURCE$ 未找到", + "message": "未找到 $RESOURCE$", "placeholders": { "resource": { "content": "$1", @@ -8698,13 +8728,13 @@ "message": "管理组织的集合行为" }, "limitCollectionCreationDesc": { - "message": "限制为所有者和管理员可以创建集合" + "message": "仅限所有者和管理员可以创建集合" }, "limitCollectionDeletionDesc": { - "message": "限制为所有者和管理员可以删除集合" + "message": "仅限所有者和管理员可以删除集合" }, "limitItemDeletionDescription": { - "message": "限制为具有「管理集合」权限的成员可以删项目" + "message": "仅限具有「管理集合」权限的成员可以删除项目" }, "allowAdminAccessToAllCollectionItemsDesc": { "message": "所有者和管理员可以管理所有集合和项目" @@ -8734,7 +8764,7 @@ "message": "服务账户限制(可选)" }, "maxServiceAccountCost": { - "message": "最大潜在服务账户费用" + "message": "最大潜在的服务账户费用" }, "loggedInExclamation": { "message": "已登录!" @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "添加信用额度需要计费地址。", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index f5bb444aaf1..2193c9503ab 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -2153,11 +2153,17 @@ "twoStepLoginRecoveryWarning": { "message": "啟用兩步驟登入可能會將您永久鎖定在您的 Bitwarden 帳戶外。如果您無法正常使用兩步驟登入方式(例如,您遺失了裝置),則可以使用復原碼存取您的帳戶。 如果您失去帳戶的存取權限,Bitwarden 也無法幫助您。所以我們建議您記下或列印復原碼,並將其妥善保存。" }, - "restrictedItemTypesPolicy": { + "restrictedItemTypePolicy": { "message": "Remove card item type" }, - "restrictedItemTypesPolicyDesc": { - "message": "Do not allow members to create card item types." + "restrictedItemTypePolicyDesc": { + "message": "Do not allow members to create card item types. Existing cards will be automatically removed." + }, + "restrictCardTypeImport": { + "message": "Cannot import card item types" + }, + "restrictCardTypeImportDesc": { + "message": "A policy set by 1 or more organizations prevents you from importing cards to your vaults." }, "yourSingleUseRecoveryCode": { "message": "Your single-use recovery code can be used to turn off two-step login in the event that you lose access to your two-step login provider. Bitwarden recommends you write down the recovery code and keep it in a safe place." @@ -2218,6 +2224,9 @@ "disable": { "message": "停用" }, + "orgUserDetailsNotFound": { + "message": "Member details not found." + }, "revokeAccess": { "message": "撤銷存取權限" }, @@ -3266,6 +3275,9 @@ "defaultCollection": { "message": "預設集合" }, + "myItems": { + "message": "My Items" + }, "getHelp": { "message": "尋求幫助" }, @@ -5363,6 +5375,9 @@ "emergencyRejected": { "message": "已拒絕緊急存取" }, + "grantorDetailsNotFound": { + "message": "Grantor details not found" + }, "passwordResetFor": { "message": "$USER$ 的密碼已重設。您現在可以使用新密碼登入了。", "placeholders": { @@ -5372,6 +5387,9 @@ } } }, + "organizationDataOwnership": { + "message": "Enforce organization data ownership" + }, "personalOwnership": { "message": "停用個人密碼庫" }, @@ -5763,12 +5781,24 @@ } } }, + "emergencyAccessLoggedOutWarning": { + "message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.", + "placeholders": { + "name": { + "content": "$1", + "example": "John Smith" + } + } + }, "thisUser": { "message": "此使用者" }, "resetPasswordMasterPasswordPolicyInEffect": { "message": "一個或多個組織原則要求主密碼須符合下列條件:" }, + "changePasswordDelegationMasterPasswordPolicyInEffect": { + "message": "One or more organization policies require the master password to meet the following requirements:" + }, "resetPasswordSuccess": { "message": "密碼重設成功!" }, @@ -10649,5 +10679,9 @@ "example": "12/31/2024" } } + }, + "billingAddressRequiredToAddCredit": { + "message": "Billing address required to add credit.", + "description": "Error message shown when trying to add credit to a trialing organization without a billing address." } } diff --git a/libs/angular/src/auth/guards/auth.guard.spec.ts b/libs/angular/src/auth/guards/auth.guard.spec.ts index f64d6cf769d..a2e1613c6c1 100644 --- a/libs/angular/src/auth/guards/auth.guard.spec.ts +++ b/libs/angular/src/auth/guards/auth.guard.spec.ts @@ -127,7 +127,6 @@ describe("AuthGuard", () => { describe("given user is Unlocked", () => { describe("given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { const tests = [ - ForceSetPasswordReason.SsoNewJitProvisionedUser, ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, ForceSetPasswordReason.TdeOffboarding, ]; @@ -167,10 +166,6 @@ describe("AuthGuard", () => { describe("given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => { const tests = [ - { - reason: ForceSetPasswordReason.SsoNewJitProvisionedUser, - url: "/set-password-jit", - }, { reason: ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, url: "/set-password", diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index a172c45d6f9..7b8c21fef62 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -39,22 +39,6 @@ export const authGuard: CanActivateFn = async ( return false; } - if (authStatus === AuthenticationStatus.Locked) { - if (routerState != null) { - messagingService.send("lockedUrl", { url: routerState.url }); - } - // TODO PM-9674: when extension refresh is finished, remove promptBiometric - // as it has been integrated into the component as a default feature. - return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } }); - } - - if ( - !routerState.url.includes("remove-password") && - (await firstValueFrom(keyConnectorService.convertAccountRequired$)) - ) { - return router.createUrlTree(["/remove-password"]); - } - const userId = (await firstValueFrom(accountService.activeAccount$)).id; const forceSetPasswordReason = await firstValueFrom( masterPasswordService.forceSetPasswordReason$(userId), @@ -69,12 +53,31 @@ export const authGuard: CanActivateFn = async ( // User JIT provisioned into a master-password-encryption org if ( + authStatus === AuthenticationStatus.Locked && forceSetPasswordReason === ForceSetPasswordReason.SsoNewJitProvisionedUser && - !routerState.url.includes("set-password-jit") && - !routerState.url.includes("set-initial-password") + !routerState.url.includes("set-initial-password") && + isSetInitialPasswordFlagOn ) { - const route = isSetInitialPasswordFlagOn ? "/set-initial-password" : "/set-password-jit"; - return router.createUrlTree([route]); + return router.createUrlTree(["/set-initial-password"]); + } + + if ( + authStatus === AuthenticationStatus.Locked && + forceSetPasswordReason !== ForceSetPasswordReason.SsoNewJitProvisionedUser + ) { + if (routerState != null) { + messagingService.send("lockedUrl", { url: routerState.url }); + } + // TODO PM-9674: when extension refresh is finished, remove promptBiometric + // as it has been integrated into the component as a default feature. + return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } }); + } + + if ( + !routerState.url.includes("remove-password") && + (await firstValueFrom(keyConnectorService.convertAccountRequired$)) + ) { + return router.createUrlTree(["/remove-password"]); } // TDE org user has "manage account recovery" permission @@ -100,10 +103,10 @@ export const authGuard: CanActivateFn = async ( // Post- Account Recovery or Weak Password on login if ( - forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset || - (forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword && - !routerState.url.includes("update-temp-password") && - !routerState.url.includes("change-password")) + (forceSetPasswordReason === ForceSetPasswordReason.AdminForcePasswordReset || + forceSetPasswordReason === ForceSetPasswordReason.WeakMasterPassword) && + !routerState.url.includes("update-temp-password") && + !routerState.url.includes("change-password") ) { const route = isChangePasswordFlagOn ? "/change-password" : "/update-temp-password"; return router.createUrlTree([route]); diff --git a/libs/angular/src/auth/password-management/README.md b/libs/angular/src/auth/password-management/README.md new file mode 100644 index 00000000000..ca5a6355fcb --- /dev/null +++ b/libs/angular/src/auth/password-management/README.md @@ -0,0 +1,216 @@ +# Master Password Management Flows + +The Auth Team manages several components that allow a user to either: + +1. Set an initial master password +2. Change an existing master password + +This document maps all of our password management flows to the components that handle them. + +
+ +**Table of Contents** + +> - [The Base `InputPasswordComponent`](#the-base-inputpasswordcomponent) +> - [Set Initial Password Flows](#set-initial-password-flows) +> - [Change Password Flows](#change-password-flows) + +
+ +**Acronyms** + +
    +
  • MP = "master password"
  • +
  • MPE = "master password encryption"
  • +
  • TDE = "trusted device encryption"
  • +
  • JIT provision = "just-in-time provision"
  • +
+ +
+ +## The Base `InputPasswordComponent` + +Central to our master password management flows is the base [InputPasswordComponent](https://components.bitwarden.com/?path=/docs/auth-input-password--docs), which is responsible for displaying the appropriate form fields in the UI, performing form validation, and generating appropriate cryptographic properties for each flow. This keeps our UI, validation, and key generation consistent across all master password management flows. + +
+ +## Set Initial Password Flows + + + + + + + + + + + + + + + + + + + + + + + + + +
FlowRoute
(on which user sets MP)
Component(s)
+
+ Account Registration +

+
    +
  1. Standard Flow
  2. +
    +
  3. Self Hosted Flow
  4. +
    +
  5. Email Invite Flows (🌐 web only)
  6. +
    +
+
/finish-signup + RegistrationFinishComponent +
+ - embeds InputPasswordComponent +
+ Trial Initiation (🌐 web only) + /trial-initiation or
/secrets-manager-trial-initiation
+ CompleteTrialInitiationComponent +
+ - embeds InputPasswordComponent +
+
+ Upon Authentication (an existing authed user) +

+
    +
  1. User JIT provisions* into an MPE org
  2. +
    +
  3. + User JIT provisions* into a TDE org with the "manage account recovery" permission +

    That is, the user was given this permission on invitation or by the time they JIT provision.

    +
  4. +
    +
  5. + TDE user permissions upgraded +

    TDE user authenticates after permissions were upgraded to include "manage account recovery".

    +
  6. +
    +
  7. + TDE offboarding +

    User authenticates after their org offboarded from TDE and is now a MPE org.

    +

    User must be on a trusted device to set MP, otherwise user must go through Account Recovery.

    +
  8. +
+
/set-initial-password + SetInitialPasswordComponent +
+ - embeds InputPasswordComponent +
+ +\* A note on JIT provisioned user flows: + +- Even though a JIT provisioned user is a brand-new user who was “just” created, we consider them to be an “existing authed user” _from the perspective of the set initial password flow_. This is because at the time they set their initial password, their account already exists in the database (before setting their password) and they have already authenticated via SSO. +- The same is not true in the _Account Registration_ flows above—that is, during account registration when a user reaches the `/finish-signup` or `/trial-initiation` page to set their initial password, their account does not yet exist in the database, and will only be created once they set an initial password. + +
+ +## Change Password Flows + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FlowRoute
(on which user changes MP)
Component(s)
+
+ Account Settings + (Docs) +
+ (🌐 web only) +

+

User changes MP via account settings.

+
+
+ /settings/security/password +
(security-routing.module.ts) +
+ PasswordSettingsComponent +
- embeds ChangePasswordComponent +
- embeds InputPasswordComponent +
+
+ Upon Authentication +

+
    +
  1. + Login with non-compliant MP after email accept (🌐 web only) +

    User clicks an org email invite link and logs in with their MP that does not meet the org’s policy requirements.

    +
  2. +
    +
  3. + Login with non-compliant MP +

    Existing org user logs in with their MP that does not meet updated org policy requirements.

    +
  4. +
    +
  5. + Login after Account Recovery +

    User logs in after their MP was reset via Account Recovery.

    +
  6. +
+
/change-password + ChangePasswordComponent +
- embeds InputPasswordComponent +
+
+ Emergency Access Takeover + (Docs) +
+ (🌐 web only) +

+

Emergency access Grantee changes the MP for the Grantor.

+
+
Grantee opens dialog while on /settings/emergency-access + EmergencyAccessTakeoverDialogComponent +
- embeds InputPasswordComponent +
+
+ Account Recovery + (Docs) +
+ (🌐 web only) +

+

Org member with "manage account recovery" permission changes the MP for another org user via Account Recovery.

+
+
Org member opens dialog while on /organizations/{org-id}/members + AccountRecoveryDialogComponent +
- embeds InputPasswordComponent +
diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts new file mode 100644 index 00000000000..1c5edb00c8c --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -0,0 +1,248 @@ +import { firstValueFrom } from "rxjs"; + +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + OrganizationUserApiService, + OrganizationUserResetPasswordEnrollmentRequest, +} from "@bitwarden/admin-console/common"; +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management"; + +import { + SetInitialPasswordService, + SetInitialPasswordCredentials, + SetInitialPasswordUserType, +} from "./set-initial-password.service.abstraction"; + +export class DefaultSetInitialPasswordService implements SetInitialPasswordService { + constructor( + protected apiService: ApiService, + protected encryptService: EncryptService, + protected i18nService: I18nService, + protected kdfConfigService: KdfConfigService, + protected keyService: KeyService, + protected masterPasswordApiService: MasterPasswordApiService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected organizationUserApiService: OrganizationUserApiService, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, + ) {} + + async setInitialPassword( + credentials: SetInitialPasswordCredentials, + userType: SetInitialPasswordUserType, + userId: UserId, + ): Promise { + const { + newMasterKey, + newServerMasterKeyHash, + newLocalMasterKeyHash, + newPasswordHint, + kdfConfig, + orgSsoIdentifier, + orgId, + resetPasswordAutoEnroll, + } = credentials; + + for (const [key, value] of Object.entries(credentials)) { + if (value == null) { + throw new Error(`${key} not found. Could not set password.`); + } + } + if (userId == null) { + throw new Error("userId not found. Could not set password."); + } + if (userType == null) { + throw new Error("userType not found. Could not set password."); + } + + const masterKeyEncryptedUserKey = await this.makeMasterKeyEncryptedUserKey( + newMasterKey, + userId, + ); + if (masterKeyEncryptedUserKey == null || !masterKeyEncryptedUserKey[1].encryptedString) { + throw new Error("masterKeyEncryptedUserKey not found. Could not set password."); + } + + let keyPair: [string, EncString] | null = null; + let keysRequest: KeysRequest | null = null; + + if (userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + /** + * A user being JIT provisioned into a MP encryption org does not yet have a user + * asymmetric key pair, so we create it for them here. + * + * Sidenote: + * In the case of a TDE user whose permissions require that they have a MP - that user + * will already have a user asymmetric key pair by this point, so we skip this if-block + * so that we don't create a new key pair for them. + */ + + // Extra safety check (see description on https://github.com/bitwarden/clients/pull/10180): + // In case we have have a local private key and are not sure whether it has been posted to the server, + // we post the local private key instead of generating a new one + const existingUserPrivateKey = (await firstValueFrom( + this.keyService.userPrivateKey$(userId), + )) as Uint8Array; + + const existingUserPublicKey = await firstValueFrom(this.keyService.userPublicKey$(userId)); + + if (existingUserPrivateKey != null && existingUserPublicKey != null) { + const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey); + + // Existing key pair + keyPair = [ + existingUserPublicKeyB64, + await this.encryptService.wrapDecapsulationKey( + existingUserPrivateKey, + masterKeyEncryptedUserKey[0], + ), + ]; + } else { + // New key pair + keyPair = await this.keyService.makeKeyPair(masterKeyEncryptedUserKey[0]); + } + + if (keyPair == null) { + throw new Error("keyPair not found. Could not set password."); + } + if (!keyPair[1].encryptedString) { + throw new Error("encrypted private key not found. Could not set password."); + } + + keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); + } + + const request = new SetPasswordRequest( + newServerMasterKeyHash, + masterKeyEncryptedUserKey[1].encryptedString, + newPasswordHint, + orgSsoIdentifier, + keysRequest, + kdfConfig.kdfType, + kdfConfig.iterations, + ); + + await this.masterPasswordApiService.setPassword(request); + + // Clear force set password reason to allow navigation back to vault. + await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId); + + // User now has a password so update account decryption options in state + await this.updateAccountDecryptionProperties( + newMasterKey, + kdfConfig, + masterKeyEncryptedUserKey, + userId, + ); + + /** + * Set the private key only for new JIT provisioned users in MP encryption orgs. + * (Existing TDE users will have their private key set on sync or on login.) + */ + if (keyPair != null && userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + if (!keyPair[1].encryptedString) { + throw new Error("encrypted private key not found. Could not set private key in state."); + } + await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId); + } + + await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId); + + if (resetPasswordAutoEnroll) { + await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId); + } + } + + private async makeMasterKeyEncryptedUserKey( + masterKey: MasterKey, + userId: UserId, + ): Promise<[UserKey, EncString]> { + let masterKeyEncryptedUserKey: [UserKey, EncString] | null = null; + + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + + if (userKey == null) { + masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey); + } else { + masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey); + } + + return masterKeyEncryptedUserKey; + } + + private async updateAccountDecryptionProperties( + masterKey: MasterKey, + kdfConfig: KdfConfig, + masterKeyEncryptedUserKey: [UserKey, EncString], + userId: UserId, + ) { + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); + userDecryptionOpts.hasMasterPassword = true; + await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); + await this.kdfConfigService.setKdfConfig(userId, kdfConfig); + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId); + } + + private async handleResetPasswordAutoEnroll( + masterKeyHash: string, + orgId: string, + userId: UserId, + ) { + const organizationKeys = await this.organizationApiService.getKeys(orgId); + + if (organizationKeys == null) { + throw new Error( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + } + + const orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + + if (userKey == null) { + throw new Error("userKey not found. Could not handle reset password auto enroll."); + } + + // RSA encrypt user key with organization public key + const orgPublicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + userKey, + orgPublicKey, + ); + + if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey.encryptedString) { + throw new Error( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + } + + const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = masterKeyHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + + await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment( + orgId, + userId, + enrollmentRequest, + ); + } +} diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts new file mode 100644 index 00000000000..ca4d9adbd67 --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -0,0 +1,633 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + OrganizationUserApiService, + OrganizationUserResetPasswordEnrollmentRequest, +} from "@bitwarden/admin-console/common"; +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + FakeUserDecryptionOptions as UserDecryptionOptions, + InternalUserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; +import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey, UserKey, UserPrivateKey, UserPublicKey } from "@bitwarden/common/types/key"; +import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management"; + +import { DefaultSetInitialPasswordService } from "./default-set-initial-password.service.implementation"; +import { + SetInitialPasswordCredentials, + SetInitialPasswordService, + SetInitialPasswordUserType, +} from "./set-initial-password.service.abstraction"; + +describe("DefaultSetInitialPasswordService", () => { + let sut: SetInitialPasswordService; + + let apiService: MockProxy; + let encryptService: MockProxy; + let i18nService: MockProxy; + let kdfConfigService: MockProxy; + let keyService: MockProxy; + let masterPasswordApiService: MockProxy; + let masterPasswordService: MockProxy; + let organizationApiService: MockProxy; + let organizationUserApiService: MockProxy; + let userDecryptionOptionsService: MockProxy; + + beforeEach(() => { + apiService = mock(); + encryptService = mock(); + i18nService = mock(); + kdfConfigService = mock(); + keyService = mock(); + masterPasswordApiService = mock(); + masterPasswordService = mock(); + organizationApiService = mock(); + organizationUserApiService = mock(); + userDecryptionOptionsService = mock(); + + sut = new DefaultSetInitialPasswordService( + apiService, + encryptService, + i18nService, + kdfConfigService, + keyService, + masterPasswordApiService, + masterPasswordService, + organizationApiService, + organizationUserApiService, + userDecryptionOptionsService, + ); + }); + + it("should instantiate", () => { + expect(sut).not.toBeFalsy(); + }); + + describe("setInitialPassword(...)", () => { + // Mock function parameters + let credentials: SetInitialPasswordCredentials; + let userType: SetInitialPasswordUserType; + let userId: UserId; + + // Mock other function data + let userKey: UserKey; + let userKeyEncString: EncString; + let masterKeyEncryptedUserKey: [UserKey, EncString]; + + let existingUserPublicKey: UserPublicKey; + let existingUserPrivateKey: UserPrivateKey; + let userKeyEncryptedPrivateKey: EncString; + + let keyPair: [string, EncString]; + let keysRequest: KeysRequest; + + let organizationKeys: OrganizationKeysResponse; + let orgPublicKeyEncryptedUserKey: EncString; + + let userDecryptionOptions: UserDecryptionOptions; + let userDecryptionOptionsSubject: BehaviorSubject; + let setPasswordRequest: SetPasswordRequest; + + let enrollmentRequest: OrganizationUserResetPasswordEnrollmentRequest; + + beforeEach(() => { + // Mock function parameters + credentials = { + newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey, + newServerMasterKeyHash: "newServerMasterKeyHash", + newLocalMasterKeyHash: "newLocalMasterKeyHash", + newPasswordHint: "newPasswordHint", + kdfConfig: DEFAULT_KDF_CONFIG, + orgSsoIdentifier: "orgSsoIdentifier", + orgId: "orgId", + resetPasswordAutoEnroll: false, + }; + userId = "userId" as UserId; + userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; + + // Mock other function data + userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; + userKeyEncString = new EncString("masterKeyEncryptedUserKey"); + masterKeyEncryptedUserKey = [userKey, userKeyEncString]; + + existingUserPublicKey = Utils.fromB64ToArray("existingUserPublicKey") as UserPublicKey; + existingUserPrivateKey = Utils.fromB64ToArray("existingUserPrivateKey") as UserPrivateKey; + userKeyEncryptedPrivateKey = new EncString("userKeyEncryptedPrivateKey"); + + keyPair = ["publicKey", new EncString("privateKey")]; + keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString); + + organizationKeys = { + privateKey: "orgPrivateKey", + publicKey: "orgPublicKey", + } as OrganizationKeysResponse; + orgPublicKeyEncryptedUserKey = new EncString("orgPublicKeyEncryptedUserKey"); + + userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true }); + userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions); + userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + + setPasswordRequest = new SetPasswordRequest( + credentials.newServerMasterKeyHash, + masterKeyEncryptedUserKey[1].encryptedString, + credentials.newPasswordHint, + credentials.orgSsoIdentifier, + keysRequest, + credentials.kdfConfig.kdfType, + credentials.kdfConfig.iterations, + ); + + enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + enrollmentRequest.masterPasswordHash = credentials.newServerMasterKeyHash; + enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString; + }); + + interface MockConfig { + userType: SetInitialPasswordUserType; + userHasUserKey: boolean; + userHasLocalKeyPair: boolean; + resetPasswordAutoEnroll: boolean; + } + + const defaultMockConfig: MockConfig = { + userType: SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER, + userHasUserKey: true, + userHasLocalKeyPair: false, + resetPasswordAutoEnroll: false, + }; + + function setupMocks(config: MockConfig = defaultMockConfig) { + // Mock makeMasterKeyEncryptedUserKey() values + if (config.userHasUserKey) { + keyService.userKey$.mockReturnValue(of(userKey)); + keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey); + } else { + keyService.userKey$.mockReturnValue(of(null)); + keyService.makeUserKey.mockResolvedValue(masterKeyEncryptedUserKey); + } + + // Mock keyPair values + if (config.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + if (config.userHasLocalKeyPair) { + keyService.userPrivateKey$.mockReturnValue(of(existingUserPrivateKey)); + keyService.userPublicKey$.mockReturnValue(of(existingUserPublicKey)); + encryptService.wrapDecapsulationKey.mockResolvedValue(userKeyEncryptedPrivateKey); + } else { + keyService.userPrivateKey$.mockReturnValue(of(null)); + keyService.userPublicKey$.mockReturnValue(of(null)); + keyService.makeKeyPair.mockResolvedValue(keyPair); + } + } + + // Mock handleResetPasswordAutoEnroll() values + if (config.resetPasswordAutoEnroll) { + organizationApiService.getKeys.mockResolvedValue(organizationKeys); + encryptService.encapsulateKeyUnsigned.mockResolvedValue(orgPublicKeyEncryptedUserKey); + keyService.userKey$.mockReturnValue(of(userKey)); + } + } + + describe("general error handling", () => { + [ + "newMasterKey", + "newServerMasterKeyHash", + "newLocalMasterKeyHash", + "newPasswordHint", + "kdfConfig", + "orgSsoIdentifier", + "orgId", + "resetPasswordAutoEnroll", + ].forEach((key) => { + it(`should throw if ${key} is not provided on the SetInitialPasswordCredentials object`, async () => { + // Arrange + const invalidCredentials: SetInitialPasswordCredentials = { + ...credentials, + [key]: null, + }; + + // Act + const promise = sut.setInitialPassword(invalidCredentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow(`${key} not found. Could not set password.`); + }); + }); + + ["userId", "userType"].forEach((param) => { + it(`should throw if ${param} was not passed in`, async () => { + // Arrange & Act + const promise = sut.setInitialPassword( + credentials, + param === "userType" ? null : userType, + param === "userId" ? null : userId, + ); + + // Assert + await expect(promise).rejects.toThrow(`${param} not found. Could not set password.`); + }); + }); + }); + + describe("given SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER", () => { + beforeEach(() => { + userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; + }); + + describe("given the user has an existing local key pair", () => { + it("should NOT create a brand new key pair for the user", async () => { + // Arrange + setPasswordRequest.keys = { + encryptedPrivateKey: userKeyEncryptedPrivateKey.encryptedString, + publicKey: Utils.fromBufferToB64(existingUserPublicKey), + }; + + setupMocks({ ...defaultMockConfig, userHasLocalKeyPair: true }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(keyService.userPrivateKey$).toHaveBeenCalledWith(userId); + expect(keyService.userPublicKey$).toHaveBeenCalledWith(userId); + expect(encryptService.wrapDecapsulationKey).toHaveBeenCalledWith( + existingUserPrivateKey, + masterKeyEncryptedUserKey[0], + ); + expect(keyService.makeKeyPair).not.toHaveBeenCalled(); + }); + }); + + describe("given the user has a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + describe("given the user does NOT have a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userHasUserKey: false }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + it("should throw if a key pair is not found", async () => { + // Arrange + keyPair = null; + + setupMocks(); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow("keyPair not found. Could not set password."); + expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled(); + }); + + it("should throw if an encrypted private key is not found", async () => { + // Arrange + keyPair[1].encryptedString = "" as EncryptedString; + + setupMocks(); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow( + "encrypted private key not found. Could not set password.", + ); + expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled(); + }); + + describe("given the initial password has been successfully set", () => { + it("should clear the ForceSetPasswordReason by setting it to None", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + + it("should update account decryption properties", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + userDecryptionOptions, + ); + expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); + expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith( + credentials.newMasterKey, + userId, + ); + expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId); + }); + + it("should set the private key to state", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(keyService.setPrivateKey).toHaveBeenCalledWith(keyPair[1].encryptedString, userId); + }); + + it("should set the local master key hash to state", async () => { + // Arrange + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( + credentials.newLocalMasterKeyHash, + userId, + ); + }); + + describe("given resetPasswordAutoEnroll is true", () => { + it(`should handle reset password (account recovery) auto enroll`, async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + + setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest); + }); + + it("should throw if organization keys are not found", async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + organizationKeys = null; + + setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true }); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow( + "Organization keys response is null. Could not handle reset password auto enroll.", + ); + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + + ["orgPublicKeyEncryptedUserKey", "orgPublicKeyEncryptedUserKey.encryptedString"].forEach( + (property) => { + it("should throw if orgPublicKeyEncryptedUserKey is not found", async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + + if (property === "orgPublicKeyEncryptedUserKey") { + orgPublicKeyEncryptedUserKey = null; + } else { + orgPublicKeyEncryptedUserKey.encryptedString = "" as EncryptedString; + } + + setupMocks({ ...defaultMockConfig, resetPasswordAutoEnroll: true }); + + // Act + const promise = sut.setInitialPassword(credentials, userType, userId); + + // Assert + await expect(promise).rejects.toThrow( + "orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.", + ); + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith( + setPasswordRequest, + ); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }, + ); + }); + + describe("given resetPasswordAutoEnroll is false", () => { + it(`should NOT handle reset password (account recovery) auto enroll`, async () => { + // Arrange + credentials.resetPasswordAutoEnroll = false; + + setupMocks(); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe("given SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP", () => { + beforeEach(() => { + userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP; + setPasswordRequest.keys = null; + }); + + it("should NOT generate a keyPair", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(keyService.userPrivateKey$).not.toHaveBeenCalled(); + expect(keyService.userPublicKey$).not.toHaveBeenCalled(); + expect(encryptService.wrapDecapsulationKey).not.toHaveBeenCalled(); + expect(keyService.makeKeyPair).not.toHaveBeenCalled(); + }); + + describe("given the user has a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + describe("given the user does NOT have a userKey", () => { + it("should successfully set an initial password", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + }); + }); + + describe("given the initial password has been successfully set", () => { + it("should clear the ForceSetPasswordReason by setting it to None", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setForceSetPasswordReason).toHaveBeenCalledWith( + ForceSetPasswordReason.None, + userId, + ); + }); + + it("should update account decryption properties", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + userDecryptionOptions, + ); + expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); + expect(masterPasswordService.setMasterKey).toHaveBeenCalledWith( + credentials.newMasterKey, + userId, + ); + expect(keyService.setUserKey).toHaveBeenCalledWith(masterKeyEncryptedUserKey[0], userId); + }); + + it("should NOT set the private key to state", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should set the local master key hash to state", async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( + credentials.newLocalMasterKeyHash, + userId, + ); + }); + + describe("given resetPasswordAutoEnroll is true", () => { + it(`should handle reset password (account recovery) auto enroll`, async () => { + // Arrange + credentials.resetPasswordAutoEnroll = true; + + setupMocks({ ...defaultMockConfig, userType, resetPasswordAutoEnroll: true }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).toHaveBeenCalledWith(credentials.orgId, userId, enrollmentRequest); + }); + }); + + describe("given resetPasswordAutoEnroll is false", () => { + it(`should NOT handle reset password (account recovery) auto enroll`, async () => { + // Arrange + setupMocks({ ...defaultMockConfig, userType }); + + // Act + await sut.setInitialPassword(credentials, userType, userId); + + // Assert + expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); + expect( + organizationUserApiService.putOrganizationUserResetPasswordEnrollment, + ).not.toHaveBeenCalled(); + }); + }); + }); + }); + }); +}); diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html new file mode 100644 index 00000000000..c83cbbe3521 --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.html @@ -0,0 +1,29 @@ +@if (initializing) { +
+ +
+} @else { + + {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} + + + +} diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts new file mode 100644 index 00000000000..fbab9eaa2c3 --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.component.ts @@ -0,0 +1,249 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { + InputPasswordComponent, + InputPasswordFlow, + PasswordInputResult, +} from "@bitwarden/auth/angular"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + AnonLayoutWrapperDataService, + CalloutComponent, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { + SetInitialPasswordCredentials, + SetInitialPasswordService, + SetInitialPasswordUserType, +} from "./set-initial-password.service.abstraction"; + +@Component({ + standalone: true, + templateUrl: "set-initial-password.component.html", + imports: [CalloutComponent, CommonModule, InputPasswordComponent, I18nPipe], +}) +export class SetInitialPasswordComponent implements OnInit { + protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser; + + protected email?: string; + protected forceSetPasswordReason?: ForceSetPasswordReason; + protected initializing = true; + protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null; + protected orgId?: string; + protected orgSsoIdentifier?: string; + protected resetPasswordAutoEnroll?: boolean; + protected submitting = false; + protected userId?: UserId; + protected userType?: SetInitialPasswordUserType; + + constructor( + private accountService: AccountService, + private activatedRoute: ActivatedRoute, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private dialogService: DialogService, + private i18nService: I18nService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private messagingService: MessagingService, + private organizationApiService: OrganizationApiServiceAbstraction, + private policyApiService: PolicyApiServiceAbstraction, + private router: Router, + private setInitialPasswordService: SetInitialPasswordService, + private ssoLoginService: SsoLoginServiceAbstraction, + private syncService: SyncService, + private toastService: ToastService, + private validationService: ValidationService, + ) {} + + async ngOnInit() { + await this.syncService.fullSync(true); + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + this.userId = activeAccount?.id; + this.email = activeAccount?.email; + + await this.determineUserType(); + await this.handleQueryParams(); + + this.initializing = false; + } + + private async determineUserType() { + if (!this.userId) { + throw new Error("userId not found. Could not determine user type."); + } + + this.forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(this.userId), + ); + + if ( + this.forceSetPasswordReason === + ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission + ) { + this.userType = SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP; + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "setMasterPassword" }, + pageSubtitle: { key: "orgPermissionsUpdatedMustSetPassword" }, + }); + } else { + this.userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER; + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "joinOrganization" }, + pageSubtitle: { key: "finishJoiningThisOrganizationBySettingAMasterPassword" }, + }); + } + } + + private async handleQueryParams() { + if (!this.userId) { + throw new Error("userId not found. Could not handle query params."); + } + + const qParams = await firstValueFrom(this.activatedRoute.queryParams); + + this.orgSsoIdentifier = + qParams.identifier ?? + (await this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.userId)); + + if (this.orgSsoIdentifier != null) { + try { + const autoEnrollStatus = await this.organizationApiService.getAutoEnrollStatus( + this.orgSsoIdentifier, + ); + this.orgId = autoEnrollStatus.id; + this.resetPasswordAutoEnroll = autoEnrollStatus.resetPasswordEnabled; + this.masterPasswordPolicyOptions = + await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(this.orgId); + } catch { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("errorOccurred"), + }); + } + } + } + + protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) { + this.submitting = true; + + if (!passwordInputResult.newMasterKey) { + throw new Error("newMasterKey not found. Could not set initial password."); + } + if (!passwordInputResult.newServerMasterKeyHash) { + throw new Error("newServerMasterKeyHash not found. Could not set initial password."); + } + if (!passwordInputResult.newLocalMasterKeyHash) { + throw new Error("newLocalMasterKeyHash not found. Could not set initial password."); + } + // newPasswordHint can have an empty string as a valid value, so we specifically check for null or undefined + if (passwordInputResult.newPasswordHint == null) { + throw new Error("newPasswordHint not found. Could not set initial password."); + } + if (!passwordInputResult.kdfConfig) { + throw new Error("kdfConfig not found. Could not set initial password."); + } + if (!this.userId) { + throw new Error("userId not found. Could not set initial password."); + } + if (!this.userType) { + throw new Error("userType not found. Could not set initial password."); + } + if (!this.orgSsoIdentifier) { + throw new Error("orgSsoIdentifier not found. Could not set initial password."); + } + if (!this.orgId) { + throw new Error("orgId not found. Could not set initial password."); + } + // resetPasswordAutoEnroll can have `false` as a valid value, so we specifically check for null or undefined + if (this.resetPasswordAutoEnroll == null) { + throw new Error("resetPasswordAutoEnroll not found. Could not set initial password."); + } + + try { + const credentials: SetInitialPasswordCredentials = { + newMasterKey: passwordInputResult.newMasterKey, + newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash, + newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash, + newPasswordHint: passwordInputResult.newPasswordHint, + kdfConfig: passwordInputResult.kdfConfig, + orgSsoIdentifier: this.orgSsoIdentifier, + orgId: this.orgId, + resetPasswordAutoEnroll: this.resetPasswordAutoEnroll, + }; + + await this.setInitialPasswordService.setInitialPassword( + credentials, + this.userType, + this.userId, + ); + + this.showSuccessToastByUserType(); + + this.submitting = false; + await this.router.navigate(["vault"]); + } catch (e) { + this.validationService.showError(e); + this.submitting = false; + } + } + + private showSuccessToastByUserType() { + if (this.userType === SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("accountSuccessfullyCreated"), + }); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("inviteAccepted"), + }); + } + + if ( + this.userType === + SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP + ) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("masterPasswordSuccessfullySet"), + }); + } + } + + protected async logout() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + if (confirmed) { + this.messagingService.send("logout"); + } + } +} diff --git a/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts new file mode 100644 index 00000000000..e594053a906 --- /dev/null +++ b/libs/angular/src/auth/password-management/set-initial-password/set-initial-password.service.abstraction.ts @@ -0,0 +1,64 @@ +import { UserId } from "@bitwarden/common/types/guid"; +import { MasterKey } from "@bitwarden/common/types/key"; +import { KdfConfig } from "@bitwarden/key-management"; + +export const _SetInitialPasswordUserType = { + /** + * A user being "just-in-time" (JIT) provisioned into a master-password-encryption org + */ + JIT_PROVISIONED_MP_ORG_USER: "jit_provisioned_mp_org_user", + + /** + * Could be one of two scenarios: + * 1. A user being "just-in-time" (JIT) provisioned into a trusted-device-encryption org + * with the reset password permission granted ("manage account recovery"), which requires + * that the user sets a master password + * 2. An user in a trusted-device-encryption org whose permissions were upgraded to include + * the reset password permission ("manage account recovery"), which requires that the user + * sets a master password + */ + TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP: + "tde_org_user_reset_password_permission_requires_mp", +} as const; + +type _SetInitialPasswordUserType = typeof _SetInitialPasswordUserType; + +export type SetInitialPasswordUserType = + _SetInitialPasswordUserType[keyof _SetInitialPasswordUserType]; +export const SetInitialPasswordUserType: Readonly<{ + [K in keyof typeof _SetInitialPasswordUserType]: SetInitialPasswordUserType; +}> = Object.freeze(_SetInitialPasswordUserType); + +export interface SetInitialPasswordCredentials { + newMasterKey: MasterKey; + newServerMasterKeyHash: string; + newLocalMasterKeyHash: string; + newPasswordHint: string; + kdfConfig: KdfConfig; + orgSsoIdentifier: string; + orgId: string; + resetPasswordAutoEnroll: boolean; +} + +/** + * Handles setting an initial password for an existing authed user. + * + * To see the different scenarios where an existing authed user needs to set an + * initial password, see {@link SetInitialPasswordUserType} + */ +export abstract class SetInitialPasswordService { + /** + * Sets an initial password for an existing authed user who is either: + * - {@link SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER} + * - {@link SetInitialPasswordUserType.TDE_ORG_USER_RESET_PASSWORD_PERMISSION_REQUIRES_MP} + * + * @param credentials An object of the credentials needed to set the initial password + * @throws If any property on the `credentials` object is null or undefined, or if a + * masterKeyEncryptedUserKey or newKeyPair could not be created. + */ + abstract setInitialPassword: ( + credentials: SetInitialPasswordCredentials, + userType: SetInitialPasswordUserType, + userId: UserId, + ) => Promise; +} diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 559761bd1bf..96a95de501e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -35,7 +35,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { - AuthRequestApiService, + AuthRequestApiServiceAbstraction, AuthRequestService, AuthRequestServiceAbstraction, DefaultAuthRequestApiService, @@ -59,7 +59,6 @@ import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstracti import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; -import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { InternalOrganizationServiceAbstraction, @@ -257,7 +256,6 @@ import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordStrengthService, PasswordStrengthServiceAbstraction, @@ -279,6 +277,7 @@ import { FolderService as FolderServiceAbstraction, InternalFolderService, } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/vault/abstractions/search.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { @@ -295,6 +294,7 @@ import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-u import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { SearchService } from "@bitwarden/common/vault/services/search.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; @@ -339,6 +339,8 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation"; +import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction"; import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction"; import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; @@ -1181,6 +1183,11 @@ const safeProviders: SafeProvider[] = [ useClass: DevicesServiceImplementation, deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction], }), + safeProvider({ + provide: AuthRequestApiServiceAbstraction, + useClass: DefaultAuthRequestApiService, + deps: [ApiServiceAbstraction, LogService], + }), safeProvider({ provide: DeviceTrustServiceAbstraction, useClass: DeviceTrustService, @@ -1205,12 +1212,12 @@ const safeProviders: SafeProvider[] = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, - AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, KeyService, EncryptService, ApiServiceAbstraction, StateProvider, + AuthRequestApiServiceAbstraction, ], }), safeProvider({ @@ -1414,6 +1421,22 @@ const safeProviders: SafeProvider[] = [ InternalUserDecryptionOptionsServiceAbstraction, ], }), + safeProvider({ + provide: SetInitialPasswordService, + useClass: DefaultSetInitialPasswordService, + deps: [ + ApiServiceAbstraction, + EncryptService, + I18nServiceAbstraction, + KdfConfigService, + KeyService, + MasterPasswordApiServiceAbstraction, + InternalMasterPasswordServiceAbstraction, + OrganizationApiServiceAbstraction, + OrganizationUserApiService, + InternalUserDecryptionOptionsServiceAbstraction, + ], + }), safeProvider({ provide: DefaultServerSettingsService, useClass: DefaultServerSettingsService, @@ -1477,11 +1500,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultCipherAuthorizationService, deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction], }), - safeProvider({ - provide: AuthRequestApiService, - useClass: DefaultAuthRequestApiService, - deps: [ApiServiceAbstraction, LogService], - }), safeProvider({ provide: LoginApprovalComponentServiceAbstraction, useClass: DefaultLoginApprovalComponentService, diff --git a/libs/angular/src/tools/send/send.component.ts b/libs/angular/src/tools/send/send.component.ts index 5dbf3686b7d..e96bdd8e31a 100644 --- a/libs/angular/src/tools/send/send.component.ts +++ b/libs/angular/src/tools/send/send.component.ts @@ -12,7 +12,6 @@ import { combineLatest, } from "rxjs"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -25,6 +24,7 @@ import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { DialogService, ToastService } from "@bitwarden/components"; @Directive() diff --git a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts index 21528b1ddd5..9a1a31b6068 100644 --- a/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts +++ b/libs/angular/src/vault/abstractions/deprecated-vault-filter.service.ts @@ -6,6 +6,7 @@ import { Observable } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { UserId } from "@bitwarden/common/types/guid"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { DynamicTreeNode } from "../vault-filter/models/dynamic-tree-node.model"; @@ -14,11 +15,14 @@ import { DynamicTreeNode } from "../vault-filter/models/dynamic-tree-node.model" * @deprecated August 30 2022: Use new VaultFilterService with observables */ export abstract class DeprecatedVaultFilterService { - buildOrganizations: () => Promise; - buildNestedFolders: (organizationId?: string) => Observable>; - buildCollections: (organizationId?: string) => Promise>; - buildCollapsedFilterNodes: () => Promise>; - storeCollapsedFilterNodes: (collapsedFilterNodes: Set) => Promise; - checkForSingleOrganizationPolicy: () => Promise; - checkForOrganizationDataOwnershipPolicy: () => Promise; + abstract buildOrganizations(): Promise; + abstract buildNestedFolders(organizationId?: string): Observable>; + abstract buildCollections(organizationId?: string): Promise>; + abstract buildCollapsedFilterNodes(userId: UserId): Promise>; + abstract storeCollapsedFilterNodes( + collapsedFilterNodes: Set, + userId: UserId, + ): Promise; + abstract checkForSingleOrganizationPolicy(): Promise; + abstract checkForOrganizationDataOwnershipPolicy(): Promise; } diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 424243fe118..cf017899774 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -15,11 +15,11 @@ import { takeUntil, } from "rxjs"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; diff --git a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts index 936d606b936..0b0cb14bbb8 100644 --- a/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts +++ b/libs/angular/src/vault/vault-filter/components/vault-filter.component.ts @@ -7,6 +7,9 @@ import { firstValueFrom, Observable } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { CollectionView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -29,6 +32,8 @@ export class VaultFilterComponent implements OnInit { @Output() onAddFolder = new EventEmitter(); @Output() onEditFolder = new EventEmitter(); + private activeUserId: UserId; + isLoaded = false; collapsedFilterNodes: Set; organizations: Organization[]; @@ -37,14 +42,20 @@ export class VaultFilterComponent implements OnInit { collections: DynamicTreeNode; folders$: Observable>; - constructor(protected vaultFilterService: DeprecatedVaultFilterService) {} + constructor( + protected vaultFilterService: DeprecatedVaultFilterService, + protected accountService: AccountService, + ) {} get displayCollections() { return this.collections?.fullList != null && this.collections.fullList.length > 0; } async ngOnInit(): Promise { - this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes(); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes( + this.activeUserId, + ); this.organizations = await this.vaultFilterService.buildOrganizations(); if (this.organizations != null && this.organizations.length > 0) { this.activeOrganizationDataOwnershipPolicy = @@ -68,7 +79,10 @@ export class VaultFilterComponent implements OnInit { } else { this.collapsedFilterNodes.add(node.id); } - await this.vaultFilterService.storeCollapsedFilterNodes(this.collapsedFilterNodes); + await this.vaultFilterService.storeCollapsedFilterNodes( + this.collapsedFilterNodes, + this.activeUserId, + ); } async applyFilter(filter: VaultFilter) { diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index a4114e63285..3317f0c9002 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -12,7 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; +import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -28,10 +28,9 @@ const NestingDelimiter = "/"; @Injectable() export class VaultFilterService implements DeprecatedVaultFilterServiceAbstraction { - private collapsedGroupingsState: ActiveUserState = - this.stateProvider.getActive(COLLAPSED_GROUPINGS); - private readonly collapsedGroupings$: Observable> = - this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c))); + private collapsedGroupingsState(userId: UserId): SingleUserState { + return this.stateProvider.getUser(userId, COLLAPSED_GROUPINGS); + } constructor( protected organizationService: OrganizationService, @@ -43,12 +42,17 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti protected accountService: AccountService, ) {} - async storeCollapsedFilterNodes(collapsedFilterNodes: Set): Promise { - await this.collapsedGroupingsState.update(() => Array.from(collapsedFilterNodes)); + async storeCollapsedFilterNodes( + collapsedFilterNodes: Set, + userId: UserId, + ): Promise { + await this.collapsedGroupingsState(userId).update(() => Array.from(collapsedFilterNodes)); } - async buildCollapsedFilterNodes(): Promise> { - return await firstValueFrom(this.collapsedGroupings$); + async buildCollapsedFilterNodes(userId: UserId): Promise> { + return await firstValueFrom( + this.collapsedGroupingsState(userId).state$.pipe(map((c) => new Set(c))), + ); } async buildOrganizations(): Promise { diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 79157cae901..2d469e89fcd 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -261,7 +261,7 @@ export class InputPasswordComponent implements OnInit { } } - submit = async () => { + submit = async (): Promise => { try { this.isSubmitting.emit(true); @@ -280,8 +280,7 @@ export class InputPasswordComponent implements OnInit { const checkForBreaches = this.formGroup.controls.checkForBreaches?.value ?? true; if (this.flow === InputPasswordFlow.ChangePasswordDelegation) { - await this.handleChangePasswordDelegationFlow(newPassword); - return; + return await this.handleChangePasswordDelegationFlow(newPassword); } if (!this.email) { @@ -388,6 +387,7 @@ export class InputPasswordComponent implements OnInit { // 5. Emit cryptographic keys and other password related properties this.onPasswordFormSubmit.emit(passwordInputResult); + return passwordInputResult; } catch (e) { this.validationService.showError(e); } finally { @@ -441,7 +441,9 @@ export class InputPasswordComponent implements OnInit { } } - private async handleChangePasswordDelegationFlow(newPassword: string) { + private async handleChangePasswordDelegationFlow( + newPassword: string, + ): Promise { const newPasswordVerified = await this.verifyNewPassword( newPassword, this.passwordStrengthScore, @@ -456,6 +458,7 @@ export class InputPasswordComponent implements OnInit { }; this.onPasswordFormSubmit.emit(passwordInputResult); + return passwordInputResult; } /** diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index 9912c45e9d2..5e410c538f0 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -39,7 +39,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service"; +import { AuthRequestApiServiceAbstraction } from "../../common/abstractions/auth-request-api.service"; import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service"; // FIXME: update to use a const object instead of a typescript enum @@ -85,7 +85,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private accountService: AccountService, private anonymousHubService: AnonymousHubService, private appIdService: AppIdService, - private authRequestApiService: AuthRequestApiService, + private authRequestApiService: AuthRequestApiServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private authService: AuthService, private cryptoFunctionService: CryptoFunctionService, diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index b78ca098dea..07b59ac661f 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -23,10 +23,12 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -116,6 +118,7 @@ export class SsoComponent implements OnInit { private toastService: ToastService, private ssoComponentService: SsoComponentService, private loginSuccessHandlerService: LoginSuccessHandlerService, + private configService: ConfigService, ) { environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html"; @@ -531,7 +534,12 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - await this.router.navigate(["set-password-jit"], { + const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag( + FeatureFlag.PM16117_SetInitialPasswordRefactor, + ); + const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password-jit"; + + await this.router.navigate([route], { queryParams: { identifier: orgIdentifier, }, diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index 00cad105f95..4ab3841e48e 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -27,6 +27,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -75,6 +76,7 @@ describe("TwoFactorAuthComponent", () => { let mockLoginSuccessHandlerService: MockProxy; let mockTwoFactorAuthCompCacheService: MockProxy; let mockAuthService: MockProxy; + let mockConfigService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -110,6 +112,7 @@ describe("TwoFactorAuthComponent", () => { mockToastService = mock(); mockTwoFactorAuthCompService = mock(); mockAuthService = mock(); + mockConfigService = mock(); mockEnvService = mock(); mockLoginSuccessHandlerService = mock(); @@ -209,6 +212,7 @@ describe("TwoFactorAuthComponent", () => { useValue: mockTwoFactorAuthCompCacheService, }, { provide: AuthService, useValue: mockAuthService }, + { provide: ConfigService, useValue: mockConfigService }, ], }); @@ -225,22 +229,6 @@ describe("TwoFactorAuthComponent", () => { expect(component).toBeTruthy(); }); - // Shared tests - const testChangePasswordOnSuccessfulLogin = () => { - it("navigates to the component's defined change password route when user doesn't have a MP and key connector isn't enabled", async () => { - // Act - await component.submit("testToken"); - - // Assert - expect(mockRouter.navigate).toHaveBeenCalledTimes(1); - expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], { - queryParams: { - identifier: component.orgSsoIdentifier, - }, - }); - }); - }; - describe("Standard 2FA scenarios", () => { describe("submit", () => { const token = "testToken"; @@ -280,20 +268,76 @@ describe("TwoFactorAuthComponent", () => { selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword); }); - testChangePasswordOnSuccessfulLogin(); + describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { + it("navigates to the /set-initial-password route when user doesn't have a MP and key connector isn't enabled", async () => { + // Arrange + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + // Act + await component.submit("testToken"); + + // Assert + expect(mockRouter.navigate).toHaveBeenCalledTimes(1); + expect(mockRouter.navigate).toHaveBeenCalledWith(["set-initial-password"], { + queryParams: { + identifier: component.orgSsoIdentifier, + }, + }); + }); + }); + + describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => { + it("navigates to the /set-password route when user doesn't have a MP and key connector isn't enabled", async () => { + // Arrange + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + // Act + await component.submit("testToken"); + + // Assert + expect(mockRouter.navigate).toHaveBeenCalledTimes(1); + expect(mockRouter.navigate).toHaveBeenCalledWith(["set-password"], { + queryParams: { + identifier: component.orgSsoIdentifier, + }, + }); + }); + }); }); - it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => { - selectedUserDecryptionOptions.next( - mockUserDecryptionOpts.noMasterPasswordWithKeyConnector, - ); + describe("Given the PM16117_SetInitialPasswordRefactor feature flag is ON", () => { + it("does not navigate to the /set-initial-password route when the user has key connector even if user has no master password", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); - await component.submit(token, remember); + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.noMasterPasswordWithKeyConnector, + ); - expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], { - queryParams: { - identifier: component.orgSsoIdentifier, - }, + await component.submit(token, remember); + + expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-initial-password"], { + queryParams: { + identifier: component.orgSsoIdentifier, + }, + }); + }); + }); + + describe("Given the PM16117_SetInitialPasswordRefactor feature flag is OFF", () => { + it("does not navigate to the /set-password route when the user has key connector even if user has no master password", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.noMasterPasswordWithKeyConnector, + ); + + await component.submit(token, remember); + + expect(mockRouter.navigate).not.toHaveBeenCalledWith(["set-password"], { + queryParams: { + identifier: component.orgSsoIdentifier, + }, + }); }); }); }); diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index b811d48a48f..43a63498634 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -32,7 +32,9 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -169,6 +171,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private loginSuccessHandlerService: LoginSuccessHandlerService, private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService, private authService: AuthService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -559,7 +562,12 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { } private async handleChangePasswordRequired(orgIdentifier: string | undefined) { - await this.router.navigate(["set-password"], { + const isSetInitialPasswordRefactorFlagOn = await this.configService.getFeatureFlag( + FeatureFlag.PM16117_SetInitialPasswordRefactor, + ); + const route = isSetInitialPasswordRefactorFlagOn ? "set-initial-password" : "set-password"; + + await this.router.navigate([route], { queryParams: { identifier: orgIdentifier, }, diff --git a/libs/auth/src/common/abstractions/auth-request-api.service.ts b/libs/auth/src/common/abstractions/auth-request-api.service.ts index 1b0befc0df4..6a6358fa2c2 100644 --- a/libs/auth/src/common/abstractions/auth-request-api.service.ts +++ b/libs/auth/src/common/abstractions/auth-request-api.service.ts @@ -1,7 +1,16 @@ import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; + +export abstract class AuthRequestApiServiceAbstraction { + /** + * Gets a list of pending auth requests based on the user. There will only be one AuthRequest per device and the + * AuthRequest will be the most recent pending request. + * + * @returns A promise that resolves to a list response containing auth request responses. + */ + abstract getPendingAuthRequests(): Promise>; -export abstract class AuthRequestApiService { /** * Gets an auth request by its ID. * diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index 75bb8686163..956fd771039 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -41,6 +41,12 @@ export abstract class AuthRequestServiceAbstraction { * @throws If `userId` is not provided. */ abstract clearAdminAuthRequest: (userId: UserId) => Promise; + /** + * Gets a list of standard pending auth requests for the user. + * @returns An observable of an array of auth request. + * The array will be empty if there are no pending auth requests. + */ + abstract getPendingAuthRequests$(): Observable>; /** * Approve or deny an auth request. * @param approve True to approve, false to deny. diff --git a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts index c9fec1400c9..15517a9a0e5 100644 --- a/libs/auth/src/common/services/auth-request/auth-request-api.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request-api.service.ts @@ -1,16 +1,23 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AuthRequestApiService } from "../../abstractions/auth-request-api.service"; +import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service"; -export class DefaultAuthRequestApiService implements AuthRequestApiService { +export class DefaultAuthRequestApiService implements AuthRequestApiServiceAbstraction { constructor( private apiService: ApiService, private logService: LogService, ) {} + async getPendingAuthRequests(): Promise> { + const path = `/auth-requests/pending`; + const r = await this.apiService.send("GET", path, null, true, true); + return new ListResponse(r, AuthRequestResponse); + } + async getAuthRequest(requestId: string): Promise { try { const path = `/auth-requests/${requestId}`; diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index c3d6f78f3c2..ab09e17f11f 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -10,23 +10,23 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateProvider } from "@bitwarden/common/platform/state"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { DefaultAuthRequestApiService } from "./auth-request-api.service"; import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; const stateProvider = mock(); - let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; const appIdService = mock(); const keyService = mock(); const encryptService = mock(); const apiService = mock(); + const authRequestApiService = mock(); let mockPrivateKey: Uint8Array; let mockPublicKey: Uint8Array; @@ -34,17 +34,16 @@ describe("AuthRequestService", () => { beforeEach(() => { jest.clearAllMocks(); - accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); sut = new AuthRequestService( appIdService, - accountService, masterPasswordService, keyService, encryptService, apiService, stateProvider, + authRequestApiService, ); mockPrivateKey = new Uint8Array(64); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index fca68b76bbb..93a6ba12ffb 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -1,15 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable, Subject, firstValueFrom } from "rxjs"; +import { Observable, Subject, defer, firstValueFrom, map } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -24,6 +24,7 @@ import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { AuthRequestApiServiceAbstraction } from "../../abstractions/auth-request-api.service"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; /** @@ -49,12 +50,12 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { constructor( private appIdService: AppIdService, - private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, private keyService: KeyService, private encryptService: EncryptService, private apiService: ApiService, private stateProvider: StateProvider, + private authRequestApiService: AuthRequestApiServiceAbstraction, ) { this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); this.adminLoginApproved$ = this.adminLoginApprovedSubject.asObservable(); @@ -91,6 +92,19 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId); } + /** + * @description Gets the list of all standard (not admin approval) pending AuthRequests. + */ + getPendingAuthRequests$(): Observable> { + return defer(() => this.authRequestApiService.getPendingAuthRequests()).pipe( + map((authRequestResponses: ListResponse) => { + return authRequestResponses.data.map((authRequestResponse: AuthRequestResponse) => { + return new AuthRequestResponse(authRequestResponse); + }); + }), + ); + } + async approveOrDenyAuthRequest( approve: boolean, authRequest: AuthRequestResponse, diff --git a/libs/common/spec/fake-storage.service.ts b/libs/common/spec/fake-storage.service.ts index c6d989c5abf..1eae3dbfbe3 100644 --- a/libs/common/spec/fake-storage.service.ts +++ b/libs/common/spec/fake-storage.service.ts @@ -1,119 +1 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { Subject } from "rxjs"; - -import { - AbstractStorageService, - ObservableStorageService, - StorageUpdate, -} from "../src/platform/abstractions/storage.service"; -import { StorageOptions } from "../src/platform/models/domain/storage-options"; - -const INTERNAL_KEY = "__internal__"; - -export class FakeStorageService implements AbstractStorageService, ObservableStorageService { - private store: Record; - private updatesSubject = new Subject(); - private _valuesRequireDeserialization = false; - - /** - * Returns a mock of a {@see AbstractStorageService} for asserting the expected - * amount of calls. It is not recommended to use this to mock implementations as - * they are not respected. - */ - mock: MockProxy; - - constructor(initial?: Record) { - this.store = initial ?? {}; - this.mock = mock(); - } - - /** - * Updates the internal store for this fake implementation, this bypasses any mock calls - * or updates to the {@link updates$} observable. - * @param store - */ - internalUpdateStore(store: Record) { - this.store = store; - } - - get internalStore() { - return this.store; - } - - internalUpdateValuesRequireDeserialization(value: boolean) { - this._valuesRequireDeserialization = value; - } - - get valuesRequireDeserialization(): boolean { - return this._valuesRequireDeserialization; - } - - get updates$() { - return this.updatesSubject.asObservable(); - } - - get(key: string, options?: StorageOptions): Promise { - // 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.mock.get(key, options); - const value = this.store[key] as T; - return Promise.resolve(value); - } - has(key: string, options?: StorageOptions): Promise { - // 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.mock.has(key, options); - return Promise.resolve(this.store[key] != null); - } - async save(key: string, obj: T, options?: StorageOptions): Promise { - // These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203 - // which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world. - if (typeof key !== "string" && typeof key !== "object") { - throw new TypeError( - `Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`, - ); - } - - // We don't throw this error because ElectronStorageService automatically detects this case - // and calls `delete()` instead of `set()`. - // if (typeof key !== "object" && obj === undefined) { - // throw new TypeError("Use `delete()` to clear values"); - // } - - if (this._containsReservedKey(key)) { - throw new TypeError( - `Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`, - ); - } - - // 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.mock.save(key, obj, options); - this.store[key] = obj; - this.updatesSubject.next({ key: key, updateType: "save" }); - } - remove(key: string, options?: StorageOptions): Promise { - // 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.mock.remove(key, options); - delete this.store[key]; - this.updatesSubject.next({ key: key, updateType: "remove" }); - return Promise.resolve(); - } - - private _containsReservedKey(key: string | Partial): boolean { - if (typeof key === "object") { - const firsKey = Object.keys(key)[0]; - - if (firsKey === INTERNAL_KEY) { - return true; - } - } - - if (typeof key !== "string") { - return false; - } - - return false; - } -} +export { FakeStorageService } from "@bitwarden/storage-test-utils"; diff --git a/libs/common/src/auth/models/response/auth-request.response.ts b/libs/common/src/auth/models/response/auth-request.response.ts index 372ae047f4d..94c65000919 100644 --- a/libs/common/src/auth/models/response/auth-request.response.ts +++ b/libs/common/src/auth/models/response/auth-request.response.ts @@ -18,6 +18,7 @@ export class AuthRequestResponse extends BaseResponse { responseDate?: string; isAnswered: boolean; isExpired: boolean; + deviceId?: string; // could be null or empty constructor(response: any) { super(response); @@ -33,6 +34,7 @@ export class AuthRequestResponse extends BaseResponse { this.creationDate = this.getResponseProperty("CreationDate"); this.requestApproved = this.getResponseProperty("RequestApproved"); this.responseDate = this.getResponseProperty("ResponseDate"); + this.deviceId = this.getResponseProperty("RequestDeviceId"); const requestDate = new Date(this.creationDate); const requestDateUTC = Date.UTC( diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index df2ee88877d..55c96c2334c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -12,13 +12,13 @@ import { ServerConfig } from "../platform/abstractions/config/server-config"; export enum FeatureFlag { /* Admin Console Team */ SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions", - OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript", CreateDefaultLocation = "pm-19467-create-default-location", /* Auth */ PM16117_SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor", PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor", PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence", + PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals", /* Autofill */ BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", @@ -45,7 +45,6 @@ export enum FeatureFlag { EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation", /* Tools */ - ItemShare = "item-share", DesktopSendUIRefresh = "desktop-send-ui-refresh", /* Vault */ @@ -77,7 +76,6 @@ const FALSE = false as boolean; export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.SeparateCustomRolePermissions]: FALSE, - [FeatureFlag.OptimizeNestedTraverseTypescript]: FALSE, [FeatureFlag.CreateDefaultLocation]: FALSE, /* Autofill */ @@ -91,7 +89,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableRiskInsightsNotifications]: FALSE, /* Tools */ - [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.DesktopSendUIRefresh]: FALSE, /* Vault */ @@ -107,6 +104,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM16117_SetInitialPasswordRefactor]: FALSE, [FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE, [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE, + [FeatureFlag.PM14938_BrowserExtensionLoginApproval]: FALSE, /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts index 5ce7e37778d..9963e7d24f8 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.spec.ts @@ -14,7 +14,6 @@ import { LogoutReason } from "@bitwarden/auth/common"; import { BiometricsService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; -import { SearchService } from "../../../abstractions/search.service"; import { AccountInfo } from "../../../auth/abstractions/account.service"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; @@ -28,6 +27,7 @@ import { StateEventRunnerService } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; +import { SearchService } from "../../../vault/abstractions/search.service"; import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service"; import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum"; import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type"; diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts index 04769567db2..b5ee6a1fc0f 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout.service.ts @@ -12,7 +12,6 @@ import { LogoutReason } from "@bitwarden/auth/common"; // eslint-disable-next-line no-restricted-imports import { BiometricsService } from "@bitwarden/key-management"; -import { SearchService } from "../../../abstractions/search.service"; import { AccountService } from "../../../auth/abstractions/account.service"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; @@ -25,6 +24,7 @@ import { StateEventRunnerService } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; +import { SearchService } from "../../../vault/abstractions/search.service"; import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction"; import { VaultTimeoutSettingsService } from "../abstractions/vault-timeout-settings.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vault-timeout.service"; diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 7d0ef9e9c34..fa050375391 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -124,9 +124,9 @@ export class CipherExport { domain.passwordHistory = req.passwordHistory.map((ph) => PasswordHistoryExport.toDomain(ph)); } - domain.creationDate = req.creationDate; - domain.revisionDate = req.revisionDate; - domain.deletedDate = req.deletedDate; + domain.creationDate = req.creationDate ? new Date(req.creationDate) : null; + domain.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null; + domain.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null; return domain; } diff --git a/libs/common/src/platform/ipc/ipc.service.ts b/libs/common/src/platform/ipc/ipc.service.ts index 134e615fc8b..2fba4380706 100644 --- a/libs/common/src/platform/ipc/ipc.service.ts +++ b/libs/common/src/platform/ipc/ipc.service.ts @@ -4,7 +4,7 @@ import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-inte export abstract class IpcService { private _client?: IpcClient; - protected get client(): IpcClient { + get client(): IpcClient { if (!this._client) { throw new Error("IpcService not initialized"); } diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.spec.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.spec.ts new file mode 100644 index 00000000000..6d9457389ca --- /dev/null +++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.spec.ts @@ -0,0 +1,387 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { + awaitAsync, + FakeGlobalState, + FakeStateProvider, + mockAccountServiceWith, +} from "../../../../spec"; +import { PushTechnology } from "../../../enums/push-technology.enum"; +import { UserId } from "../../../types/guid"; +import { ConfigService } from "../../abstractions/config/config.service"; +import { ServerConfig } from "../../abstractions/config/server-config"; +import { Supported } from "../../misc/support-status"; +import { Utils } from "../../misc/utils"; +import { ServerConfigData } from "../../models/data/server-config.data"; +import { PushSettingsConfigResponse } from "../../models/response/server-config.response"; +import { KeyDefinition } from "../../state"; + +import { WebPushNotificationsApiService } from "./web-push-notifications-api.service"; +import { WebPushConnector } from "./webpush-connection.service"; +import { + WEB_PUSH_SUBSCRIPTION_USERS, + WorkerWebPushConnectionService, +} from "./worker-webpush-connection.service"; + +const mockUser1 = "testUser1" as UserId; + +const createSub = (key: string) => { + return { + options: { applicationServerKey: Utils.fromUrlB64ToArray(key), userVisibleOnly: true }, + endpoint: `web.push.endpoint/?${Utils.newGuid()}`, + expirationTime: 5, + getKey: () => null, + toJSON: () => ({ endpoint: "something", keys: {}, expirationTime: 5 }), + unsubscribe: () => Promise.resolve(true), + } satisfies PushSubscription; +}; + +describe("WorkerWebpushConnectionService", () => { + let configService: MockProxy; + let webPushApiService: MockProxy; + let stateProvider: FakeStateProvider; + let pushManager: MockProxy; + const userId = "testUser1" as UserId; + + let sut: WorkerWebPushConnectionService; + + beforeEach(() => { + configService = mock(); + webPushApiService = mock(); + stateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); + pushManager = mock(); + + sut = new WorkerWebPushConnectionService( + configService, + webPushApiService, + mock({ pushManager: pushManager }), + stateProvider, + ); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + type ExtractKeyDefinitionType = T extends KeyDefinition ? U : never; + describe("supportStatus$", () => { + let fakeGlobalState: FakeGlobalState< + ExtractKeyDefinitionType + >; + + beforeEach(() => { + fakeGlobalState = stateProvider.getGlobal(WEB_PUSH_SUBSCRIPTION_USERS) as FakeGlobalState< + ExtractKeyDefinitionType + >; + }); + + test("when web push is supported, have an existing subscription, and we've already registered the user, should not call API", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + const existingSubscription = createSub("dGVzdA"); + await fakeGlobalState.nextState({ [existingSubscription.endpoint]: [userId] }); + + pushManager.getSubscription.mockResolvedValue(existingSubscription); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(0); + + expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(0); + + notificationsSub.unsubscribe(); + }); + + test("when web push is supported, have an existing subscription, and we haven't registered the user, should call API", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + const existingSubscription = createSub("dGVzdA"); + await fakeGlobalState.nextState({ + [existingSubscription.endpoint]: ["otherUserId" as UserId], + }); + + pushManager.getSubscription.mockResolvedValue(existingSubscription); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1); + + expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1); + expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({ + [existingSubscription.endpoint]: ["otherUserId", mockUser1], + }); + + notificationsSub.unsubscribe(); + }); + + test("when web push is supported, have an existing subscription, but it isn't in state, should call API and add to state", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + const existingSubscription = createSub("dGVzdA"); + await fakeGlobalState.nextState({ + [existingSubscription.endpoint]: null!, + }); + + pushManager.getSubscription.mockResolvedValue(existingSubscription); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1); + + expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1); + expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({ + [existingSubscription.endpoint]: [mockUser1], + }); + + notificationsSub.unsubscribe(); + }); + + test("when web push is supported, have an existing subscription, but state array is null, should call API and add to state", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + const existingSubscription = createSub("dGVzdA"); + await fakeGlobalState.nextState({}); + + pushManager.getSubscription.mockResolvedValue(existingSubscription); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1); + + expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1); + expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({ + [existingSubscription.endpoint]: [mockUser1], + }); + + notificationsSub.unsubscribe(); + }); + + test("when web push is supported, but we don't have an existing subscription, should call the api and wipe out existing state", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + const existingState = createSub("dGVzdA"); + await fakeGlobalState.nextState({ [existingState.endpoint]: [userId] }); + + pushManager.getSubscription.mockResolvedValue(null); + const newSubscription = createSub("dGVzdA"); + pushManager.subscribe.mockResolvedValue(newSubscription); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1); + + expect(fakeGlobalState.nextMock).toHaveBeenCalledTimes(1); + expect(fakeGlobalState.nextMock).toHaveBeenCalledWith({ + [newSubscription.endpoint]: [mockUser1], + }); + + notificationsSub.unsubscribe(); + }); + + test("when web push is supported and no existing subscription, should call API", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + + pushManager.getSubscription.mockResolvedValue(null); + pushManager.subscribe.mockResolvedValue(createSub("dGVzdA")); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(pushManager.subscribe).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1); + + notificationsSub.unsubscribe(); + }); + + test("when web push is supported and existing subscription with different key, should call API", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + + pushManager.getSubscription.mockResolvedValue(createSub("dGVzdF9hbHQ")); + + pushManager.subscribe.mockResolvedValue(createSub("dGVzdA")); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(pushManager.subscribe).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1); + + notificationsSub.unsubscribe(); + }); + + test("when server config emits multiple times quickly while api call takes a long time will only call API once", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: "dGVzdA", + }), + }), + ), + ); + + pushManager.getSubscription.mockResolvedValue(createSub("dGVzdF9hbHQ")); + + pushManager.subscribe.mockResolvedValue(createSub("dGVzdA")); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("supported"); + const service = (supportStatus as Supported).service; + expect(service).not.toBeFalsy(); + + const notificationsSub = service.notifications$.subscribe(); + + await awaitAsync(2); + + expect(pushManager.getSubscription).toHaveBeenCalledTimes(1); + expect(pushManager.subscribe).toHaveBeenCalledTimes(1); + expect(webPushApiService.putSubscription).toHaveBeenCalledTimes(1); + + notificationsSub.unsubscribe(); + }); + + it("server config shows SignalR support should return not-supported", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.SignalR, + }), + }), + ), + ); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("not-supported"); + }); + + it("server config shows web push but no public key support should return not-supported", async () => { + configService.serverConfig$ = of( + new ServerConfig( + new ServerConfigData({ + push: new PushSettingsConfigResponse({ + pushTechnology: PushTechnology.WebPush, + vapidPublicKey: null, + }), + }), + ), + ); + + const supportStatus = await firstValueFrom(sut.supportStatus$(mockUser1)); + expect(supportStatus.type).toBe("not-supported"); + }); + }); +}); diff --git a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts index a1143d14d1d..528ad90ed61 100644 --- a/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts +++ b/libs/common/src/platform/notifications/internal/worker-webpush-connection.service.ts @@ -9,6 +9,7 @@ import { Subject, Subscription, switchMap, + withLatestFrom, } from "rxjs"; import { PushTechnology } from "../../../enums/push-technology.enum"; @@ -17,6 +18,7 @@ import { UserId } from "../../../types/guid"; import { ConfigService } from "../../abstractions/config/config.service"; import { SupportStatus } from "../../misc/support-status"; import { Utils } from "../../misc/utils"; +import { KeyDefinition, StateProvider, WEB_PUSH_SUBSCRIPTION } from "../../state"; import { WebPushNotificationsApiService } from "./web-push-notifications-api.service"; import { WebPushConnectionService, WebPushConnector } from "./webpush-connection.service"; @@ -48,6 +50,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService private readonly configService: ConfigService, private readonly webPushApiService: WebPushNotificationsApiService, private readonly serviceWorkerRegistration: ServiceWorkerRegistration, + private readonly stateProvider: StateProvider, ) {} start(): Subscription { @@ -97,6 +100,7 @@ export class WorkerWebPushConnectionService implements WebPushConnectionService this.serviceWorkerRegistration, this.pushEvent, this.pushChangeEvent, + this.stateProvider, ), } satisfies SupportStatus; }), @@ -114,20 +118,36 @@ class MyWebPushConnector implements WebPushConnector { private readonly serviceWorkerRegistration: ServiceWorkerRegistration, private readonly pushEvent$: Observable, private readonly pushChangeEvent$: Observable, + private readonly stateProvider: StateProvider, ) { + const subscriptionUsersState = this.stateProvider.getGlobal(WEB_PUSH_SUBSCRIPTION_USERS); this.notifications$ = this.getOrCreateSubscription$(this.vapidPublicKey).pipe( - concatMap((subscription) => { - return defer(() => { - if (subscription == null) { - throw new Error("Expected a non-null subscription."); - } - return this.webPushApiService.putSubscription(subscription.toJSON()); - }).pipe( - switchMap(() => this.pushEvent$), - map((e) => { - return new NotificationResponse(e.data.json().data); - }), - ); + withLatestFrom(subscriptionUsersState.state$.pipe(map((x) => x ?? {}))), + concatMap(async ([[isExistingSubscription, subscription], subscriptionUsers]) => { + if (subscription == null) { + throw new Error("Expected a non-null subscription."); + } + + // If this is a new subscription, we can clear state and start over + if (!isExistingSubscription) { + subscriptionUsers = {}; + } + + // If the user is already subscribed, we don't need to do anything + if (subscriptionUsers[subscription.endpoint]?.includes(this.userId)) { + return; + } + subscriptionUsers[subscription.endpoint] ??= []; + subscriptionUsers[subscription.endpoint].push(this.userId); + // Update the state with the new subscription-user association + await subscriptionUsersState.update(() => subscriptionUsers); + + // Inform the server about the new subscription-user association + await this.webPushApiService.putSubscription(subscription.toJSON()); + }), + switchMap(() => this.pushEvent$), + map((e) => { + return new NotificationResponse(e.data.json().data); }), ); } @@ -146,7 +166,7 @@ class MyWebPushConnector implements WebPushConnector { await this.serviceWorkerRegistration.pushManager.getSubscription(); if (existingSubscription == null) { - return await this.pushManagerSubscribe(key); + return [false, await this.pushManagerSubscribe(key)] as const; } const subscriptionKey = Utils.fromBufferToUrlB64( @@ -159,12 +179,30 @@ class MyWebPushConnector implements WebPushConnector { if (subscriptionKey !== key) { // There is a subscription, but it's not for the current server, unsubscribe and then make a new one await existingSubscription.unsubscribe(); - return await this.pushManagerSubscribe(key); + return [false, await this.pushManagerSubscribe(key)] as const; } - return existingSubscription; + return [true, existingSubscription] as const; }), - this.pushChangeEvent$.pipe(map((event) => event.newSubscription)), + this.pushChangeEvent$.pipe(map((event) => [false, event.newSubscription] as const)), ); } } + +export const WEB_PUSH_SUBSCRIPTION_USERS = new KeyDefinition>( + WEB_PUSH_SUBSCRIPTION, + "subUsers", + { + deserializer: (obj) => { + if (obj == null) { + return {}; + } + + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = Array.isArray(value) ? value : []; + } + return result; + }, + }, +); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 563f8404931..f9e6a5007c7 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -128,6 +128,9 @@ export const EXTENSION_INITIAL_INSTALL_DISK = new StateDefinition( "extensionInitialInstall", "disk", ); +export const WEB_PUSH_SUBSCRIPTION = new StateDefinition("webPushSubscription", "disk", { + web: "disk-local", +}); // Design System diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index f9d308ba2a0..ca6cd6570a4 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1813,6 +1813,11 @@ export class ApiService implements ApiServiceAbstraction { if (authed) { const authHeader = await this.getActiveBearerToken(); headers.set("Authorization", "Bearer " + authHeader); + } else { + // For unauthenticated requests, we need to tell the server what the device is for flag targeting, + // since it won't be able to get it from the access token. + const appId = await this.appIdService.getAppId(); + headers.set("Device-Identifier", appId); } if (body != null) { diff --git a/libs/common/src/abstractions/search.service.ts b/libs/common/src/vault/abstractions/search.service.ts similarity index 82% rename from libs/common/src/abstractions/search.service.ts rename to libs/common/src/vault/abstractions/search.service.ts index 2bff33bf2db..c981aa748a4 100644 --- a/libs/common/src/abstractions/search.service.ts +++ b/libs/common/src/vault/abstractions/search.service.ts @@ -2,9 +2,9 @@ // @ts-strict-ignore import { Observable } from "rxjs"; -import { SendView } from "../tools/send/models/view/send.view"; -import { IndexedEntityId, UserId } from "../types/guid"; -import { CipherView } from "../vault/models/view/cipher.view"; +import { SendView } from "../../tools/send/models/view/send.view"; +import { IndexedEntityId, UserId } from "../../types/guid"; +import { CipherView } from "../models/view/cipher.view"; export abstract class SearchService { indexedEntityId$: (userId: UserId) => Observable; diff --git a/libs/common/src/vault/enums/extension-page-urls.enum.ts b/libs/common/src/vault/enums/extension-page-urls.enum.ts new file mode 100644 index 00000000000..95f9e0a21df --- /dev/null +++ b/libs/common/src/vault/enums/extension-page-urls.enum.ts @@ -0,0 +1,12 @@ +import { UnionOfValues } from "../types/union-of-values"; + +/** + * Available pages within the extension by their URL. + * Useful when opening a specific page within the popup. + */ +export const ExtensionPageUrls: Record = { + Index: "popup/index.html#/", + AtRiskPasswords: "popup/index.html#/at-risk-passwords", +} as const; + +export type ExtensionPageUrls = UnionOfValues; diff --git a/libs/common/src/vault/enums/index.ts b/libs/common/src/vault/enums/index.ts index d7d1d06d2b9..c996a14a81a 100644 --- a/libs/common/src/vault/enums/index.ts +++ b/libs/common/src/vault/enums/index.ts @@ -3,3 +3,4 @@ export * from "./cipher-type"; export * from "./field-type.enum"; export * from "./linked-id-type.enum"; export * from "./secure-note-type.enum"; +export * from "./extension-page-urls.enum"; diff --git a/libs/common/src/vault/enums/vault-messages.enum.ts b/libs/common/src/vault/enums/vault-messages.enum.ts index 73272564432..fe76cd72427 100644 --- a/libs/common/src/vault/enums/vault-messages.enum.ts +++ b/libs/common/src/vault/enums/vault-messages.enum.ts @@ -1,7 +1,9 @@ const VaultMessages = { HasBwInstalled: "hasBwInstalled", checkBwInstalled: "checkIfBWExtensionInstalled", + /** @deprecated use {@link OpenBrowserExtensionToUrl} */ OpenAtRiskPasswords: "openAtRiskPasswords", + OpenBrowserExtensionToUrl: "openBrowserExtensionToUrl", PopupOpened: "popupOpened", } as const; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index e6d11a82b69..9cc9226cd46 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -353,14 +353,14 @@ export class Cipher extends Domain implements Decryptable { type: this.type, favorite: this.favorite ?? false, organizationUseTotp: this.organizationUseTotp ?? false, - edit: this.edit, + edit: this.edit ?? true, permissions: this.permissions ? { delete: this.permissions.delete, restore: this.permissions.restore, } : undefined, - viewPassword: this.viewPassword, + viewPassword: this.viewPassword ?? true, localData: this.localData ? { lastUsedDate: this.localData.lastUsedDate diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index b3e6fad70dd..5874d99c99d 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -97,8 +97,8 @@ export class LoginUri extends Domain { */ toSdkLoginUri(): SdkLoginUri { return { - uri: this.uri.toJSON(), - uriChecksum: this.uriChecksum.toJSON(), + uri: this.uri?.toJSON(), + uriChecksum: this.uriChecksum?.toJSON(), match: this.match, }; } diff --git a/libs/common/src/vault/service-utils.spec.ts b/libs/common/src/vault/service-utils.spec.ts index 619d3d72ee6..db414da76d7 100644 --- a/libs/common/src/vault/service-utils.spec.ts +++ b/libs/common/src/vault/service-utils.spec.ts @@ -36,24 +36,6 @@ describe("serviceUtils", () => { }); }); - describe("nestedTraverse_vNext", () => { - it("should traverse a tree and add a node at the correct position given a valid path", () => { - const nodeToBeAdded: FakeObject = { id: "1.2.1", name: "1.2.1" }; - const path = ["1", "1.2", "1.2.1"]; - - ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/"); - expect(nodeTree[0].children[1].children[0].node).toEqual(nodeToBeAdded); - }); - - it("should combine the path for missing nodes and use as the added node name given an invalid path", () => { - const nodeToBeAdded: FakeObject = { id: "blank", name: "blank" }; - const path = ["3", "3.1", "3.1.1"]; - - ServiceUtils.nestedTraverse_vNext(nodeTree, 0, path, nodeToBeAdded, null, "/"); - expect(nodeTree[2].children[0].node.name).toEqual("3.1/3.1.1"); - }); - }); - describe("getTreeNodeObject", () => { it("should return a matching node given a single tree branch and a valid id", () => { const id = "1.1.1"; diff --git a/libs/common/src/vault/service-utils.ts b/libs/common/src/vault/service-utils.ts index 9595434223f..0d863e6ad0b 100644 --- a/libs/common/src/vault/service-utils.ts +++ b/libs/common/src/vault/service-utils.ts @@ -4,64 +4,6 @@ import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node"; export class ServiceUtils { - static nestedTraverse( - nodeTree: TreeNode[], - partIndex: number, - parts: string[], - obj: ITreeNodeObject, - parent: TreeNode | undefined, - delimiter: string, - ) { - if (parts.length <= partIndex) { - return; - } - - const end: boolean = partIndex === parts.length - 1; - const partName: string = parts[partIndex]; - - for (let i = 0; i < nodeTree.length; i++) { - if (nodeTree[i].node.name !== partName) { - continue; - } - if (end && nodeTree[i].node.id !== obj.id) { - // Another node exists with the same name as the node being added - nodeTree.push(new TreeNode(obj, parent, partName)); - return; - } - // Move down the tree to the next level - ServiceUtils.nestedTraverse( - nodeTree[i].children, - partIndex + 1, - parts, - obj, - nodeTree[i], - delimiter, - ); - return; - } - - // If there's no node here with the same name... - if (nodeTree.filter((n) => n.node.name === partName).length === 0) { - // And we're at the end of the path given, add the node - if (end) { - nodeTree.push(new TreeNode(obj, parent, partName)); - return; - } - // And we're not at the end of the path, combine the current name with the next name - // 1, *1.2, 1.2.1 becomes - // 1, *1.2/1.2.1 - const newPartName = partName + delimiter + parts[partIndex + 1]; - ServiceUtils.nestedTraverse( - nodeTree, - 0, - [newPartName, ...parts.slice(partIndex + 2)], - obj, - parent, - delimiter, - ); - } - } - /** * Recursively adds a node to nodeTree * @param {TreeNode[]} nodeTree - An array of TreeNodes that the node will be added to @@ -71,7 +13,7 @@ export class ServiceUtils { * @param {ITreeNodeObject} parent - The parent node of the `obj` node * @param {string} delimiter - The delimiter used to split the path string, will be used to combine the path for missing nodes */ - static nestedTraverse_vNext( + static nestedTraverse( nodeTree: TreeNode[], partIndex: number, parts: string[], @@ -104,7 +46,7 @@ export class ServiceUtils { // 1, *1.2, 1.2.1 becomes // 1, *1.2/1.2.1 const newPartName = partName + delimiter + parts[partIndex + 1]; - ServiceUtils.nestedTraverse_vNext( + ServiceUtils.nestedTraverse( nodeTree, 0, [newPartName, ...parts.slice(partIndex + 2)], @@ -114,7 +56,7 @@ export class ServiceUtils { ); } else { // There is a node here with the same name, descend into it - ServiceUtils.nestedTraverse_vNext( + ServiceUtils.nestedTraverse( matchingNodes[0].children, partIndex + 1, parts, diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 1a0b1568775..2fd9b03a37e 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -10,7 +10,6 @@ import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-a import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray, makeSymmetricCryptoKey } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; -import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { BulkEncryptService } from "../../key-management/crypto/abstractions/bulk-encrypt.service"; @@ -29,6 +28,7 @@ import { CipherKey, OrgKey, UserKey } from "../../types/key"; import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { EncryptionContext } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; +import { SearchService } from "../abstractions/search.service"; import { FieldType } from "../enums"; import { CipherRepromptType } from "../enums/cipher-reprompt-type"; import { CipherType } from "../enums/cipher-type"; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index d8d180a7c3e..b4f79b2467e 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -9,7 +9,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; -import { SearchService } from "../../abstractions/search.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; @@ -38,6 +37,7 @@ import { EncryptionContext, } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; +import { SearchService } from "../abstractions/search.service"; import { FieldType } from "../enums"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/vault/services/search.service.ts similarity index 96% rename from libs/common/src/services/search.service.ts rename to libs/common/src/vault/services/search.service.ts index 3e6a070195a..4b7a26b6a31 100644 --- a/libs/common/src/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -4,21 +4,21 @@ import * as lunr from "lunr"; import { Observable, firstValueFrom, map } from "rxjs"; import { Jsonify } from "type-fest"; -import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; -import { UriMatchStrategy } from "../models/domain/domain-service"; -import { I18nService } from "../platform/abstractions/i18n.service"; -import { LogService } from "../platform/abstractions/log.service"; +import { UriMatchStrategy } from "../../models/domain/domain-service"; +import { I18nService } from "../../platform/abstractions/i18n.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { SingleUserState, StateProvider, UserKeyDefinition, VAULT_SEARCH_MEMORY, -} from "../platform/state"; -import { SendView } from "../tools/send/models/view/send.view"; -import { IndexedEntityId, UserId } from "../types/guid"; -import { FieldType } from "../vault/enums"; -import { CipherType } from "../vault/enums/cipher-type"; -import { CipherView } from "../vault/models/view/cipher.view"; +} from "../../platform/state"; +import { SendView } from "../../tools/send/models/view/send.view"; +import { IndexedEntityId, UserId } from "../../types/guid"; +import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; +import { FieldType } from "../enums"; +import { CipherType } from "../enums/cipher-type"; +import { CipherView } from "../models/view/cipher.view"; export type SerializedLunrIndex = { version: string; diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.component.html b/libs/components/src/anon-layout/anon-layout-wrapper.component.html index 95b1e6cadfe..0d393b30362 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.component.html +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.html @@ -4,7 +4,7 @@ [icon]="pageIcon" [showReadonlyHostname]="showReadonlyHostname" [maxWidth]="maxWidth" - [titleAreaMaxWidth]="titleAreaMaxWidth" + [hideCardWrapper]="hideCardWrapper" > diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts index ffc601bdf1d..20380f137a6 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts @@ -10,7 +10,7 @@ import { Translation } from "../dialog"; import { Icon } from "../icon"; import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service"; -import { AnonLayoutComponent } from "./anon-layout.component"; +import { AnonLayoutComponent, AnonLayoutMaxWidth } from "./anon-layout.component"; export interface AnonLayoutWrapperData { /** @@ -36,11 +36,11 @@ export interface AnonLayoutWrapperData { /** * Optional flag to set the max-width of the page. Defaults to 'md' if not provided. */ - maxWidth?: "md" | "3xl"; + maxWidth?: AnonLayoutMaxWidth; /** - * Optional flag to set the max-width of the title area. Defaults to null if not provided. + * Hide the card that wraps the default content. Defaults to false. */ - titleAreaMaxWidth?: "md"; + hideCardWrapper?: boolean; } @Component({ @@ -54,8 +54,8 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { protected pageSubtitle: string; protected pageIcon: Icon; protected showReadonlyHostname: boolean; - protected maxWidth: "md" | "3xl"; - protected titleAreaMaxWidth: "md"; + protected maxWidth: AnonLayoutMaxWidth; + protected hideCardWrapper: boolean; constructor( private router: Router, @@ -106,7 +106,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]); this.maxWidth = firstChildRouteData["maxWidth"]; - this.titleAreaMaxWidth = firstChildRouteData["titleAreaMaxWidth"]; + this.hideCardWrapper = Boolean(firstChildRouteData["hideCardWrapper"]); } private listenForServiceDataChanges() { @@ -143,6 +143,10 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.showReadonlyHostname = data.showReadonlyHostname; } + if (data.hideCardWrapper !== undefined) { + this.hideCardWrapper = data.hideCardWrapper; + } + // Manually fire change detection to avoid ExpressionChangedAfterItHasBeenCheckedError // when setting the page data from a service this.changeDetectorRef.detectChanges(); @@ -164,7 +168,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.pageIcon = null; this.showReadonlyHostname = null; this.maxWidth = null; - this.titleAreaMaxWidth = null; + this.hideCardWrapper = null; } ngOnDestroy() { diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts b/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts index 57fba034c7e..4de7952344b 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts @@ -87,6 +87,7 @@ const decorators = (options: { appLogoLabel: "app logo label", finishCreatingYourAccountBySettingAPassword: "Finish creating your account by setting a password", + enterpriseSingleSignOn: "Enterprise Single Sign-On", }); }, }, diff --git a/libs/components/src/anon-layout/anon-layout.component.html b/libs/components/src/anon-layout/anon-layout.component.html index 1e16dba82cc..4dfde5e7ef3 100644 --- a/libs/components/src/anon-layout/anon-layout.component.html +++ b/libs/components/src/anon-layout/anon-layout.component.html @@ -13,11 +13,8 @@ -
-
+
+
@@ -36,14 +33,20 @@
-
- -
+ @if (hideCardWrapper) { +
+ +
+ } @else { +
+ +
+ }
@@ -60,3 +63,7 @@
+ + + + diff --git a/libs/components/src/anon-layout/anon-layout.component.ts b/libs/components/src/anon-layout/anon-layout.component.ts index 4155a186384..ee3a7ca7bee 100644 --- a/libs/components/src/anon-layout/anon-layout.component.ts +++ b/libs/components/src/anon-layout/anon-layout.component.ts @@ -14,6 +14,8 @@ import { BitwardenLogo, BitwardenShield } from "../icon/icons"; import { SharedModule } from "../shared"; import { TypographyModule } from "../typography"; +export type AnonLayoutMaxWidth = "md" | "lg" | "xl" | "2xl" | "3xl"; + @Component({ selector: "auth-anon-layout", templateUrl: "./anon-layout.component.html", @@ -33,29 +35,38 @@ export class AnonLayoutComponent implements OnInit, OnChanges { @Input() hideLogo: boolean = false; @Input() hideFooter: boolean = false; @Input() hideIcon: boolean = false; + @Input() hideCardWrapper: boolean = false; /** - * Max width of the title area content - * - * @default null - */ - @Input() titleAreaMaxWidth?: "md"; - - /** - * Max width of the layout content + * Max width of the anon layout title, subtitle, and content areas. * * @default 'md' */ - @Input() maxWidth: "md" | "3xl" = "md"; + @Input() maxWidth: AnonLayoutMaxWidth = "md"; protected logo = BitwardenLogo; - protected year = "2024"; + protected year: string; protected clientType: ClientType; protected hostname: string; protected version: string; protected hideYearAndVersion = false; + get maxWidthClass(): string { + switch (this.maxWidth) { + case "md": + return "tw-max-w-md"; + case "lg": + return "tw-max-w-lg"; + case "xl": + return "tw-max-w-xl"; + case "2xl": + return "tw-max-w-2xl"; + case "3xl": + return "tw-max-w-3xl"; + } + } + constructor( private environmentService: EnvironmentService, private platformUtilsService: PlatformUtilsService, @@ -67,7 +78,6 @@ export class AnonLayoutComponent implements OnInit, OnChanges { async ngOnInit() { this.maxWidth = this.maxWidth ?? "md"; - this.titleAreaMaxWidth = this.titleAreaMaxWidth ?? null; this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname(); this.version = await this.platformUtilsService.getApplicationVersion(); diff --git a/libs/components/src/anon-layout/anon-layout.mdx b/libs/components/src/anon-layout/anon-layout.mdx index 039a1aa5f28..9d40d617b0d 100644 --- a/libs/components/src/anon-layout/anon-layout.mdx +++ b/libs/components/src/anon-layout/anon-layout.mdx @@ -165,4 +165,4 @@ import { EnvironmentSelectorComponent } from "./components/environment-selector/ --- - + diff --git a/libs/components/src/anon-layout/anon-layout.stories.ts b/libs/components/src/anon-layout/anon-layout.stories.ts index 395703fc018..24aaf10f7ba 100644 --- a/libs/components/src/anon-layout/anon-layout.stories.ts +++ b/libs/components/src/anon-layout/anon-layout.stories.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule } from "../button"; +import { Icon } from "../icon"; import { LockIcon } from "../icon/icons"; import { I18nMockService } from "../utils/i18n-mock.service"; @@ -18,6 +19,23 @@ class MockPlatformUtilsService implements Partial { getClientType = () => ClientType.Web; } +type StoryArgs = Pick< + AnonLayoutComponent, + | "title" + | "subtitle" + | "showReadonlyHostname" + | "hideCardWrapper" + | "hideIcon" + | "hideLogo" + | "hideFooter" + | "maxWidth" +> & { + contentLength: "normal" | "long" | "thin"; + showSecondary: boolean; + useDefaultIcon: boolean; + icon: Icon; +}; + export default { title: "Component Library/Anon Layout", component: AnonLayoutComponent, @@ -31,12 +49,11 @@ export default { }, { provide: I18nService, - useFactory: () => { - return new I18nMockService({ + useFactory: () => + new I18nMockService({ accessing: "Accessing", appLogoLabel: "app logo label", - }); - }, + }), }, { provide: EnvironmentService, @@ -55,174 +72,179 @@ export default { ], }), ], + + render: (args: StoryArgs) => { + const { useDefaultIcon, icon, ...rest } = args; + return { + props: { + ...rest, + icon: useDefaultIcon ? null : icon, + }, + template: ` + + +
Thin Content
+
+
Long Content
+
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
+
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
+
+
+
Normal Content
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+
+ +
+
+ Secondary Projected Content (optional) +
+ +
+
+ `, + }; + }, + + argTypes: { + title: { control: "text" }, + subtitle: { control: "text" }, + + icon: { control: false, table: { disable: true } }, + useDefaultIcon: { + control: false, + table: { disable: true }, + description: "If true, passes null so component falls back to its built-in icon", + }, + + showReadonlyHostname: { control: "boolean" }, + maxWidth: { + control: "select", + options: ["md", "lg", "xl", "2xl", "3xl"], + }, + + hideCardWrapper: { control: "boolean" }, + hideIcon: { control: "boolean" }, + hideLogo: { control: "boolean" }, + hideFooter: { control: "boolean" }, + + contentLength: { + control: "radio", + options: ["normal", "long", "thin"], + }, + + showSecondary: { control: "boolean" }, + }, + args: { title: "The Page Title", subtitle: "The subtitle (optional)", - showReadonlyHostname: true, icon: LockIcon, + useDefaultIcon: false, + showReadonlyHostname: false, + maxWidth: "md", + hideCardWrapper: false, + hideIcon: false, hideLogo: false, + hideFooter: false, + contentLength: "normal", + showSecondary: false, }, -} as Meta; +} as Meta; -type Story = StoryObj; +type Story = StoryObj; -export const WithPrimaryContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. - ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
-
- `, - }), +export const NormalPrimaryContent: Story = { + args: { + contentLength: "normal", + }, }; -export const WithSecondaryContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. - // Notice that slot="secondary" is requred to project any secondary content. - ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
- -
-
Secondary Projected Content (optional)
- -
-
- `, - }), +export const LongPrimaryContent: Story = { + args: { + contentLength: "long", + }, }; -export const WithLongContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. - ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
-
- -
-
Secondary Projected Content (optional)
-

Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est?

- -
-
- `, - }), +export const ThinPrimaryContent: Story = { + args: { + contentLength: "thin", + }, }; -export const WithThinPrimaryContent: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. - ` - -
Lorem ipsum
- -
-
Secondary Projected Content (optional)
- -
-
- `, - }), +export const LongContentAndTitlesAndDefaultWidth: Story = { + args: { + title: + "This is a very long title that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + subtitle: + "This is a very long subtitle that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + contentLength: "long", + }, }; -export const WithCustomIcon: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. - ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
-
- `, - }), +export const LongContentAndTitlesAndLargestWidth: Story = { + args: { + title: + "This is a very long title that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + subtitle: + "This is a very long subtitle that might not fit in the default width. It's really long and descriptive, so it might take up more space than usual.", + contentLength: "long", + maxWidth: "3xl", + }, }; -export const HideIcon: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. - ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
-
- `, - }), +export const SecondaryContent: Story = { + args: { + showSecondary: true, + }, }; -export const HideLogo: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. - ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
-
- `, - }), +export const NoTitle: Story = { args: { title: undefined } }; + +export const NoSubtitle: Story = { args: { subtitle: undefined } }; + +export const NoWrapper: Story = { + args: { hideCardWrapper: true }, }; -export const HideFooter: Story = { - render: (args) => ({ - props: args, - template: - // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. - ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
-
- `, - }), +export const DefaultIcon: Story = { + args: { useDefaultIcon: true }, }; -export const WithTitleAreaMaxWidth: Story = { - render: (args) => ({ - props: { - ...args, - title: "This is a very long long title to demonstrate titleAreaMaxWidth set to 'md'", - subtitle: - "This is a very long subtitle that demonstrates how the max width container handles longer text content with the titleAreaMaxWidth input set to 'md'. Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?", - }, - template: ` - -
-
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
-
- `, - }), +export const NoIcon: Story = { + args: { hideIcon: true }, +}; + +export const NoLogo: Story = { + args: { hideLogo: true }, +}; + +export const NoFooter: Story = { + args: { hideFooter: true }, +}; + +export const ReadonlyHostname: Story = { + args: { showReadonlyHostname: true }, +}; + +export const MinimalState: Story = { + args: { + title: undefined, + subtitle: undefined, + contentLength: "normal", + hideCardWrapper: true, + hideIcon: true, + hideLogo: true, + hideFooter: true, + }, }; diff --git a/libs/components/src/icon/icon.mdx b/libs/components/src/icon/icon.mdx index d1809c81cd2..01f03d1861b 100644 --- a/libs/components/src/icon/icon.mdx +++ b/libs/components/src/icon/icon.mdx @@ -41,12 +41,14 @@ import { IconModule } from "@bitwarden/components"; - A non-comprehensive list of common colors and their associated classes is below: - | Hardcoded Value | Tailwind Stroke Class | Tailwind Fill Class | Tailwind Variable | - | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------------------- | ----------------------- | - | `#020F66` | `tw-stroke-art-primary` | `tw-fill-art-primary` | `--color-art-primary` | - | `#10949D` | `tw-stroke-art-accent` | `tw-fill-art-accent` | `--color-art-accent` | - | `#2CDDE9` | `tw-stroke-art-accent` | `tw-fill-art-accent` | `--color-art-accent` | - | `#89929F` | `tw-stroke-secondary-600` | `tw-fill-secondary-600` | `--color-secondary-600` | + | Hardcoded Value | Tailwind Stroke Class | Tailwind Fill Class | Tailwind Variable | + | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----------------------------------- | ----------------------------------- | + | `#020F66` | `tw-stroke-illustration-outline` | `tw-fill-illustration-outline` | `--color-illustration-outline` | + | `#DBE5F6` | `tw-stroke-illustration-bg-primary` | `tw-fill-illustration-bg-primary` | `--color-illustration-bg-primary` | + | `#AAC3EF` | `tw-stroke-illustration-bg-secondary` | `tw-fill-illustration-bg-secondary` | `--color-illustration-bg-secondary` | + | `#FFFFFF` | `tw-stroke-illustration-bg-tertiary` | `tw-fill-illustration-bg-tertiary` | `--color-illustration-bg-tertiary` | + | `#FFBF00` | `tw-stroke-illustration-tertiary` | `tw-fill-illustration-tertiary` | `--color-illustration-tertiary` | + | `#175DDC` | `tw-stroke-illustration-logo` | `tw-fill-illustration-logo` | `--color-illustration-logo` | - If the hex that you have on an SVG path is not listed above, there are a few ways to figure out the appropriate Tailwind class: @@ -56,20 +58,20 @@ import { IconModule } from "@bitwarden/components"; - Click on an individual path on the SVG until you see the path's properties in the right-hand panel. - Scroll down to the Colors section. - - Example: `Color/Art/Primary` + - Example: `Color/Illustration/Outline` - This also includes Hex or RGB values that can be used to find the appropriate Tailwind variable as well if you follow the manual search option below. - Create the appropriate stroke or fill class from the color used. - - Example: `Color/Art/Primary` corresponds to `--color-art-primary` which corresponds to - `tw-stroke-art-primary` or `tw-fill-art-primary`. + - Example: `Color/Illustration/Outline` corresponds to `--color-illustration-outline` which + corresponds to `tw-stroke-illustration-outline` or `tw-fill-illustration-outline`. - **Option 2: Manual Search** - Take the path's stroke or fill hex value and convert it to RGB using a tool like [Hex to RGB](https://www.rgbtohex.net/hex-to-rgb/). - Search for the RGB value without commas in our `tw-theme.css` to find the Tailwind variable that corresponds to the color. - Create the appropriate stroke or fill class using the Tailwind variable. - - Example: `--color-art-primary` corresponds to `tw-stroke-art-primary` or - `tw-fill-art-primary`. + - Example: `--color-illustration-outline` corresponds to `tw-stroke-illustration-outline` + or `tw-fill-illustration-outline`. 6. **Remove any hardcoded width or height attributes** if your SVG has a configured [viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox) attribute in order diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index bc174d14d23..528697600c4 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { hasModifierKey } from "@angular/cdk/keycodes"; import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay"; import { TemplatePortal } from "@angular/cdk/portal"; import { @@ -76,6 +77,13 @@ export class MenuTriggerForDirective implements OnDestroy { this.overlayRef.attach(templatePortal); this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => { + // Closing the menu is handled in this.destroyMenu, so we want to prevent the escape key + // from doing its normal default action, which would otherwise cause a parent component + // (like a dialog) or extension window to close + if (event?.key === "Escape" && !hasModifierKey(event)) { + event.preventDefault(); + } + if (["Tab", "Escape"].includes(event?.key)) { // Required to ensure tab order resumes correctly this.elementRef.nativeElement.focus(); diff --git a/libs/components/src/select/select.component.html b/libs/components/src/select/select.component.html index 84de9827b97..6d4c431f234 100644 --- a/libs/components/src/select/select.component.html +++ b/libs/components/src/select/select.component.html @@ -9,6 +9,7 @@ [clearable]="false" (close)="onClose()" appendTo="body" + [keyDownFn]="onKeyDown" >
diff --git a/libs/components/src/select/select.component.ts b/libs/components/src/select/select.component.ts index d2c48bf0f6e..909566bf1f8 100644 --- a/libs/components/src/select/select.component.ts +++ b/libs/components/src/select/select.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { hasModifierKey } from "@angular/cdk/keycodes"; import { Component, ContentChildren, @@ -185,4 +186,20 @@ export class SelectComponent implements BitFormFieldControl, ControlValueAcce protected onClose() { this.closed.emit(); } + + /** + * Prevent Escape key press from propagating to parent components + * (for example, parent dialog should not close when Escape is pressed in the select) + * + * @returns true to keep default key behavior; false to prevent default key behavior + * + * Needs to be arrow function to retain `this` scope. + */ + protected onKeyDown = (event: KeyboardEvent) => { + if (this.select.isOpen && event.key === "Escape" && !hasModifierKey(event)) { + event.stopPropagation(); + } + + return true; + }; } diff --git a/libs/components/src/stories/colors.mdx b/libs/components/src/stories/colors.mdx index 3a4a4f0fe3a..87ca673797b 100644 --- a/libs/components/src/stories/colors.mdx +++ b/libs/components/src/stories/colors.mdx @@ -62,9 +62,14 @@ export const Table = (args) => ( {Row("notification-600")} - {Row("art-primary")} - {Row("art-accent")} + {Row("illustration-outline")} + {Row("illustration-bg-primary")} + {Row("illustration-bg-secondary")} + {Row("illustration-bg-tertiary")} + {Row("illustration-tertiary")} + {Row("illustration-logo")} + Text @@ -78,6 +83,7 @@ export const Table = (args) => ( {Row("text-alt2")} {Row("text-code")} + ); diff --git a/libs/components/src/stories/virtual-scrolling.mdx b/libs/components/src/stories/virtual-scrolling.mdx index 94a86090dce..ab51d9865db 100644 --- a/libs/components/src/stories/virtual-scrolling.mdx +++ b/libs/components/src/stories/virtual-scrolling.mdx @@ -16,7 +16,7 @@ We export a similar directive, `bitScrollLayout`, that integrates with `bit-layo and should be used instead of `scrollWindow`. ```html - + @@ -27,7 +27,10 @@ and should be used instead of `scrollWindow`. Due to the initialization order of Angular components and their templates, `bitScrollLayout` will error if it is used _in the same template_ as the layout component: +With `bit-layout`: + ```html + @@ -35,20 +38,43 @@ error if it is used _in the same template_ as the layout component: ``` +With `popup-page`: + +```html + + + + + + +``` + In this particular composition, the child content gets constructed before the template of -`bit-layout` and thus has no scroll container to reference. Workarounds include: +`bit-layout` (or `popup-page`) and thus has no scroll container to reference. Workarounds include: 1. Wrap the child in another component. (This tends to happen by default when the layout is integrated with a `router-outlet`.) +With `bit-layout`: + ```html ``` +With `popup-page`: + +```html + + + +``` + 2. Use a `defer` block. +With `bit-layout`: + ```html @defer (on immediate) { @@ -58,3 +84,15 @@ In this particular composition, the child content gets constructed before the te } ``` + +With `popup-page`: + +```html + + @defer (on immediate) { + + +
+ } + +``` diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 103b90e0752..078357491e5 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -46,6 +46,7 @@ --color-notification-100: 255 225 247; --color-notification-600: 192 17 118; + /*art styles deprecated, use 'illustration' instead*/ --color-art-primary: 2 15 102; --color-art-accent: 44 221 223; @@ -58,6 +59,15 @@ --color-marketing-logo: 23 93 220; --tw-ring-offset-color: #ffffff; + + --tw-sm-breakpoint: 640px; + + --color-illustration-outline: 2 15 102; + --color-illustration-bg-primary: 219 229 246; + --color-illustration-bg-secondary: 170 195 239; + --color-illustration-bg-tertiary: 255 255 255; + --color-illustration-tertiary: 255 191 0; + --color-illustration-logo: 23 93 220; } .theme_light { @@ -104,6 +114,7 @@ --color-notification-100: 117 37 83; --color-notification-600: 255 143 208; + /*art styles deprecated, use 'illustration' instead*/ --color-art-primary: 243 246 249; --color-art-accent: 44 221 233; @@ -116,6 +127,13 @@ --color-marketing-logo: 255 255 255; --tw-ring-offset-color: #1f242e; + + --color-illustration-outline: 23 93 220; + --color-illustration-bg-primary: 170 195 239; + --color-illustration-bg-secondary: 121 161 233; + --color-illustration-bg-tertiary: 243 246 249; + --color-illustration-tertiary: 255 191 0; + --color-illustration-logo: 255 255 255; } /** diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index fde59f4a089..c38515cf775 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -63,6 +63,7 @@ module.exports = { 100: rgba("--color-notification-100"), 600: rgba("--color-notification-600"), }, + // art styles deprecated, use 'illustration' instead art: { primary: rgba("--color-art-primary"), accent: rgba("--color-art-accent"), @@ -83,6 +84,14 @@ module.exports = { alt4: rgba("--color-background-alt4"), }, "marketing-logo": rgba("--color-marketing-logo"), + illustration: { + outline: rgba("--color-illustration-outline"), + "bg-primary": rgba("--color-illustration-bg-primary"), + "bg-secondary": rgba("--color-illustration-bg-secondary"), + "bg-tertiary": rgba("--color-illustration-bg-tertiary"), + tertiary: rgba("--color-illustration-tertiary"), + logo: rgba("--color-illustration-logo"), + }, }, textColor: { main: rgba("--color-text-main"), diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 59ab6739c06..f6d6603dca9 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -435,6 +435,12 @@ and select Export items → Enter your Master Password and select Continue. → Save the CSV file on your device. + + On the desktop application, go to Tools → Export → Enter your master password + → Select XML Format (*.xml) as Export format → Click on next → Choose which + entries should be included in the export → Click on next to export into the location + previously chosen. + { + it("should return error with invalid export version", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(MacOS_WrongVersion); + expect(result.errorMessage).toBe( + "Unsupported export version detected - (only 17.0 is supported)", + ); + }); + + it("should not create a folder/collection if the group fingerprint is null", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(MacOS_PasswordDepotXmlFile); + expect(result.folders.length).toBe(0); + }); + + it("should create folders and with correct assignments", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(MacOS_MultipleFolders); + + // Expect 10 ciphers, 5 without a folder and 3 within 'folder macos' and 2 with 'folder 2' + expect(result.ciphers.length).toBe(10); + + expect(result.folders.length).toBe(2); + expect(result.folders[0].name).toBe("folder macos"); + expect(result.folders[1].name).toBe("folder 2"); + + // 3 items within 'folder macos' + expect(result.folderRelationships[0]).toEqual([5, 0]); + expect(result.folderRelationships[1]).toEqual([6, 0]); + expect(result.folderRelationships[2]).toEqual([7, 0]); + + //2 items with 'folder 2' + expect(result.folderRelationships[3]).toEqual([8, 1]); + expect(result.folderRelationships[4]).toEqual([9, 1]); + }); + + it("should parse custom fields from a MacOS exported file", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(MacOS_PasswordDepotXmlFile); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toBe("card 1"); + expect(cipher.notes).toBe("comment"); + + expect(cipher.card).not.toBeNull(); + + expect(cipher.card.cardholderName).toBe("some CC holder"); + expect(cipher.card.number).toBe("4242424242424242"); + expect(cipher.card.brand).toBe("Visa"); + expect(cipher.card.expMonth).toBe("8"); + expect(cipher.card.expYear).toBe("2028"); + expect(cipher.card.code).toBe("125"); + }); +}); diff --git a/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts new file mode 100644 index 00000000000..ea84603aef4 --- /dev/null +++ b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts @@ -0,0 +1,496 @@ +// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. +// eslint-disable-next-line no-restricted-imports +import { CollectionView } from "@bitwarden/admin-console/common"; +import { FieldType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { CipherType } from "@bitwarden/sdk-internal"; + +import { + EncryptedFileData, + InvalidRootNodeData, + InvalidVersionData, + CreditCardTestData, + MissingPasswordsNodeData, + PasswordTestData, + IdentityTestData, + RDPTestData, + SoftwareLicenseTestData, + TeamViewerTestData, + PuttyTestData, + BankingTestData, + InformationTestData, + CertificateTestData, + EncryptedFileTestData, + DocumentTestData, +} from "../spec-data/password-depot-xml"; + +import { PasswordDepot17XmlImporter } from "./password-depot-17-xml-importer"; + +describe("Password Depot 17 Xml Importer", () => { + it("should return error with missing root tag", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(InvalidRootNodeData); + expect(result.errorMessage).toBe("Missing `passwordfile` node."); + }); + + it("should return error with invalid export version", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(InvalidVersionData); + expect(result.errorMessage).toBe( + "Unsupported export version detected - (only 17.0 is supported)", + ); + }); + + it("should return error if file is marked as encrypted", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(EncryptedFileData); + expect(result.errorMessage).toBe("Encrypted Password Depot files are not supported."); + }); + + it("should return error with missing passwords node tag", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(MissingPasswordsNodeData); + expect(result.success).toBe(false); + expect(result.errorMessage).toBe("Missing `passwordfile > passwords` node."); + }); + + it("should parse groups nodes into folders", async () => { + const importer = new PasswordDepot17XmlImporter(); + const folder = new FolderView(); + folder.name = "tempDB"; + const actual = [folder]; + + const result = await importer.parse(PasswordTestData); + expect(result.folders).toEqual(actual); + }); + + it("should parse password type into logins", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(PasswordTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.name).toBe("password type"); + expect(cipher.notes).toBe("someComment"); + + expect(cipher.login).not.toBeNull(); + expect(cipher.login.username).toBe("someUser"); + expect(cipher.login.password).toBe("p6J<]fmjv!:H&iJ7/Mwt@3i8"); + expect(cipher.login.uri).toBe("http://example.com"); + }); + + it("should parse any unmapped fields as custom fields", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(PasswordTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.name).toBe("password type"); + + expect(cipher.fields).not.toBeNull(); + + expect(cipher.fields[0].name).toBe("lastmodified"); + expect(cipher.fields[0].value).toBe("07.05.2025 13:37:56"); + expect(cipher.fields[0].type).toBe(FieldType.Text); + + expect(cipher.fields[1].name).toBe("expirydate"); + expect(cipher.fields[1].value).toBe("07.05.2025"); + expect(cipher.fields[0].type).toBe(FieldType.Text); + + expect(cipher.fields[2].name).toBe("importance"); + expect(cipher.fields[2].value).toBe("0"); + + let customField = cipher.fields.find((f) => f.name === "passwort"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("password"); + expect(customField.type).toEqual(FieldType.Hidden); + + customField = cipher.fields.find((f) => f.name === "memo"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("memo"); + expect(customField.type).toEqual(FieldType.Text); + + customField = cipher.fields.find((f) => f.name === "datum"); + expect(customField).toBeDefined(); + const expectedDate = new Date("2025-05-13T00:00:00Z"); + expect(customField.value).toEqual(expectedDate.toLocaleDateString()); + expect(customField.type).toEqual(FieldType.Text); + + customField = cipher.fields.find((f) => f.name === "nummer"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("1"); + expect(customField.type).toEqual(FieldType.Text); + + customField = cipher.fields.find((f) => f.name === "boolean"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("1"); + expect(customField.type).toEqual(FieldType.Boolean); + + customField = cipher.fields.find((f) => f.name === "decimal"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("1,01"); + expect(customField.type).toEqual(FieldType.Text); + + customField = cipher.fields.find((f) => f.name === "email"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("who@cares.com"); + expect(customField.type).toEqual(FieldType.Text); + + customField = cipher.fields.find((f) => f.name === "url"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("example.com"); + expect(customField.type).toEqual(FieldType.Text); + }); + + it("should parse credit cards", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(CreditCardTestData); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toBe("some CreditCard"); + expect(cipher.notes).toBe("someComment"); + + expect(cipher.card).not.toBeNull(); + + expect(cipher.card.cardholderName).toBe("some CC holder"); + expect(cipher.card.number).toBe("4222422242224222"); + expect(cipher.card.brand).toBe("Visa"); + expect(cipher.card.expMonth).toBe("5"); + expect(cipher.card.expYear).toBe("2026"); + expect(cipher.card.code).toBe("123"); + }); + + it("should parse identity type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(IdentityTestData); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toBe("identity type"); + expect(cipher.notes).toBe("someNote"); + + expect(cipher.identity).not.toBeNull(); + + expect(cipher.identity.firstName).toBe("firstName"); + expect(cipher.identity.lastName).toBe("surName"); + expect(cipher.identity.email).toBe("email"); + expect(cipher.identity.company).toBe("someCompany"); + expect(cipher.identity.address1).toBe("someStreet"); + expect(cipher.identity.address2).toBe("address 2"); + expect(cipher.identity.city).toBe("town"); + expect(cipher.identity.state).toBe("state"); + expect(cipher.identity.postalCode).toBe("zipCode"); + expect(cipher.identity.country).toBe("country"); + expect(cipher.identity.phone).toBe("phoneNumber"); + }); + + it("should parse RDP type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(RDPTestData); + + const cipher = result.ciphers.shift(); + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.name).toBe("rdp type"); + expect(cipher.notes).toBe("someNote"); + + expect(cipher.login).not.toBeNull(); + expect(cipher.login.username).toBe("someUser"); + expect(cipher.login.password).toBe("somePassword"); + expect(cipher.login.uri).toBe("ms-rd:subscribe?url=https://contoso.com"); + }); + + it("should parse software license into secure notes", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(SoftwareLicenseTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.SecureNote); + expect(cipher.name).toBe("software-license type"); + expect(cipher.notes).toBe("someComment"); + + expect(cipher.secureNote).not.toBeNull(); + expect(cipher.secureNote.type).toBe(SecureNoteType.Generic); + + let customField = cipher.fields.find((f) => f.name === "IDS_LicenseProduct"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("someProduct"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseVersion"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("someVersion"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseName"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("some User"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseKey"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("license-key"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseAdditionalKey"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("additional-license-key"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseURL"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("example.com"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseProtected"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("1"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseUserName"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("someUserName"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicensePassword"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("somePassword"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicensePurchaseDate"); + expect(customField).toBeDefined(); + const expectedDate = new Date("2025-05-12T00:00:00Z"); + expect(customField.value).toEqual(expectedDate.toLocaleDateString()); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseOrderNumber"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("order number"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseEmail"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("someEmail"); + + customField = cipher.fields.find((f) => f.name === "IDS_LicenseExpires"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("Nie"); + }); + + it("should parse team viewer into login type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(TeamViewerTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.name).toBe("TeamViewer type"); + expect(cipher.notes).toBe("someNote"); + + expect(cipher.login).not.toBeNull(); + expect(cipher.login.password).toBe("somePassword"); + expect(cipher.login.username).toBe(""); + expect(cipher.login.uri).toBe("partnerId"); + + const customField = cipher.fields.find((f) => f.name === "IDS_TeamViewerMode"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("0"); + }); + + it("should parse putty into login type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(PuttyTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.name).toBe("Putty type"); + expect(cipher.notes).toBe("someNote"); + + expect(cipher.login).not.toBeNull(); + expect(cipher.login.password).toBe("somePassword"); + expect(cipher.login.username).toBe("someUser"); + expect(cipher.login.uri).toBe("localhost"); + + let customField = cipher.fields.find((f) => f.name === "IDS_PuTTyProtocol"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("0"); + + customField = cipher.fields.find((f) => f.name === "IDS_PuTTyKeyFile"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("pathToKeyFile"); + + customField = cipher.fields.find((f) => f.name === "IDS_PuTTyKeyPassword"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("passwordForKeyFile"); + + customField = cipher.fields.find((f) => f.name === "IDS_PuTTyPort"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("8080"); + }); + + it("should parse banking item type into login type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(BankingTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.name).toBe("banking type"); + expect(cipher.notes).toBe("someNote"); + + expect(cipher.login).not.toBeNull(); + expect(cipher.login.password).toBe("somePassword"); + expect(cipher.login.username).toBe("someUser"); + expect(cipher.login.uri).toBe("http://some-bank.com"); + + let customField = cipher.fields.find((f) => f.name === "IDS_ECHolder"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("account holder"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECAccountNumber"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("1234567890"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECBLZ"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("12345678"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECBankName"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("someBank"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECBIC"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("bic"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECIBAN"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("iban"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECCardNumber"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("12345678"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECPhone"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("0049"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECLegitimacyID"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("1234"); + + customField = cipher.fields.find((f) => f.name === "IDS_ECPIN"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("123"); + + customField = cipher.fields.find((f) => f.name === "tan_1_value"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("1234"); + + customField = cipher.fields.find((f) => f.name === "tan_1_used"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("12.05.2025 15:10:54"); + + // TAN entries + customField = cipher.fields.find((f) => f.name === "tan_1_amount"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual(" 100,00"); + + customField = cipher.fields.find((f) => f.name === "tan_1_comment"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("some TAN note"); + + customField = cipher.fields.find((f) => f.name === "tan_1_ccode"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("123"); + + customField = cipher.fields.find((f) => f.name === "tan_2_value"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("4321"); + + customField = cipher.fields.find((f) => f.name === "tan_2_amount"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual(" 0,00"); + }); + + it("should parse information into secure note type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(InformationTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.SecureNote); + expect(cipher.name).toBe("information type"); + expect(cipher.notes).toBe("some note content"); + }); + + it("should parse certificate into login type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(CertificateTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.name).toBe("certificate type"); + expect(cipher.notes).toBe("someNote"); + + expect(cipher.login).not.toBeNull(); + expect(cipher.login.password).toBe("somePassword"); + }); + + it("should parse encrypted file into login type", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(EncryptedFileTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.Login); + expect(cipher.name).toBe("encrypted file type"); + expect(cipher.notes).toBe("some comment"); + + expect(cipher.login).not.toBeNull(); + expect(cipher.login.password).toBe("somePassword"); + }); + + it("should parse document type into secure note", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(DocumentTestData); + + const cipher = result.ciphers.shift(); + + expect(cipher.type).toEqual(CipherType.SecureNote); + expect(cipher.name).toBe("document type"); + expect(cipher.notes).toBe("document comment"); + + let customField = cipher.fields.find((f) => f.name === "IDS_DocumentSize"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("27071"); + + customField = cipher.fields.find((f) => f.name === "IDS_DocumentFolder"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("C:\\Users\\DJSMI\\Downloads\\"); + + customField = cipher.fields.find((f) => f.name === "IDS_DocumentFile"); + expect(customField).toBeDefined(); + expect(customField.value).toEqual("C:\\Users\\DJSMI\\Downloads\\some.pdf"); + }); + + it("should parse favourites and set them on the target item", async () => { + const importer = new PasswordDepot17XmlImporter(); + const result = await importer.parse(PasswordTestData); + + let cipher = result.ciphers.shift(); + expect(cipher.name).toBe("password type"); + expect(cipher.favorite).toBe(false); + + cipher = result.ciphers.shift(); + expect(cipher.name).toBe("password type (2)"); + expect(cipher.favorite).toBe(true); + + cipher = result.ciphers.shift(); + expect(cipher.name).toBe("password type (3)"); + expect(cipher.favorite).toBe(true); + }); + + it("should parse groups nodes into collections when importing into an organization", async () => { + const importer = new PasswordDepot17XmlImporter(); + importer.organizationId = "someOrgId"; + const collection = new CollectionView(); + collection.name = "tempDB"; + const actual = [collection]; + + const result = await importer.parse(PasswordTestData); + expect(result.collections).toEqual(actual); + }); +}); diff --git a/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.ts b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.ts new file mode 100644 index 00000000000..ab1f6b4689f --- /dev/null +++ b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.ts @@ -0,0 +1,500 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { CipherType, FieldType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; + +import { ImportResult } from "../../models/import-result"; +import { BaseImporter } from "../base-importer"; +import { Importer } from "../importer"; + +import { PasswordDepotItemType, PasswordDepotCustomFieldType } from "./types"; + +/** + * Importer for Password Depot 17 xml files. + * @see https://www.password-depot.de/ + * It provides methods to parse the XML data, extract relevant information, and create cipher objects + */ +export class PasswordDepot17XmlImporter extends BaseImporter implements Importer { + result = new ImportResult(); + + _favouritesLookupTable = new Set(); + + // Parse the XML data from the Password Depot export file and extracts the relevant information + parse(data: string): Promise { + const doc: XMLDocument = this.parseXml(data); + if (doc == null) { + this.result.success = false; + return Promise.resolve(this.result); + } + + // Check if the root node is present + const rootNode = doc.querySelector("passwordfile"); + if (rootNode == null) { + this.result.errorMessage = "Missing `passwordfile` node."; + this.result.success = false; + return Promise.resolve(this.result); + } + + // Check if the version is supported + const headerNode = this.querySelectorDirectChild(rootNode, "header"); + if (headerNode == null) { + this.result.success = false; + return Promise.resolve(this.result); + } + + let versionNode = this.querySelectorDirectChild(headerNode, "version"); + if (versionNode == null) { + // Adding a fallback for MacOS Password Depot 17.0 export files + // These files do not have a version node, but a dataformat node instead + versionNode = this.querySelectorDirectChild(headerNode, "dataformat"); + if (versionNode == null) { + this.result.success = false; + return Promise.resolve(this.result); + } + } + + const version = versionNode.textContent; + if (!version.startsWith("17")) { + this.result.errorMessage = "Unsupported export version detected - (only 17.0 is supported)"; + this.result.success = false; + return Promise.resolve(this.result); + } + + // Abort import if the file is encrypted + const encryptedNode = this.querySelectorDirectChild(headerNode, "encrypted"); + if (encryptedNode != null && encryptedNode.textContent == "True") { + this.result.errorMessage = "Encrypted Password Depot files are not supported."; + this.result.success = false; + return Promise.resolve(this.result); + } + + // Check if the passwords node is present + // This node contains all the password entries + const passwordsNode = rootNode.querySelector("passwords"); + if (passwordsNode == null) { + this.result.errorMessage = "Missing `passwordfile > passwords` node."; + this.result.success = false; + return Promise.resolve(this.result); + } + + this.buildFavouritesLookupTable(rootNode); + + this.querySelectorAllDirectChild(passwordsNode, "group").forEach((group) => { + this.traverse(group, ""); + }); + + if (this.organization) { + this.moveFoldersToCollections(this.result); + } + + this.result.success = true; + return Promise.resolve(this.result); + } + + // Traverses the XML tree and processes each node + // It starts from the root node and goes through each group and item + // This method is recursive and handles nested groups + private traverse(node: Element, groupPrefixName: string) { + const folderIndex = this.result.folders.length; + let groupName = groupPrefixName; + + if (groupName !== "") { + groupName += "/"; + } + + // Check if the group has a fingerprint attribute (GUID of a folder) + const groupFingerprint = node.attributes.getNamedItem("fingerprint"); + if (groupFingerprint?.textContent != "" && groupFingerprint.textContent != "null") { + const nameEl = node.attributes.getNamedItem("name"); + groupName += nameEl == null ? "-" : nameEl.textContent; + const folder = new FolderView(); + folder.name = groupName; + this.result.folders.push(folder); + } + + this.querySelectorAllDirectChild(node, "item").forEach((entry) => { + const cipherIndex = this.result.ciphers.length; + + const cipher = this.initLoginCipher(); + //Set default item type similar how we default to Login in other importers + let sourceType: PasswordDepotItemType = PasswordDepotItemType.Password; + + const entryFields = entry.children; + for (let i = 0; i < entryFields.length; i++) { + const entryField = entryFields[i]; + + // Skip processing historical entries + if (entryField.tagName === "hitems") { + continue; + } + + if (entryField.tagName === "description") { + cipher.name = entryField.textContent; + continue; + } + + if (entryField.tagName === "comment") { + cipher.notes = entryField.textContent; + continue; + } + + if (entryField.tagName === "type") { + sourceType = entryField.textContent as PasswordDepotItemType; + switch (sourceType) { + case PasswordDepotItemType.Password: + case PasswordDepotItemType.RDP: + case PasswordDepotItemType.Putty: + case PasswordDepotItemType.TeamViewer: + case PasswordDepotItemType.Banking: + case PasswordDepotItemType.Certificate: + case PasswordDepotItemType.EncryptedFile: + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + break; + case PasswordDepotItemType.CreditCard: + cipher.type = CipherType.Card; + cipher.card = new CardView(); + break; + case PasswordDepotItemType.SoftwareLicense: + case PasswordDepotItemType.Information: + case PasswordDepotItemType.Document: + cipher.type = CipherType.SecureNote; + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + break; + case PasswordDepotItemType.Identity: + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); + break; + } + continue; + } + + if ( + sourceType === PasswordDepotItemType.Password || + sourceType === PasswordDepotItemType.RDP || + sourceType === PasswordDepotItemType.Putty || + sourceType === PasswordDepotItemType.TeamViewer || + sourceType === PasswordDepotItemType.Banking || + sourceType === PasswordDepotItemType.Certificate || + sourceType === PasswordDepotItemType.EncryptedFile + ) { + if (this.parseLoginFields(entryField, cipher)) { + continue; + } + } + + // fingerprint is the GUID of the entry + // Base on the previously parsed favourites, we can identify an entry and set the favorite flag accordingly + if (entryField.tagName === "fingerprint") { + if (this._favouritesLookupTable.has(entryField.textContent)) { + cipher.favorite = true; + } + } + + if (entryField.tagName === "customfields") { + this.parseCustomFields(entryField, sourceType, cipher); + continue; + } + + if (sourceType === PasswordDepotItemType.Banking && entryField.tagName === "tans") { + this.querySelectorAllDirectChild(entryField, "tan").forEach((tanEntry) => { + this.parseBankingTANs(tanEntry, cipher); + }); + continue; + } + + this.processKvp(cipher, entryField.tagName, entryField.textContent, FieldType.Text); + } + + this.cleanupCipher(cipher); + this.result.ciphers.push(cipher); + + if (groupName !== "") { + this.result.folderRelationships.push([cipherIndex, folderIndex]); + } + }); + + this.querySelectorAllDirectChild(node, "group").forEach((group) => { + this.traverse(group, groupName); + }); + } + + // Parses custom fields and adds them to the cipher + // It iterates through all the custom fields and adds them to the cipher + private parseCustomFields( + entryField: Element, + sourceType: PasswordDepotItemType, + cipher: CipherView, + ) { + this.querySelectorAllDirectChild(entryField, "field").forEach((customField) => { + const customFieldObject = this.parseCustomField(customField); + if (customFieldObject == null) { + return; + } + + switch (sourceType) { + case PasswordDepotItemType.CreditCard: + if (this.parseCreditCardCustomFields(customFieldObject, cipher)) { + return; + } + break; + case PasswordDepotItemType.Identity: + if (this.parseIdentityCustomFields(customFieldObject, cipher)) { + return; + } + break; + case PasswordDepotItemType.Information: + if (this.parseInformationCustomFields(customFieldObject, cipher)) { + return; + } + break; + default: + // For other types, we will process the custom field as a regular key-value pair + break; + } + + this.processKvp( + cipher, + customFieldObject.name, + customFieldObject.value, + customFieldObject.type, + ); + }); + } + + // Parses login fields and adds them to the cipher + private parseLoginFields(entryField: Element, cipher: CipherView): boolean { + if (entryField.tagName === "username") { + cipher.login.username = entryField.textContent; + return true; + } + + if (entryField.tagName === "password") { + cipher.login.password = entryField.textContent; + return true; + } + + if (entryField.tagName === "url") { + cipher.login.uris = this.makeUriArray(entryField.textContent); + return true; + } + + return false; + } + + // Parses a custom field and adds it to the cipher + private parseCustomField(customField: Element): FieldView | null { + let key: string = undefined; + let value: string = undefined; + let sourceFieldType: PasswordDepotCustomFieldType = PasswordDepotCustomFieldType.Memo; + let visible: string = undefined; + // A custom field is represented by a element + // On exports from the Windows clients: It contains a , , and optionally a and element + // On exports from the MacOs clients the key-values are defined as xml attributes instead of child nodes + if (customField.hasAttributes()) { + key = customField.getAttribute("name"); + if (key == null) { + return null; + } + + value = customField.getAttribute("value"); + + const typeAttr = customField.getAttribute("type"); + sourceFieldType = + typeAttr != null + ? (typeAttr as PasswordDepotCustomFieldType) + : PasswordDepotCustomFieldType.Memo; + + visible = customField.getAttribute("visible"); + } else { + const keyEl = this.querySelectorDirectChild(customField, "name"); + key = keyEl != null ? keyEl.textContent : null; + + if (key == null) { + return null; + } + + const valueEl = this.querySelectorDirectChild(customField, "value"); + value = valueEl != null ? valueEl.textContent : null; + + const typeEl = this.querySelectorDirectChild(customField, "type"); + sourceFieldType = + typeEl != null + ? (typeEl.textContent as PasswordDepotCustomFieldType) + : PasswordDepotCustomFieldType.Memo; + + const visibleEl = this.querySelectorDirectChild(customField, "visible"); + visible = visibleEl != null ? visibleEl.textContent : null; + } + + if (sourceFieldType === PasswordDepotCustomFieldType.Date) { + if (!isNaN(value as unknown as number)) { + // Convert excel date format to JavaScript date + const numericValue = parseInt(value); + const secondsInDay = 86400; + const missingLeapYearDays = secondsInDay * 1000; + value = new Date((numericValue - (25567 + 2)) * missingLeapYearDays).toLocaleDateString(); + } + } + + if (sourceFieldType === PasswordDepotCustomFieldType.Password) { + return { name: key, value: value, type: FieldType.Hidden, linkedId: null } as FieldView; + } + + if (sourceFieldType === PasswordDepotCustomFieldType.Boolean) { + return { name: key, value: value, type: FieldType.Boolean, linkedId: null } as FieldView; + } + + if (visible == "0") { + return { name: key, value: value, type: FieldType.Hidden, linkedId: null } as FieldView; + } + + return { name: key, value: value, type: FieldType.Text, linkedId: null } as FieldView; + } + + // Parses credit card fields and adds them to the cipher + private parseCreditCardCustomFields(customField: FieldView, cipher: CipherView): boolean { + if (customField.name === "IDS_CardHolder") { + cipher.card.cardholderName = customField.value; + return true; + } + + if (customField.name === "IDS_CardNumber") { + cipher.card.number = customField.value; + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); + return true; + } + + if (customField.name === "IDS_CardExpires") { + this.setCardExpiration(cipher, customField.value); + return true; + } + + if (customField.name === "IDS_CardCode") { + cipher.card.code = customField.value; + return true; + } + + return false; + } + + // Parses identity fields and adds them to the cipher + private parseIdentityCustomFields(customField: FieldView, cipher: CipherView): boolean { + if (customField.name === "IDS_IdentityName") { + this.processFullName(cipher, customField.value); + return true; + } + + if (customField.name === "IDS_IdentityEmail") { + cipher.identity.email = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityFirstName") { + cipher.identity.firstName = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityLastName") { + cipher.identity.lastName = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityCompany") { + cipher.identity.company = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityAddress1") { + cipher.identity.address1 = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityAddress2") { + cipher.identity.address2 = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityCity") { + cipher.identity.city = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityState") { + cipher.identity.state = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityZIP") { + cipher.identity.postalCode = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityCountry") { + cipher.identity.country = customField.value; + return true; + } + + if (customField.name === "IDS_IdentityPhone") { + cipher.identity.phone = customField.value; + return true; + } + + return false; + } + + // Parses information custom fields and adds them to the cipher + private parseInformationCustomFields(customField: FieldView, cipher: CipherView): boolean { + if (customField.name === "IDS_InformationText") { + cipher.notes = customField.value; + return true; + } + + return false; + } + + // Parses TAN objects and adds them to the cipher + // It iterates through all the TAN fields and adds them to the cipher + private parseBankingTANs(TANsField: Element, cipher: CipherView) { + let tanNumber = "0"; + const entryFields = TANsField.children; + for (let i = 0; i < entryFields.length; i++) { + const entryField = entryFields[i]; + + if (entryField.tagName === "number") { + tanNumber = entryField.textContent; + continue; + } + + this.processKvp(cipher, `tan_${tanNumber}_${entryField.tagName}`, entryField.textContent); + } + } + + // Parses the favourites-node from the XML file, which contains a base64 encoded string + // The string contains the fingerprints/GUIDs of the favourited entries, separated by new lines + private buildFavouritesLookupTable(rootNode: Element): void { + const favouritesNode = this.querySelectorDirectChild(rootNode, "favorites"); + if (favouritesNode == null) { + return; + } + + const decodedBase64String = atob(favouritesNode.textContent); + if (decodedBase64String.indexOf("\r\n") > 0) { + decodedBase64String.split("\r\n").forEach((line) => { + this._favouritesLookupTable.add(line); + }); + return; + } + + decodedBase64String.split("\n").forEach((line) => { + this._favouritesLookupTable.add(line); + }); + } +} diff --git a/libs/importer/src/importers/password-depot/types/index.ts b/libs/importer/src/importers/password-depot/types/index.ts new file mode 100644 index 00000000000..709e5992ae8 --- /dev/null +++ b/libs/importer/src/importers/password-depot/types/index.ts @@ -0,0 +1,2 @@ +export * from "./password-depot-item-type"; +export * from "./password-depot-custom-field-type"; diff --git a/libs/importer/src/importers/password-depot/types/password-depot-custom-field-type.ts b/libs/importer/src/importers/password-depot/types/password-depot-custom-field-type.ts new file mode 100644 index 00000000000..166378b38b4 --- /dev/null +++ b/libs/importer/src/importers/password-depot/types/password-depot-custom-field-type.ts @@ -0,0 +1,15 @@ +/** This object represents the different custom field types in Password Depot */ +export const PasswordDepotCustomFieldType = Object.freeze({ + Password: "1", + Memo: "2", + Date: "3", + Number: "4", + Boolean: "5", + Decimal: "6", + Email: "7", + URL: "8", +} as const); + +/** This type represents the different custom field types in Password Depot */ +export type PasswordDepotCustomFieldType = + (typeof PasswordDepotCustomFieldType)[keyof typeof PasswordDepotCustomFieldType]; diff --git a/libs/importer/src/importers/password-depot/types/password-depot-item-type.ts b/libs/importer/src/importers/password-depot/types/password-depot-item-type.ts new file mode 100644 index 00000000000..04ec3a48c85 --- /dev/null +++ b/libs/importer/src/importers/password-depot/types/password-depot-item-type.ts @@ -0,0 +1,19 @@ +/** This object represents the different item types in Password Depot */ +export const PasswordDepotItemType = Object.freeze({ + Password: "0", + CreditCard: "1", + SoftwareLicense: "2", + Identity: "3", + Information: "4", + Banking: "5", + EncryptedFile: "6", + Document: "7", + RDP: "8", + Putty: "9", + TeamViewer: "10", + Certificate: "11", +} as const); + +/** This type represents the different item types in Password Depot */ +export type PasswordDepotItemType = + (typeof PasswordDepotItemType)[keyof typeof PasswordDepotItemType]; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/banking.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/banking.xml.ts new file mode 100644 index 00000000000..3a2da1c27bf --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/banking.xml.ts @@ -0,0 +1,283 @@ +export const BankingTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + banking type + 5 + somePassword + someUser + some-bank.com + someNote + 12.05.2025 15:11:29 + 02.05.2027 + 1 + DEB91652-52C6-402E-9D44-3557829BC6DF + + 127 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:09:17 + 12.05.2025 15:09:17 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_ECHolder + account holder + -1 + 0 + 0 + + + IDS_ECAccountNumber + 1234567890 + -1 + 0 + 0 + + + IDS_ECBLZ + 12345678 + -1 + 0 + 0 + + + IDS_ECBankName + someBank + -1 + 0 + 0 + + + IDS_ECBIC + bic + -1 + 0 + 0 + + + IDS_ECIBAN + iban + -1 + 0 + 0 + + + IDS_ECCardNumber + 12345678 + -1 + 0 + 10 + + + IDS_ECPhone + 0049 + 0 + 0 + 0 + + + IDS_ECLegitimacyID + 1234 + -1 + 0 + 0 + + + IDS_ECPIN + 123 + 0 + 0 + 1 + + + + + 1 + 1234 + 12.05.2025 15:10:54 + 100,00 + some TAN note + 123 + + + 2 + 4321 + 0,00 + + + + + + + banking type + 5 + somePassword + someUser + some-bank.com + someNote + 12.05.2025 15:10:35 + 02.05.2027 + 1 + DEB91652-52C6-402E-9D44-3557829BC6DF + + 127 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:09:17 + 12.05.2025 15:09:17 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_ECHolder + account holder + -1 + 0 + 0 + + + IDS_ECAccountNumber + 1234567890 + -1 + 0 + 0 + + + IDS_ECBLZ + 12345678 + -1 + 0 + 0 + + + IDS_ECBankName + someBank + -1 + 0 + 0 + + + IDS_ECBIC + bic + -1 + 0 + 0 + + + IDS_ECIBAN + iban + -1 + 0 + 0 + + + IDS_ECCardNumber + 12345678 + -1 + 0 + 10 + + + IDS_ECPhone + 0049 + 0 + 0 + 0 + + + IDS_ECLegitimacyID + 1234 + -1 + 0 + 0 + + + IDS_ECPIN + 123 + 0 + 0 + 1 + + + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/certificate.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/certificate.xml.ts new file mode 100644 index 00000000000..eb8c463d57f --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/certificate.xml.ts @@ -0,0 +1,76 @@ +export const CertificateTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + certificate type + 11 + somePassword + + + someNote + 12.05.2025 15:15:57 + 00.00.0000 + 1 + 21288702-B042-46D9-9DDF-B44A5CD04A72 + + 130 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:15:26 + 12.05.2025 15:15:26 + 0 + 0 + 0 + 1 + + 0 + 2 + someTag + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/credit-card.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/credit-card.xml.ts new file mode 100644 index 00000000000..f0af49bbfae --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/credit-card.xml.ts @@ -0,0 +1,148 @@ +export const CreditCardTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + some CreditCard + 1 + 4222422242224222 + some CC holder + + someComment + 08.05.2025 12:09:47 + 08.05.2026 + 1 + DD9B52F8-B2CE-42C2-A891-5E20DA23EA20 + + 126 + 0 + + + + + 0 + + + 08.05.2025 12:08:48 + 08.05.2025 12:08:48 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_CardType + 0 + 0 + 0 + 4 + + + IDS_CardHolder + some CC holder + -1 + 0 + 0 + + + IDS_CardNumber + 4222422242224222 + -1 + 0 + 10 + + + IDS_CardExpires + 05/2026 + -1 + 0 + 3 + + + IDS_CardCode + 123 + -1 + 0 + 0 + + + IDS_CardPhone + + -1 + 0 + 0 + + + IDS_CardURL + + -1 + 0 + 8 + + + IDS_CardAdditionalCode + + -1 + 0 + 0 + + + IDS_CardAdditionalInfo + + -1 + 0 + 0 + + + IDS_CardPIN + 123 + -1 + 0 + 1 + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/document.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/document.xml.ts new file mode 100644 index 00000000000..4f607c9b048 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/document.xml.ts @@ -0,0 +1,99 @@ +export const DocumentTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + document type + 7 + + + + document comment + 03.06.2025 17:45:30 + 00.00.0000 + 1 + 1B8E7F2C-9229-43C6-AB89-42101809C822 + + 133 + 0 + + + Allgemein + + 0 + + + 05.06.2025 21:49:49 + 30.12.1899 00:00:00 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_DocumentSize + 27071 + 0 + 0 + 4 + + + IDS_DocumentFolder + C:\\Users\\DJSMI\\Downloads\\ + 0 + 0 + 0 + + + IDS_DocumentFile + C:\\Users\\DJSMI\\Downloads\\some.pdf + 0 + 0 + 0 + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/encrypted-file.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/encrypted-file.xml.ts new file mode 100644 index 00000000000..2d2e929440a --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/encrypted-file.xml.ts @@ -0,0 +1,76 @@ +export const EncryptedFileTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + encrypted file type + 6 + somePassword + + + some comment + 12.05.2025 15:15:17 + 00.00.0000 + 1 + E4CA245D-A326-4359-9488-CC207B33C6C0 + + 132 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:14:58 + 12.05.2025 15:14:58 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/identity.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/identity.xml.ts new file mode 100644 index 00000000000..dfa275aa778 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/identity.xml.ts @@ -0,0 +1,197 @@ +export const IdentityTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + identity type + 3 + + account-name/id + website + someNote + 12.05.2025 15:14:33 + 00.00.0000 + 1 + 0E6085E9-7560-4826-814E-EFE1724E1377 + + 129 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:12:52 + 12.05.2025 15:12:52 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_IdentityName + account-name/id + -1 + 0 + 0 + + + IDS_IdentityEmail + email + -1 + 0 + 0 + + + IDS_IdentityFirstName + firstName + -1 + 0 + 0 + + + IDS_IdentityLastName + surName + -1 + 0 + 0 + + + IDS_IdentityCompany + someCompany + -1 + 0 + 0 + + + IDS_IdentityAddress1 + someStreet + -1 + 0 + 0 + + + IDS_IdentityAddress2 + address 2 + -1 + 0 + 0 + + + IDS_IdentityCity + town + -1 + 0 + 0 + + + IDS_IdentityState + state + -1 + 0 + 0 + + + IDS_IdentityZIP + zipCode + -1 + 0 + 0 + + + IDS_IdentityCountry + country + -1 + 0 + 0 + + + IDS_IdentityPhone + phoneNumber + -1 + 0 + 0 + + + IDS_IdentityWebsite + website + -1 + 0 + 8 + + + IDS_IdentityBirthDate + 45789 + -1 + 0 + 3 + + + IDS_IdentityMobile + mobileNumber + -1 + 0 + 0 + + + IDS_IdentityFax + faxNumber + -1 + 0 + 0 + + + IDS_IdentityHouseNumber + 123 + -1 + 0 + 0 + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/index.ts b/libs/importer/src/importers/spec-data/password-depot-xml/index.ts new file mode 100644 index 00000000000..6d5903ed471 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/index.ts @@ -0,0 +1,19 @@ +export { InvalidRootNodeData } from "./missing-root-node.xml"; +export { MissingPasswordsNodeData } from "./missing-passwords-node.xml"; +export { InvalidVersionData } from "./wrong-version.xml"; +export { EncryptedFileData } from "./noop-encrypted-file.xml"; +export { PasswordTestData } from "./password.xml"; +export { CreditCardTestData } from "./credit-card.xml"; +export { IdentityTestData } from "./identity.xml"; +export { RDPTestData } from "./rdp.xml"; +export { SoftwareLicenseTestData } from "./software-license.xml"; +export { TeamViewerTestData } from "./team-viewer.xml"; +export { PuttyTestData } from "./putty.xml"; +export { BankingTestData } from "./banking.xml"; +export { InformationTestData } from "./information.xml"; +export { CertificateTestData } from "./certificate.xml"; +export { EncryptedFileTestData } from "./encrypted-file.xml"; +export { DocumentTestData } from "./document.xml"; +export { MacOS_WrongVersion } from "./macos-wrong-version.xml"; +export { MacOS_PasswordDepotXmlFile } from "./macos-customfields.xml"; +export { MacOS_MultipleFolders } from "./macos-multiple-folders.xml"; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/information.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/information.xml.ts new file mode 100644 index 00000000000..1f07882ea64 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/information.xml.ts @@ -0,0 +1,85 @@ +export const InformationTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + information type + 4 + + + + + 12.05.2025 15:14:54 + 00.00.0000 + 1 + 546AFAE7-6F64-4040-838B-AFE691580356 + + 131 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:14:39 + 12.05.2025 15:14:39 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_InformationText + some note content + 0 + 0 + 2 + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/macos-customfields.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/macos-customfields.xml.ts new file mode 100644 index 00000000000..d83eae6bb6d --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/macos-customfields.xml.ts @@ -0,0 +1,42 @@ +export const MacOS_PasswordDepotXmlFile = ` + +
+ Password Depot + 0 + 0 + 0 + 1 + 1 + 17 + 30.12.1899 00:00:00 + 23.06.2025 16:30:50 + 25.06.2025 14:30:47 + 2C1A154A-3BB0-4871-9537-3634DE303F8E + 7 +
+ + + + card 1 + 1 + comment + 23.06.2025 16:14:33 + EBF4AC3D-86C9-49BE-826B-BAE5FF9E3575 + 23.06.2025 16:13:40 + 25.06.2025 14:17:10 + + + + + + + + + + + + + + + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/macos-multiple-folders.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/macos-multiple-folders.xml.ts new file mode 100644 index 00000000000..174e9415fa1 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/macos-multiple-folders.xml.ts @@ -0,0 +1,215 @@ +export const MacOS_MultipleFolders = ` + +
+ Password Depot + 0 + 0 + 0 + 1 + 1 + 17 + 30.12.1899 00:00:00 + 27.06.2025 10:39:07 + 27.06.2025 10:39:27 + 7DCDD3FA-F512-4CD4-AEED-DE2A4C8375CF + 7 +
+ + + + remote desktop + 8 + pass + username + compjter + comment + 26.06.2025 16:04:57 + 81316050-9B9C-4D9B-9549-45B52A0BE6BB + + Private + 26.06.2025 16:04:32 + 27.06.2025 10:28:02 + tgmac + + + teamviewer + 10 + pass + partnerid + comment + 26.06.2025 16:05:28 + 26.06.2025 + 8AAACC16-4FD4-4E52-9C1F-6979B051B510 + Internet + 26.06.2025 16:05:03 + 27.06.2025 10:28:02 + tag + + + + + + ec card + 5 + pass + user + url + 26.06.2025 16:08:35 + B5C148A4-C408-427C-B69C-F88E7C529FA4 + + 26.06.2025 16:08:00 + 27.06.2025 10:28:02 + + + + + + + + + + + + + + + identity + 3 + 26.06.2025 16:09:50 + 87574AD4-8844-4A01-9381-AFF0907198A3 + 26.06.2025 16:09:19 + 27.06.2025 10:28:02 + + + + + + + + + + + + + + + + + + + + + + credit card + 1 + comment + 26.06.2025 19:07:38 + E98E3CBA-1578-48AD-8E41-CFD3280045BB + 26.06.2025 16:06:32 + 27.06.2025 10:28:02 + + + + + + + + + + + + + + + + password + passmac + usernam + comment + 26.06.2025 16:04:30 + DE8AD61B-8EC0-4E72-9BC8-971E80712B50 + + General + 26.06.2025 16:04:04 + 27.06.2025 10:28:02 + tagmac + + + informationb + 4 + 26.06.2025 16:10:01 + 7E9E6941-BB3B-47F2-9E43-33F900EBBF95 + Banking + 26.06.2025 16:09:53 + 27.06.2025 10:28:02 + + + + + + certificat + 11 + passsss + comment + 26.06.2025 16:10:28 + 26.06.2025 + 1F36748F-0374-445E-B020-282EAE26259F + Internet + 26.06.2025 16:10:10 + 27.06.2025 10:28:02 + tag + + + + + putty + 9 + pass + username + host + comment + 26.06.2025 16:06:08 + 26.06.2025 + 7947A949-98F0-4F26-BE12-5FFAFE7601C8 + + Banking + 26.06.2025 16:05:38 + 27.06.2025 10:28:02 + tag + + + + + + + + + soft license + 2 + 26.06.2025 16:09:02 + 2A5CF4C1-70D0-4F27-A1DE-4CFEF5FB71CF + 26.06.2025 16:08:43 + 27.06.2025 10:28:02 + + + + + + + + + + + + + + + + + + + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/macos-wrong-version.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/macos-wrong-version.xml.ts new file mode 100644 index 00000000000..771bf813e48 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/macos-wrong-version.xml.ts @@ -0,0 +1,21 @@ +export const MacOS_WrongVersion = ` + +
+ Password Depot + 0 + 0 + 0 + 1 + 1 + 18 + 30.12.1899 00:00:00 + 23.06.2025 16:30:50 + 25.06.2025 14:30:47 + 2C1A154A-3BB0-4871-9537-3634DE303F8E + 7 +
+ + + + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/missing-passwords-node.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/missing-passwords-node.xml.ts new file mode 100644 index 00000000000..d07beb8521c --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/missing-passwords-node.xml.ts @@ -0,0 +1,25 @@ +export const MissingPasswordsNodeData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + + + + + + + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/missing-root-node.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/missing-root-node.xml.ts new file mode 100644 index 00000000000..aca2a2f6fa1 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/missing-root-node.xml.ts @@ -0,0 +1,28 @@ +export const InvalidRootNodeData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + + + + + + + + + + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/noop-encrypted-file.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/noop-encrypted-file.xml.ts new file mode 100644 index 00000000000..e8050726b25 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/noop-encrypted-file.xml.ts @@ -0,0 +1,27 @@ +export const EncryptedFileData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + True + 0 + 30.12.1899 00:00:00 + +
+ + + + + + + + + + + + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/password.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/password.xml.ts new file mode 100644 index 00000000000..7d15fce3aa8 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/password.xml.ts @@ -0,0 +1,222 @@ +export const PasswordTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + password type + 0 + p6J<]fmjv!:H&iJ7/Mwt@3i8 + someUser + example.com + someComment + 07.05.2025 13:37:56 + 07.05.2025 + 0 + 27ACAC2D-8DDA-4088-8D3A-E6C5F40ED46E + + 0 + 0 + + + Allgemein + + 0 + + + 07.05.2025 13:36:48 + 07.05.2025 13:36:48 + 0 + 0 + 0 + 1 + + 0 + 2 + someTag + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + passwort + password + -1 + 0 + 1 + + + memo + memo + -1 + 0 + 2 + + + datum + 45790 + -1 + 0 + 3 + + + nummer + 1 + -1 + 0 + 4 + + + boolean + 1 + -1 + 0 + 5 + + + decimal + 1,01 + -1 + 0 + 6 + + + email + who@cares.com + -1 + 0 + 7 + + + url + example.com + -1 + 0 + 8 + + + + + password type (2) + 0 + p6J<]fmjv!:H&iJ7/Mwt@3i8 + someUser + + someComment + 07.05.2025 13:37:56 + 07.05.2025 + 0 + AF74FF86-FE39-4584-8E96-FE950C289DF8 + + 0 + 0 + + + Allgemein + + 0 + + + 07.05.2025 13:36:48 + 07.05.2025 13:36:48 + 0 + 0 + 0 + 1 + + 0 + 2 + someTag + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + password type (3) + 0 + p6J<]fmjv!:H&iJ7/Mwt@3i8 + someUser + + someComment + 07.05.2025 13:37:56 + 07.05.2025 + 0 + BF74FF86-FA39-4584-8E96-FA950C249DF8 + + 0 + 0 + + + Allgemein + + 0 + + + 07.05.2025 13:36:48 + 07.05.2025 13:36:48 + 0 + 0 + 0 + 1 + + 0 + 2 + someTag + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4CkJGNzRGRjg2LUZBMzktNDU4NC04RTk2LUZBOTUwQzI0OURGOA== + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/putty.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/putty.xml.ts new file mode 100644 index 00000000000..d878b04cd3c --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/putty.xml.ts @@ -0,0 +1,106 @@ +export const PuttyTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + Putty type + 9 + somePassword + someUser + localhost + someNote + 12.05.2025 15:09:09 + 00.00.0000 + 1 + 32207D79-B70B-4987-BC73-3F7AD75D2C63 + + 125 + 0 + + cli command + Allgemein + + 0 + + + 12.05.2025 15:08:18 + 12.05.2025 15:08:18 + 0 + 0 + 0 + 1 + + 0 + 2 + someTag + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_PuTTyProtocol + 0 + 0 + 0 + 0 + + + IDS_PuTTyKeyFile + pathToKeyFile + -1 + 0 + 0 + + + IDS_PuTTyKeyPassword + passwordForKeyFile + -1 + 0 + 1 + + + IDS_PuTTyPort + 8080 + -1 + 0 + 4 + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/rdp.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/rdp.xml.ts new file mode 100644 index 00000000000..fafc375f9a6 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/rdp.xml.ts @@ -0,0 +1,76 @@ +export const RDPTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + rdp type + 8 + somePassword + someUser + ms-rd:subscribe?url=https://contoso.com + someNote + 12.05.2025 15:07:33 + 12.05.2025 + 1 + 24CFF328-3036-48E3-99A3-85CD337725D3 + + 123 + 0 + + commandline command + Allgemein + + 0 + + + 12.05.2025 15:06:24 + 12.05.2025 15:06:24 + 0 + 0 + 0 + 1 + + 0 + 2 + sometag + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/software-license.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/software-license.xml.ts new file mode 100644 index 00000000000..5ab9437c3d7 --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/software-license.xml.ts @@ -0,0 +1,169 @@ +export const SoftwareLicenseTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + software-license type + 2 + somePassword + someUserName + example.com + someComment + 12.05.2025 15:12:48 + 00.00.0000 + 1 + 220206EB-BE82-4E78-8FFB-9316D854721F + + 128 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:11:33 + 12.05.2025 15:11:33 + 0 + 0 + 0 + 1 + + 0 + 2 + + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_LicenseProduct + someProduct + 0 + 0 + 0 + + + IDS_LicenseVersion + someVersion + 0 + 0 + 0 + + + IDS_LicenseName + some User + -1 + 0 + 0 + + + IDS_LicenseKey + license-key + -1 + 0 + 0 + + + IDS_LicenseAdditionalKey + additional-license-key + -1 + 0 + 0 + + + IDS_LicenseURL + example.com + -1 + 0 + 8 + + + IDS_LicenseProtected + 1 + 0 + 0 + 5 + + + IDS_LicenseUserName + someUserName + -1 + 0 + 0 + + + IDS_LicensePassword + somePassword + -1 + 0 + 1 + + + IDS_LicensePurchaseDate + 45789 + 0 + 0 + 3 + + + IDS_LicenseOrderNumber + order number + -1 + 0 + 0 + + + IDS_LicenseEmail + someEmail + -1 + 0 + 7 + + + IDS_LicenseExpires + Nie + 0 + 0 + 3 + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/team-viewer.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/team-viewer.xml.ts new file mode 100644 index 00000000000..911c621c59a --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/team-viewer.xml.ts @@ -0,0 +1,85 @@ +export const TeamViewerTestData = ` + +
+ Password Depot + 17.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + TeamViewer type + 10 + somePassword + + partnerId + someNote + 12.05.2025 15:08:14 + 00.00.0000 + 1 + AE650032-5963-4D93-8E2E-69F216405C29 + + 124 + 0 + + + Allgemein + + 0 + + + 12.05.2025 15:07:41 + 12.05.2025 15:07:41 + 0 + 0 + 0 + 1 + + 0 + 2 + someTag + DJSMI + + + 0 + + 0 + + 0 + + 161 + 0 + + + + IDS_TeamViewerMode + 0 + 0 + 0 + 0 + + + + + + + + + + + QUY3NEZGODYtRkUzOS00NTg0LThFOTYtRkU5NTBDMjg5REY4DQo= + + + + + + QWxsZ2VtZWluDQpIb21lIEJhbmtpbmcNCkludGVybmV0DQpQcml2YXQNCldpbmRvd3MNCg== + +
`; diff --git a/libs/importer/src/importers/spec-data/password-depot-xml/wrong-version.xml.ts b/libs/importer/src/importers/spec-data/password-depot-xml/wrong-version.xml.ts new file mode 100644 index 00000000000..90b766ded1b --- /dev/null +++ b/libs/importer/src/importers/spec-data/password-depot-xml/wrong-version.xml.ts @@ -0,0 +1,28 @@ +export const InvalidVersionData = ` + +
+ Password Depot + 18.0.0 + + + CCDA8015-7F21-4344-BDF0-9EA6AF8AE31D + 12.05.2025 15:16:11 + False + 0 + 30.12.1899 00:00:00 + +
+ + + + + + + + + + + + + +
`; diff --git a/libs/importer/src/models/import-options.ts b/libs/importer/src/models/import-options.ts index a8c4b4e0a8a..205dbaf0198 100644 --- a/libs/importer/src/models/import-options.ts +++ b/libs/importer/src/models/import-options.ts @@ -73,6 +73,7 @@ export const regularImportOptions = [ { id: "passkyjson", name: "Passky (json)" }, { id: "passwordxpcsv", name: "Password XP (csv)" }, { id: "netwrixpasswordsecure", name: "Netwrix Password Secure (csv)" }, + { id: "passworddepot17xml", name: "Password Depot 17 (xml)" }, ] as const; export type ImportType = diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 3789ee7536c..2b9d2e490f7 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -90,6 +90,7 @@ import { YotiCsvImporter, ZohoVaultCsvImporter, PasswordXPCsvImporter, + PasswordDepot17XmlImporter, } from "../importers"; import { Importer } from "../importers/importer"; import { @@ -348,6 +349,8 @@ export class ImportService implements ImportServiceAbstraction { return new PasswordXPCsvImporter(); case "netwrixpasswordsecure": return new NetwrixPasswordSecureCsvImporter(); + case "passworddepot17xml": + return new PasswordDepot17XmlImporter(); default: return null; } diff --git a/libs/storage-test-utils/README.md b/libs/storage-test-utils/README.md new file mode 100644 index 00000000000..2be8817e402 --- /dev/null +++ b/libs/storage-test-utils/README.md @@ -0,0 +1,5 @@ +# storage-test-utils + +Owned by: platform + +Test tools for the storage library diff --git a/libs/storage-test-utils/eslint.config.mjs b/libs/storage-test-utils/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/storage-test-utils/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/storage-test-utils/jest.config.js b/libs/storage-test-utils/jest.config.js new file mode 100644 index 00000000000..a145b5b2f4c --- /dev/null +++ b/libs/storage-test-utils/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "storage-test-utils", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/storage-test-utils", +}; diff --git a/libs/storage-test-utils/package.json b/libs/storage-test-utils/package.json new file mode 100644 index 00000000000..22d83f2334e --- /dev/null +++ b/libs/storage-test-utils/package.json @@ -0,0 +1,11 @@ +{ + "name": "@bitwarden/storage-test-utils", + "version": "0.0.1", + "description": "Test tools for the storage library", + "private": true, + "type": "commonjs", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "GPL-3.0", + "author": "platform" +} diff --git a/libs/storage-test-utils/project.json b/libs/storage-test-utils/project.json new file mode 100644 index 00000000000..f1aad63c9e3 --- /dev/null +++ b/libs/storage-test-utils/project.json @@ -0,0 +1,33 @@ +{ + "name": "storage-test-utils", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/storage-test-utils/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/storage-test-utils", + "main": "libs/storage-test-utils/src/index.ts", + "tsConfig": "libs/storage-test-utils/tsconfig.lib.json", + "assets": ["libs/storage-test-utils/*.md"] + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/storage-test-utils/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/storage-test-utils/jest.config.js" + } + } + } +} diff --git a/libs/storage-test-utils/src/fake-storage.service.ts b/libs/storage-test-utils/src/fake-storage.service.ts new file mode 100644 index 00000000000..aa902cf0da8 --- /dev/null +++ b/libs/storage-test-utils/src/fake-storage.service.ts @@ -0,0 +1,119 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { Subject } from "rxjs"; + +import { + AbstractStorageService, + ObservableStorageService, + StorageUpdate, + StorageOptions, +} from "@bitwarden/storage-core"; + +const INTERNAL_KEY = "__internal__"; + +export class FakeStorageService implements AbstractStorageService, ObservableStorageService { + private store: Record; + private updatesSubject = new Subject(); + private _valuesRequireDeserialization = false; + + /** + * Returns a mock of a {@see AbstractStorageService} for asserting the expected + * amount of calls. It is not recommended to use this to mock implementations as + * they are not respected. + */ + mock: MockProxy; + + constructor(initial?: Record) { + this.store = initial ?? {}; + this.mock = mock(); + } + + /** + * Updates the internal store for this fake implementation, this bypasses any mock calls + * or updates to the {@link updates$} observable. + * @param store + */ + internalUpdateStore(store: Record) { + this.store = store; + } + + get internalStore() { + return this.store; + } + + internalUpdateValuesRequireDeserialization(value: boolean) { + this._valuesRequireDeserialization = value; + } + + get valuesRequireDeserialization(): boolean { + return this._valuesRequireDeserialization; + } + + get updates$() { + return this.updatesSubject.asObservable(); + } + + get(key: string, options?: StorageOptions): Promise { + // 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.mock.get(key, options); + const value = this.store[key] as T; + return Promise.resolve(value); + } + has(key: string, options?: StorageOptions): Promise { + // 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.mock.has(key, options); + return Promise.resolve(this.store[key] != null); + } + async save(key: string, obj: T, options?: StorageOptions): Promise { + // These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203 + // which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world. + if (typeof key !== "string" && typeof key !== "object") { + throw new TypeError( + `Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`, + ); + } + + // We don't throw this error because ElectronStorageService automatically detects this case + // and calls `delete()` instead of `set()`. + // if (typeof key !== "object" && obj === undefined) { + // throw new TypeError("Use `delete()` to clear values"); + // } + + if (this._containsReservedKey(key)) { + throw new TypeError( + `Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`, + ); + } + + // 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.mock.save(key, obj, options); + this.store[key] = obj; + this.updatesSubject.next({ key: key, updateType: "save" }); + } + remove(key: string, options?: StorageOptions): Promise { + // 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.mock.remove(key, options); + delete this.store[key]; + this.updatesSubject.next({ key: key, updateType: "remove" }); + return Promise.resolve(); + } + + private _containsReservedKey(key: string | Partial): boolean { + if (typeof key === "object") { + const firsKey = Object.keys(key)[0]; + + if (firsKey === INTERNAL_KEY) { + return true; + } + } + + if (typeof key !== "string") { + return false; + } + + return false; + } +} diff --git a/libs/storage-test-utils/src/index.ts b/libs/storage-test-utils/src/index.ts new file mode 100644 index 00000000000..dc5fdc1125b --- /dev/null +++ b/libs/storage-test-utils/src/index.ts @@ -0,0 +1 @@ +export * from "./fake-storage.service"; diff --git a/libs/storage-test-utils/src/storage-test-utils.spec.ts b/libs/storage-test-utils/src/storage-test-utils.spec.ts new file mode 100644 index 00000000000..c323d4ce386 --- /dev/null +++ b/libs/storage-test-utils/src/storage-test-utils.spec.ts @@ -0,0 +1,8 @@ +import * as lib from "./index"; + +describe("storage-test-utils", () => { + // This test will fail until something is exported from index.ts + it("should work", () => { + expect(lib).toBeDefined(); + }); +}); diff --git a/libs/storage-test-utils/tsconfig.json b/libs/storage-test-utils/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/storage-test-utils/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/storage-test-utils/tsconfig.lib.json b/libs/storage-test-utils/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/storage-test-utils/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.js", "src/**/*.spec.ts"] +} diff --git a/libs/storage-test-utils/tsconfig.spec.json b/libs/storage-test-utils/tsconfig.spec.json new file mode 100644 index 00000000000..901c72378dd --- /dev/null +++ b/libs/storage-test-utils/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../..//dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/libs/tools/send/send-ui/src/services/send-items.service.spec.ts b/libs/tools/send/send-ui/src/services/send-items.service.spec.ts index 77e3725e813..cf46c909da5 100644 --- a/libs/tools/send/send-ui/src/services/send-items.service.spec.ts +++ b/libs/tools/send/send-ui/src/services/send-items.service.spec.ts @@ -2,11 +2,11 @@ import { TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; import { BehaviorSubject, first, Subject } from "rxjs"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { mockAccountServiceWith } from "../../../../../common/spec"; diff --git a/libs/tools/send/send-ui/src/services/send-items.service.ts b/libs/tools/send/send-ui/src/services/send-items.service.ts index 1ade6f37f71..52e1e3d669e 100644 --- a/libs/tools/send/send-ui/src/services/send-items.service.ts +++ b/libs/tools/send/send-ui/src/services/send-items.service.ts @@ -14,11 +14,11 @@ import { tap, } from "rxjs"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { SendListFiltersService } from "./send-list-filters.service"; diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.spec.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.spec.ts new file mode 100644 index 00000000000..3b2573b8f94 --- /dev/null +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.spec.ts @@ -0,0 +1,161 @@ +import { TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + CipherService, + EncryptionContext, +} from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; +import { CipherType } from "@bitwarden/sdk-internal"; + +import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; + +import { DefaultCipherFormService } from "./default-cipher-form.service"; + +describe("DefaultCipherFormService", () => { + let service: DefaultCipherFormService; + let testBed: TestBed; + const cipherServiceMock = mock(); + + let markAsCompleteMock: jest.Mock; + let pendingTasks$: jest.Mock; + + beforeEach(() => { + markAsCompleteMock = jest.fn().mockResolvedValue(undefined); + pendingTasks$ = jest.fn().mockReturnValue(of([])); + cipherServiceMock.encrypt.mockResolvedValue({} as EncryptionContext); + + testBed = TestBed.configureTestingModule({ + providers: [ + { provide: CipherService, useValue: cipherServiceMock }, + { provide: TaskService, useValue: { markAsComplete: markAsCompleteMock, pendingTasks$ } }, + { + provide: AccountService, + useValue: { activeAccount$: of({ id: "user-1" as UserId } as Account) }, + }, + DefaultCipherFormService, + ], + }); + + service = testBed.inject(DefaultCipherFormService); + }); + + describe("markAssociatedTaskAsComplete", () => { + it("does not call markAsComplete when the cipher is not a login", async () => { + pendingTasks$.mockReturnValueOnce( + of([ + { + type: SecurityTaskType.UpdateAtRiskCredential, + cipherId: "cipher-1", + userId: "user-1" as UserId, + }, + ]), + ); + + const cardCipher = new CipherView(); + cardCipher.type = CipherType.Card; + cardCipher.id = "cipher-1"; + + await service.saveCipher(cardCipher, { + originalCipher: new Cipher(), + admin: false, + } as CipherFormConfig); + + expect(markAsCompleteMock).not.toHaveBeenCalled(); + }); + + it("does not call markAsComplete when there is no associated credential tasks", async () => { + pendingTasks$.mockReturnValueOnce(of([])); + + const originalCipher = new Cipher(); + originalCipher.type = CipherType.Login; + + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.id = "cipher-1"; + cipher.login = new LoginView(); + cipher.login.password = "password123"; + + cipherServiceMock.decrypt.mockResolvedValue({ + ...cipher, + login: { + ...cipher.login, + password: "newPassword123", + }, + } as CipherView); + + await service.saveCipher(cipher, { + originalCipher: originalCipher, + admin: false, + } as CipherFormConfig); + + expect(markAsCompleteMock).not.toHaveBeenCalled(); + }); + + it("does not call markAsComplete when the password has not changed", async () => { + pendingTasks$.mockReturnValueOnce( + of([ + { + type: SecurityTaskType.UpdateAtRiskCredential, + cipherId: "cipher-1", + userId: "user-1" as UserId, + }, + ]), + ); + + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.id = "cipher-1"; + cipher.login = new LoginView(); + cipher.login.password = "password123"; + + cipherServiceMock.decrypt.mockResolvedValue(cipher); + + await service.saveCipher(cipher, { + originalCipher: new Cipher(), + admin: false, + } as CipherFormConfig); + + expect(markAsCompleteMock).not.toHaveBeenCalled(); + }); + + it("calls markAsComplete when the cipher password has changed and there is an associated credential task", async () => { + pendingTasks$.mockReturnValueOnce( + of([ + { + type: SecurityTaskType.UpdateAtRiskCredential, + cipherId: "cipher-1", + userId: "user-1" as UserId, + }, + ]), + ); + + const cipher = new CipherView(); + cipher.type = CipherType.Login; + cipher.id = "cipher-1"; + cipher.login = new LoginView(); + cipher.login.password = "password123"; + + cipherServiceMock.decrypt.mockResolvedValue({ + ...cipher, + login: { + ...cipher.login, + password: "newPassword123", + }, + } as CipherView); + + await service.saveCipher(cipher, { + originalCipher: new Cipher(), + admin: false, + } as CipherFormConfig); + + expect(markAsCompleteMock).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 99f853d4c86..5228c85c3f7 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -1,13 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { inject, Injectable } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; import { CipherFormConfig } from "../abstractions/cipher-form-config.service"; import { CipherFormService } from "../abstractions/cipher-form.service"; @@ -20,6 +23,7 @@ function isSetEqual(a: Set, b: Set) { export class DefaultCipherFormService implements CipherFormService { private cipherService: CipherService = inject(CipherService); private accountService: AccountService = inject(AccountService); + private taskService: TaskService = inject(TaskService); async decryptCipher(cipher: Cipher): Promise { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); @@ -89,6 +93,8 @@ export class DefaultCipherFormService implements CipherFormService { } } + await this.markAssociatedTaskAsComplete(activeUserId, cipher, config); + // Its possible the cipher was made no longer available due to collection assignment changes // e.g. The cipher was moved to a collection that the user no longer has access to if (savedCipher == null) { @@ -97,4 +103,47 @@ export class DefaultCipherFormService implements CipherFormService { return await this.cipherService.decrypt(savedCipher, activeUserId); } + + /** + * When a cipher has an associated pending `UpdateAtRiskCredential` task + * and the password has changed, mark the task as complete. + */ + private async markAssociatedTaskAsComplete( + userId: UserId, + updatedCipher: CipherView, + config: CipherFormConfig, + ) { + const decryptedOriginalCipherCipher = await this.cipherService.decrypt( + config.originalCipher, + userId, + ); + + const associatedPendingTask = await firstValueFrom( + this.taskService + .pendingTasks$(userId) + .pipe( + map((tasks) => + tasks.find( + (task) => + task.type === SecurityTaskType.UpdateAtRiskCredential && + task.cipherId === updatedCipher.id, + ), + ), + ), + ); + + const passwordHasChanged = + updatedCipher.type === CipherType.Login && + updatedCipher.login.password && + updatedCipher.login.password !== decryptedOriginalCipherCipher?.login?.password; + + // When there is not an associated pending task or the password has not changed, + // no action needed-return early. + if (!associatedPendingTask || !passwordHasChanged) { + return; + } + + // If the cipher is a login and the password has changed, mark the associated task as complete + await this.taskService.markAsComplete(associatedPendingTask.id, userId); + } } diff --git a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html index 2a31cd01c3a..e74c0b06818 100644 --- a/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html +++ b/libs/vault/src/cipher-view/sshkey-sections/sshkey-view.component.html @@ -19,6 +19,7 @@ bitIconButton bitPasswordInputToggle data-testid="toggle-privateKey" + [(toggled)]="revealSshKey" >