mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge branch 'main' into feat/engines
This commit is contained in:
commit
d20beaa1fc
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@ -96,6 +96,12 @@ libs/logging @bitwarden/team-platform-dev
|
||||
libs/storage-test-utils @bitwarden/team-platform-dev
|
||||
libs/messaging @bitwarden/team-platform-dev
|
||||
libs/messaging-internal @bitwarden/team-platform-dev
|
||||
libs/serialization @bitwarden/team-platform-dev
|
||||
libs/guid @bitwarden/team-platform-dev
|
||||
libs/client-type @bitwarden/team-platform-dev
|
||||
libs/core-test-utils @bitwarden/team-platform-dev
|
||||
libs/state @bitwarden/team-platform-dev
|
||||
libs/state-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
|
||||
|
||||
5
.github/workflows/build-cli.yml
vendored
5
.github/workflows/build-cli.yml
vendored
@ -71,6 +71,7 @@ jobs:
|
||||
|
||||
- name: Get Node Version
|
||||
id: retrieve-node-version
|
||||
working-directory: ./
|
||||
run: |
|
||||
NODE_NVMRC=$(cat .nvmrc)
|
||||
NODE_VERSION=${NODE_NVMRC/v/''}
|
||||
@ -104,7 +105,7 @@ jobs:
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
_WIN_PKG_FETCH_VERSION: 20.11.1
|
||||
_WIN_PKG_FETCH_VERSION: 22.15.1
|
||||
_WIN_PKG_VERSION: 3.5
|
||||
permissions:
|
||||
contents: read
|
||||
@ -283,7 +284,7 @@ jobs:
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
_WIN_PKG_FETCH_VERSION: 20.11.1
|
||||
_WIN_PKG_FETCH_VERSION: 22.15.1
|
||||
_WIN_PKG_VERSION: 3.5
|
||||
steps:
|
||||
- name: Check out repo
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2025.7.0",
|
||||
"version": "2025.7.1",
|
||||
"scripts": {
|
||||
"build": "npm run build:chrome",
|
||||
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
|
||||
@ -3657,25 +3657,6 @@
|
||||
"thisRequestIsNoLongerValid": {
|
||||
"message": "This request is no longer valid."
|
||||
},
|
||||
"areYouTryingToAccessYourAccount": {
|
||||
"message": "Are you trying to access your account?"
|
||||
},
|
||||
"logInConfirmedForEmailOnDevice": {
|
||||
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "name@example.com"
|
||||
},
|
||||
"device": {
|
||||
"content": "$2",
|
||||
"example": "iOS"
|
||||
}
|
||||
}
|
||||
},
|
||||
"youDeniedALogInAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
|
||||
},
|
||||
"loginRequestHasAlreadyExpired": {
|
||||
"message": "Login request has already expired."
|
||||
},
|
||||
@ -5592,5 +5573,11 @@
|
||||
"wasmNotSupported": {
|
||||
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
|
||||
"description": "'WebAssembly' is a technical term and should not be translated."
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
}
|
||||
}
|
||||
|
||||
@ -548,7 +548,7 @@
|
||||
"message": "वॉल्ट खोजे"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "खोज रीसेट करें"
|
||||
},
|
||||
"edit": {
|
||||
"message": "संपादन करें"
|
||||
|
||||
@ -338,7 +338,7 @@
|
||||
"message": "Przejść do bitwarden.com?"
|
||||
},
|
||||
"bitwardenForBusiness": {
|
||||
"message": "Bitwarden dla biznesu"
|
||||
"message": "Bitwarden dla firm"
|
||||
},
|
||||
"bitwardenAuthenticator": {
|
||||
"message": "Bitwarden Authenticator"
|
||||
@ -347,16 +347,16 @@
|
||||
"message": "Bitwarden Authenticator umożliwia przechowywanie kluczy uwierzytelniających i generowanie kodów TOTP dla weryfikacji dwustopniowej. Dowiedz się wiecej na bitwarden.com"
|
||||
},
|
||||
"bitwardenSecretsManager": {
|
||||
"message": "Menedżer sekretów Bitwarden"
|
||||
"message": "Bitwarden Secrets Manager"
|
||||
},
|
||||
"continueToSecretsManagerPageDesc": {
|
||||
"message": "Bezpiecznie przechowuj, zarządzaj i udostępniaj sekrety programistów z Menedżerem sekretów Bitwarden. Dowiedz się więcej na stronie bitwarden.com."
|
||||
"message": "Bezpiecznie przechowuj, zarządzaj i udostępniaj sekrety deweloperów za pomocą usługi Bitwarden Secrets Manager. Dowiedz się więcej na stronie bitwarden.com."
|
||||
},
|
||||
"passwordlessDotDev": {
|
||||
"message": "Passwordless.dev"
|
||||
},
|
||||
"continueToPasswordlessDotDevPageDesc": {
|
||||
"message": "Twórz przyjemne i bezpieczne doświadczenia z logowaniem wolne od tradycyjnych haseł za pomocą Passwordless.dev. Dowiedz się więcej na stronie bitwarden.com."
|
||||
"message": "Loguj się szybko i bezpiecznie bez tradycyjnych haseł za pomocą usługi Passwordless.dev. Dowiedz się więcej na stronie bitwarden.com."
|
||||
},
|
||||
"freeBitwardenFamilies": {
|
||||
"message": "Darmowy plan rodzinny"
|
||||
@ -541,14 +541,14 @@
|
||||
"description": "Label for the avoid ambiguous characters checkbox."
|
||||
},
|
||||
"generatorPolicyInEffect": {
|
||||
"message": "Wymagania polityki przedsiębiorstwa zostały użyte do ustawienia opcji generatora.",
|
||||
"message": "Zasady organizacji zostały zastosowane do opcji generatora.",
|
||||
"description": "Indicates that a policy limits the credential generator screen."
|
||||
},
|
||||
"searchVault": {
|
||||
"message": "Szukaj w sejfie"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "Zresetuj wyszukiwanie"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Edytuj"
|
||||
@ -650,7 +650,7 @@
|
||||
"message": "Blokowanie sejfu"
|
||||
},
|
||||
"otherOptions": {
|
||||
"message": "Pozostałe opcje"
|
||||
"message": "Inne opcje"
|
||||
},
|
||||
"rateExtension": {
|
||||
"message": "Oceń rozszerzenie"
|
||||
@ -805,7 +805,7 @@
|
||||
"message": "Zalogowano!"
|
||||
},
|
||||
"youSuccessfullyLoggedIn": {
|
||||
"message": "Zalogowałeś się pomyślnie"
|
||||
"message": "Zalogowano"
|
||||
},
|
||||
"youMayCloseThisWindow": {
|
||||
"message": "Możesz zamknąć to okno"
|
||||
@ -833,7 +833,7 @@
|
||||
}
|
||||
},
|
||||
"autofillError": {
|
||||
"message": "Nie można zastosować autouzupełnienia na tej stronie. Skopiuj i wklej informacje ręcznie."
|
||||
"message": "Nie można uzupełnić elementu na tej stronie internetowej. Skopiuj i wklej informacje ręcznie."
|
||||
},
|
||||
"totpCaptureError": {
|
||||
"message": "Nie można zeskanować kodu QR z obecnej strony"
|
||||
@ -1028,7 +1028,7 @@
|
||||
"message": "Proponuj dodanie elementu, jeśli nie ma go w sejfie. Dotyczy wszystkich zalogowanych kont."
|
||||
},
|
||||
"showCardsInVaultViewV2": {
|
||||
"message": "Pokazuj zawsze karty w sugestiach autouzupełniania"
|
||||
"message": "Zawsze pokazuj karty w sugestiach autouzupełniania"
|
||||
},
|
||||
"showCardsCurrentTab": {
|
||||
"message": "Pokaż karty na stronie głównej"
|
||||
@ -1037,7 +1037,7 @@
|
||||
"message": "Wyświetla karty na głównej karcie sejfu."
|
||||
},
|
||||
"showIdentitiesInVaultViewV2": {
|
||||
"message": "Pokazuj zawsze tożsamości w sugestiach autouzupełniania"
|
||||
"message": "Zawsze pokazuj tożsamości w sugestiach autouzupełniania"
|
||||
},
|
||||
"showIdentitiesCurrentTab": {
|
||||
"message": "Pokaż tożsamości na stronie głównej"
|
||||
@ -1066,7 +1066,7 @@
|
||||
"message": "Zapisz"
|
||||
},
|
||||
"notificationViewAria": {
|
||||
"message": "Wyświetl $ITEMNAME$, otworzy się w nowym oknie",
|
||||
"message": "Pokaż $ITEMNAME$, otwiera się w nowym oknie",
|
||||
"placeholders": {
|
||||
"itemName": {
|
||||
"content": "$1"
|
||||
@ -1177,7 +1177,7 @@
|
||||
"description": "Detailed error message shown when saving login details fails."
|
||||
},
|
||||
"changePasswordWarning": {
|
||||
"message": "Po zmianie hasła musisz się zalogować przy użyciu nowego hasła. Aktywne sesje na innych urządzeniach zostaną wylogowane w ciągu jednej godziny."
|
||||
"message": "Po zmianie hasła zaloguj się za pomocą nowego hasła. Aktywne sesje na innych urządzeniach zostaną wylogowane w ciągu jednej godziny."
|
||||
},
|
||||
"accountRecoveryUpdateMasterPasswordSubtitle": {
|
||||
"message": "Zmień hasło główne, aby zakończyć odzyskiwanie konta."
|
||||
@ -1204,7 +1204,7 @@
|
||||
"message": "Zaktualizuj"
|
||||
},
|
||||
"notificationUnlockDesc": {
|
||||
"message": "Odblokuj swój sejf Bitwarden, aby ukończyć żądanie autouzupełniania."
|
||||
"message": "Odblokuj sejf Bitwarden, aby uzupełnić dane."
|
||||
},
|
||||
"notificationUnlock": {
|
||||
"message": "Odblokuj"
|
||||
@ -1273,7 +1273,7 @@
|
||||
"message": "Rodzaj eksportu"
|
||||
},
|
||||
"accountRestricted": {
|
||||
"message": "Konto ograniczone"
|
||||
"message": "Konto zostało ograniczone"
|
||||
},
|
||||
"filePasswordAndConfirmFilePasswordDoNotMatch": {
|
||||
"message": "Hasła pliku nie pasują do siebie."
|
||||
@ -1305,7 +1305,7 @@
|
||||
"message": "Udostępnione"
|
||||
},
|
||||
"bitwardenForBusinessPageDesc": {
|
||||
"message": "Bitwarden dla biznesu pozwala na udostępnianie zawartości sejfu innym użytkownikom za pośrednictwem organizacji. Dowiedz się więcej na stronie bitwarden.com."
|
||||
"message": "Bitwarden dla firm pozwala na udostępnianie zawartości sejfu innym użytkownikom za pośrednictwem organizacji. Dowiedz się więcej na stronie bitwarden.com."
|
||||
},
|
||||
"moveToOrganization": {
|
||||
"message": "Przenieś do organizacji"
|
||||
@ -1459,7 +1459,7 @@
|
||||
"message": "Automatycznie kopiuje kod TOTP do schowka podczas autouzupełniania."
|
||||
},
|
||||
"enableAutoBiometricsPrompt": {
|
||||
"message": "Poproś o dane biometryczne przy uruchomieniu"
|
||||
"message": "Wymagaj odblokowania biometrią po uruchomieniu przeglądarki"
|
||||
},
|
||||
"premiumRequired": {
|
||||
"message": "Konto premium jest wymagane"
|
||||
@ -1983,7 +1983,7 @@
|
||||
"message": "Kolekcje"
|
||||
},
|
||||
"nCollections": {
|
||||
"message": "Kolekcje ($COUNT$)",
|
||||
"message": "W $COUNT$ kolekcjach",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
@ -2069,7 +2069,7 @@
|
||||
"description": "Default URI match detection for autofill."
|
||||
},
|
||||
"toggleOptions": {
|
||||
"message": "Zmień opcje"
|
||||
"message": "Przełącz opcje"
|
||||
},
|
||||
"toggleCurrentUris": {
|
||||
"message": "Przełącz obecny URI",
|
||||
@ -2099,7 +2099,7 @@
|
||||
"message": "Brak zawartości do pokazania"
|
||||
},
|
||||
"nothingGeneratedRecently": {
|
||||
"message": "Nic nie zostało wygenerowane przez ciebie w ostatnim czasie"
|
||||
"message": "Nic nie zostało wygenerowane w ostatnim czasie"
|
||||
},
|
||||
"remove": {
|
||||
"message": "Usuń"
|
||||
@ -2150,7 +2150,7 @@
|
||||
"message": "Hasło główne jest słabe"
|
||||
},
|
||||
"weakMasterPasswordDesc": {
|
||||
"message": "Wybrane przez Ciebie hasło główne jest słabe. Powinieneś użyć silniejszego hasła (lub frazy), aby właściwie chronić swoje konto Bitwarden. Czy na pewno chcesz użyć tego hasła głównego?"
|
||||
"message": "Użyj silniejszego hasła, aby odpowiednio chronić konto Bitwarden. Czy na pewno chcesz użyć tego hasła głównego?"
|
||||
},
|
||||
"pin": {
|
||||
"message": "Kod PIN",
|
||||
@ -2190,7 +2190,7 @@
|
||||
"message": "Oczekiwanie na potwierdzenie z aplikacji desktopowej"
|
||||
},
|
||||
"awaitDesktopDesc": {
|
||||
"message": "Włącz dane biometryczne w aplikacji desktopowej Bitwarden, aby włączyć tę samą funkcję w przeglądarce."
|
||||
"message": "Włącz najpierw biometrię w aplikacji desktopowej Bitwarden, aby skonfigurować dane biometryczne w przeglądarce."
|
||||
},
|
||||
"lockWithMasterPassOnRestart": {
|
||||
"message": "Zablokuj hasłem głównym po uruchomieniu przeglądarki"
|
||||
@ -2226,7 +2226,7 @@
|
||||
"message": "Użyj tej nazwy użytkownika"
|
||||
},
|
||||
"securePasswordGenerated": {
|
||||
"message": "Wygenerowane bezpieczne hasło! Nie zapomnij również zaktualizować hasła na stronie."
|
||||
"message": "Bezpieczne hasło zostało wygenerowane! Nie zapomnij zaktualizować hasła na stronie internetowej."
|
||||
},
|
||||
"useGeneratorHelpTextPartOne": {
|
||||
"message": "Użyj generatora",
|
||||
@ -2371,7 +2371,7 @@
|
||||
"message": "Anuluj subskrypcję"
|
||||
},
|
||||
"atAnyTime": {
|
||||
"message": "w każdej chwili."
|
||||
"message": "w dowolnym momencie."
|
||||
},
|
||||
"byContinuingYouAgreeToThe": {
|
||||
"message": "Kontynuując, akceptujesz"
|
||||
@ -2410,7 +2410,7 @@
|
||||
"message": "Weryfikacja synchronizacji z aplikacją desktopową"
|
||||
},
|
||||
"desktopIntegrationVerificationText": {
|
||||
"message": "Zweryfikuj aplikację desktopową z odciskiem klucza: "
|
||||
"message": "Zweryfikuj aplikację desktopową z identyfikatorem: "
|
||||
},
|
||||
"desktopIntegrationDisabledTitle": {
|
||||
"message": "Połączenie z przeglądarką jest wyłączone"
|
||||
@ -2452,7 +2452,7 @@
|
||||
"message": "Biometria jest wyłączona"
|
||||
},
|
||||
"biometricsNotEnabledDesc": {
|
||||
"message": "Aby włączyć dane biometryczne w przeglądarce, musisz włączyć tę samą funkcję w ustawianiach aplikacji desktopowej."
|
||||
"message": "Aby skonfigurować dane biometryczne w przeglądarce, włącz najpierw biometrię w aplikacji desktopowej."
|
||||
},
|
||||
"biometricsNotSupportedTitle": {
|
||||
"message": "Biometria nie jest obsługiwana"
|
||||
@ -2622,7 +2622,7 @@
|
||||
"message": "Zmień zagrożone hasła szybciej"
|
||||
},
|
||||
"changeAtRiskPasswordsFasterDesc": {
|
||||
"message": "Zaktualizuj swoje ustawienia, aby szybko autouzupełniać hasła i generować nowe"
|
||||
"message": "Zaktualizuj ustawienia, aby szybko uzupełniać hasła i generować nowe."
|
||||
},
|
||||
"reviewAtRiskLogins": {
|
||||
"message": "Sprawdź zagrożone dane logowania"
|
||||
@ -3431,7 +3431,7 @@
|
||||
"message": "Unikalny identyfikator konta"
|
||||
},
|
||||
"fingerprintMatchInfo": {
|
||||
"message": "Upewnij się, że sejf jest odblokowany, a identyfikator konta pasuje do drugiego urządzenia."
|
||||
"message": "Upewnij się, że sejf jest odblokowany, a identyfikator pasuje do drugiego urządzenia."
|
||||
},
|
||||
"resendNotification": {
|
||||
"message": "Wyślij ponownie powiadomienie"
|
||||
@ -3449,7 +3449,7 @@
|
||||
"message": "aplikacji internetowej"
|
||||
},
|
||||
"notificationSentDevicePart2": {
|
||||
"message": "Upewnij się, że fraza odcisku palca zgadza się z tą poniżej, zanim zatwierdzisz."
|
||||
"message": "Upewnij się, że identyfikator jest zgodny."
|
||||
},
|
||||
"aNotificationWasSentToYourDevice": {
|
||||
"message": "Powiadomienie zostało wysłane na urządzenie"
|
||||
@ -3486,7 +3486,7 @@
|
||||
"message": "Urządzenie"
|
||||
},
|
||||
"loginStatus": {
|
||||
"message": "Status zalogowania"
|
||||
"message": "Status logowania"
|
||||
},
|
||||
"masterPasswordChanged": {
|
||||
"message": "Hasło główne zostało zapisane"
|
||||
@ -3537,7 +3537,7 @@
|
||||
}
|
||||
},
|
||||
"autofillSelectInfoWithoutCommand": {
|
||||
"message": "Wybierz element z tego ekranu lub zobacz inne opcje w ustawieniach."
|
||||
"message": "Wybierz element lub zobacz inne opcje w ustawieniach."
|
||||
},
|
||||
"gotIt": {
|
||||
"message": "Ok"
|
||||
@ -3582,7 +3582,7 @@
|
||||
"message": "Otwiera w nowym oknie"
|
||||
},
|
||||
"rememberThisDeviceToMakeFutureLoginsSeamless": {
|
||||
"message": "Zapamiętaj to urządzenie, aby przyszłe logowania były bezproblemowe"
|
||||
"message": "Zapamiętaj urządzenie, aby przyszłe logowania były bezproblemowe"
|
||||
},
|
||||
"manageDevices": {
|
||||
"message": "Zarządzaj urządzeniami"
|
||||
@ -3808,7 +3808,7 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendsBodyNoItems": {
|
||||
"message": "Udostępniaj pliki i teksty każdemu, na dowolnej platformie. Informacje będę szyfrowane end-to-end, zapewniając poufność.",
|
||||
"message": "Udostępniaj pliki i teksty każdemu na dowolnej platformie. Informacje będę szyfrowane end-to-end, zapewniając poufność.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"inputRequired": {
|
||||
@ -3946,7 +3946,7 @@
|
||||
"message": "Przełącz nawigację boczną"
|
||||
},
|
||||
"skipToContent": {
|
||||
"message": "Przejdź do treści"
|
||||
"message": "Przejdź do zawartości"
|
||||
},
|
||||
"bitwardenOverlayButton": {
|
||||
"message": "Przycisk menu autouzupełniania Bitwarden",
|
||||
@ -4751,7 +4751,7 @@
|
||||
"message": "Pobierz Bitwarden"
|
||||
},
|
||||
"downloadBitwardenOnAllDevices": {
|
||||
"message": "Pobierz Bitwarden na wszystkie urządzenia"
|
||||
"message": "Bitwarden na inne urządzenia"
|
||||
},
|
||||
"getTheMobileApp": {
|
||||
"message": "Pobierz aplikację mobilną"
|
||||
@ -4763,7 +4763,7 @@
|
||||
"message": "Pobierz aplikację desktopową"
|
||||
},
|
||||
"getTheDesktopAppDesc": {
|
||||
"message": "Uzyskaj dostęp do sejfu bez przeglądarki, a następnie ustaw odblokowanie biometryczne, aby przyspieszyć odblokowanie zarówno w aplikacji desktopowej, jak i w rozszerzeniu przeglądarki."
|
||||
"message": "Uzyskaj dostęp do sejfu bez przeglądarki. Skonfiguruj biometrię, aby przyśpieszyć odblokowywanie aplikacji."
|
||||
},
|
||||
"downloadFromBitwardenNow": {
|
||||
"message": "Pobierz z bitwarden.com"
|
||||
@ -4889,7 +4889,7 @@
|
||||
"message": "Karta wygasła"
|
||||
},
|
||||
"cardExpiredMessage": {
|
||||
"message": "Jeśli ją wznowiłeś, zaktualizuj informacje o karcie"
|
||||
"message": "Jeśli karta została odnowiona, zaktualizuj informacje o niej"
|
||||
},
|
||||
"cardDetails": {
|
||||
"message": "Szczegóły karty"
|
||||
@ -5078,7 +5078,7 @@
|
||||
"message": "Przypisano kolekcje"
|
||||
},
|
||||
"nothingSelected": {
|
||||
"message": "Nie zaznaczyłeś żadnych elementów."
|
||||
"message": "Nie zaznaczono żadnych elementów."
|
||||
},
|
||||
"itemsMovedToOrg": {
|
||||
"message": "Elementy zostały przeniesione do organizacji $ORGNAME$",
|
||||
@ -5143,7 +5143,7 @@
|
||||
"message": "Domyślny systemu"
|
||||
},
|
||||
"enterprisePolicyRequirementsApplied": {
|
||||
"message": "Zastosowano wymagania zasady organizacji"
|
||||
"message": "Wymagania zasady organizacji zostały zastosowane"
|
||||
},
|
||||
"sshPrivateKey": {
|
||||
"message": "Klucz prywatny"
|
||||
@ -5170,7 +5170,7 @@
|
||||
"message": "RSA 4096-bit"
|
||||
},
|
||||
"retry": {
|
||||
"message": "Powtórz"
|
||||
"message": "Spróbuj ponownie"
|
||||
},
|
||||
"vaultCustomTimeoutMinimum": {
|
||||
"message": "Minimalny niestandardowy czas to 1 minuta."
|
||||
@ -5458,7 +5458,7 @@
|
||||
"message": "Zaktualizuj aplikację desktopową"
|
||||
},
|
||||
"updateDesktopAppOrDisableFingerprintDialogMessage": {
|
||||
"message": "Aby używać odblokowywania biometrycznego, zaktualizuj aplikację na komputerze lub wyłącz odblokowywanie odciskiem palca w ustawieniach aplikacji na komputerze."
|
||||
"message": "Aby używać biometrii, zaktualizuj aplikację desktopową."
|
||||
},
|
||||
"changeAtRiskPassword": {
|
||||
"message": "Zmień zagrożone hasło"
|
||||
@ -5587,7 +5587,7 @@
|
||||
"description": "Aria label for the body content of the generator nudge"
|
||||
},
|
||||
"noPermissionsViewPage": {
|
||||
"message": "Nie masz uprawnień do przeglądania tej strony. Spróbuj zalogować się na inne konto."
|
||||
"message": "Nie masz uprawnień do przeglądania tej strony. Zaloguj się na inne konto."
|
||||
},
|
||||
"wasmNotSupported": {
|
||||
"message": "WebAssembly nie jest obsługiwany w przeglądarce lub jest wyłączony. WebAssembly jest wymagany do korzystania z aplikacji Bitwarden.",
|
||||
|
||||
@ -548,7 +548,7 @@
|
||||
"message": "Претражи сеф"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "Ресетовати претрагу"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Уреди"
|
||||
@ -1833,7 +1833,7 @@
|
||||
"message": "Сигурносни код"
|
||||
},
|
||||
"cardNumber": {
|
||||
"message": "card number"
|
||||
"message": "број картице"
|
||||
},
|
||||
"ex": {
|
||||
"message": "нпр."
|
||||
@ -3467,7 +3467,7 @@
|
||||
"message": "Захтев је послат"
|
||||
},
|
||||
"loginRequestApprovedForEmailOnDevice": {
|
||||
"message": "Login request approved for $EMAIL$ on $DEVICE$",
|
||||
"message": "Захтев за пријаву одобрен за $EMAIL$ на $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3480,13 +3480,13 @@
|
||||
}
|
||||
},
|
||||
"youDeniedLoginAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
|
||||
"message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то били ви, покушајте поново да се пријавите помоћу уређаја."
|
||||
},
|
||||
"device": {
|
||||
"message": "Device"
|
||||
"message": "Уређај"
|
||||
},
|
||||
"loginStatus": {
|
||||
"message": "Login status"
|
||||
"message": "Статус пријаве"
|
||||
},
|
||||
"masterPasswordChanged": {
|
||||
"message": "Главна лозинка сачувана"
|
||||
@ -3585,28 +3585,28 @@
|
||||
"message": "Запамтити овај уређај да би будуће пријаве биле беспрекорне"
|
||||
},
|
||||
"manageDevices": {
|
||||
"message": "Manage devices"
|
||||
"message": "Управљање уређајима"
|
||||
},
|
||||
"currentSession": {
|
||||
"message": "Current session"
|
||||
"message": "Тренутна сесија"
|
||||
},
|
||||
"mobile": {
|
||||
"message": "Mobile",
|
||||
"message": "Мобилни",
|
||||
"description": "Mobile app"
|
||||
},
|
||||
"extension": {
|
||||
"message": "Extension",
|
||||
"message": "Додатак",
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
"message": "Desktop",
|
||||
"message": "Десктоп",
|
||||
"description": "Desktop app"
|
||||
},
|
||||
"webVault": {
|
||||
"message": "Web vault"
|
||||
"message": "Интернет Сеф"
|
||||
},
|
||||
"webApp": {
|
||||
"message": "Web app"
|
||||
"message": "Веб апликација"
|
||||
},
|
||||
"cli": {
|
||||
"message": "CLI"
|
||||
@ -3616,22 +3616,22 @@
|
||||
"description": "Software Development Kit"
|
||||
},
|
||||
"requestPending": {
|
||||
"message": "Request pending"
|
||||
"message": "Захтев је на чекању"
|
||||
},
|
||||
"firstLogin": {
|
||||
"message": "First login"
|
||||
"message": "Прва пријава"
|
||||
},
|
||||
"trusted": {
|
||||
"message": "Trusted"
|
||||
"message": "Поуздан"
|
||||
},
|
||||
"needsApproval": {
|
||||
"message": "Needs approval"
|
||||
"message": "Потребно је одобрење"
|
||||
},
|
||||
"devices": {
|
||||
"message": "Devices"
|
||||
"message": "Уређаји"
|
||||
},
|
||||
"accessAttemptBy": {
|
||||
"message": "Access attempt by $EMAIL$",
|
||||
"message": "Покушај приступа са $EMAIL$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3640,28 +3640,28 @@
|
||||
}
|
||||
},
|
||||
"confirmAccess": {
|
||||
"message": "Confirm access"
|
||||
"message": "Потврди приступ"
|
||||
},
|
||||
"denyAccess": {
|
||||
"message": "Deny access"
|
||||
"message": "Одбиј приступ"
|
||||
},
|
||||
"time": {
|
||||
"message": "Time"
|
||||
"message": "Време"
|
||||
},
|
||||
"deviceType": {
|
||||
"message": "Device Type"
|
||||
"message": "Тип уређаја"
|
||||
},
|
||||
"loginRequest": {
|
||||
"message": "Login request"
|
||||
"message": "Захтев за пријаву"
|
||||
},
|
||||
"thisRequestIsNoLongerValid": {
|
||||
"message": "This request is no longer valid."
|
||||
"message": "Овај захтев више није важећи."
|
||||
},
|
||||
"areYouTryingToAccessYourAccount": {
|
||||
"message": "Are you trying to access your account?"
|
||||
"message": "Да ли покушавате да приступите вашем налогу?"
|
||||
},
|
||||
"logInConfirmedForEmailOnDevice": {
|
||||
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
|
||||
"message": "Пријава потврђена за $EMAIL$ на $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3674,16 +3674,16 @@
|
||||
}
|
||||
},
|
||||
"youDeniedALogInAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
|
||||
"message": "Одбили сте покушај пријаве са другог уређаја. Ако сте то заиста били ви, покушајте поново да се пријавите помоћу уређаја."
|
||||
},
|
||||
"loginRequestHasAlreadyExpired": {
|
||||
"message": "Login request has already expired."
|
||||
"message": "Захтев за пријаву је већ истекао."
|
||||
},
|
||||
"justNow": {
|
||||
"message": "Just now"
|
||||
"message": "Управо сада"
|
||||
},
|
||||
"requestedXMinutesAgo": {
|
||||
"message": "Requested $MINUTES$ minutes ago",
|
||||
"message": "Затражено пре $MINUTES$ минута",
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"content": "$1",
|
||||
@ -4601,7 +4601,7 @@
|
||||
}
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"message": "Copy $FIELD$, $CIPHERNAME$",
|
||||
"message": "Копирај $FIELD$, $CIPHERNAME$",
|
||||
"description": "Title for a button that copies a field value to the clipboard.",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
|
||||
@ -462,13 +462,13 @@
|
||||
"message": "Parola oluştur"
|
||||
},
|
||||
"generatePassphrase": {
|
||||
"message": "Parola üret"
|
||||
"message": "Parola cümlesi üret"
|
||||
},
|
||||
"passwordGenerated": {
|
||||
"message": "Parola üretildi"
|
||||
},
|
||||
"passphraseGenerated": {
|
||||
"message": "Parola ifadesi oluşturuldu"
|
||||
"message": "Parola cümlesi üretildi"
|
||||
},
|
||||
"usernameGenerated": {
|
||||
"message": "Kullanıcı adı üretildi"
|
||||
@ -572,7 +572,7 @@
|
||||
"message": "Kimlik doğrulama sırrı"
|
||||
},
|
||||
"passphrase": {
|
||||
"message": "Uzun söz"
|
||||
"message": "Parola cümlesi"
|
||||
},
|
||||
"favorite": {
|
||||
"message": "Favori"
|
||||
@ -2220,7 +2220,7 @@
|
||||
"message": "Bu parolayı kullan"
|
||||
},
|
||||
"useThisPassphrase": {
|
||||
"message": "Bu parola ifadesini kullanın"
|
||||
"message": "Bu parola cümlesini kullan"
|
||||
},
|
||||
"useThisUsername": {
|
||||
"message": "Bu kullanıcı adını kullan"
|
||||
@ -3183,7 +3183,7 @@
|
||||
}
|
||||
},
|
||||
"passphraseNumWordsRecommendationHint": {
|
||||
"message": "Güçlü bir parola ifadesi oluşturmak için $RECOMMENDED$ veya daha fazla kelime kullanın.",
|
||||
"message": " Güçlü bir parola cümlesi üretmek için en az $RECOMMENDED$ kelime kullanın.",
|
||||
"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": {
|
||||
|
||||
@ -548,7 +548,7 @@
|
||||
"message": "Пошук"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "Скинути пошук"
|
||||
},
|
||||
"edit": {
|
||||
"message": "Змінити"
|
||||
@ -1833,7 +1833,7 @@
|
||||
"message": "Код безпеки"
|
||||
},
|
||||
"cardNumber": {
|
||||
"message": "card number"
|
||||
"message": "номер картки"
|
||||
},
|
||||
"ex": {
|
||||
"message": "зразок"
|
||||
@ -3467,7 +3467,7 @@
|
||||
"message": "Запит надіслано"
|
||||
},
|
||||
"loginRequestApprovedForEmailOnDevice": {
|
||||
"message": "Login request approved for $EMAIL$ on $DEVICE$",
|
||||
"message": "Запит входу підтверджено для $EMAIL$ на $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3480,13 +3480,13 @@
|
||||
}
|
||||
},
|
||||
"youDeniedLoginAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
|
||||
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були ви, спробуйте ввійти з пристроєм знову."
|
||||
},
|
||||
"device": {
|
||||
"message": "Device"
|
||||
"message": "Пристрій"
|
||||
},
|
||||
"loginStatus": {
|
||||
"message": "Login status"
|
||||
"message": "Стан входу в систему"
|
||||
},
|
||||
"masterPasswordChanged": {
|
||||
"message": "Головний пароль збережено"
|
||||
@ -3585,28 +3585,28 @@
|
||||
"message": "Запам'ятайте цей пристрій, щоб спростити майбутні входи в систему"
|
||||
},
|
||||
"manageDevices": {
|
||||
"message": "Manage devices"
|
||||
"message": "Керувати пристроями"
|
||||
},
|
||||
"currentSession": {
|
||||
"message": "Current session"
|
||||
"message": "Поточний сеанс"
|
||||
},
|
||||
"mobile": {
|
||||
"message": "Mobile",
|
||||
"message": "Мобільний",
|
||||
"description": "Mobile app"
|
||||
},
|
||||
"extension": {
|
||||
"message": "Extension",
|
||||
"message": "Розширення",
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
"message": "Desktop",
|
||||
"message": "Комп'ютер",
|
||||
"description": "Desktop app"
|
||||
},
|
||||
"webVault": {
|
||||
"message": "Web vault"
|
||||
"message": "Вебсховище"
|
||||
},
|
||||
"webApp": {
|
||||
"message": "Web app"
|
||||
"message": "Вебпрограма"
|
||||
},
|
||||
"cli": {
|
||||
"message": "CLI"
|
||||
@ -3616,22 +3616,22 @@
|
||||
"description": "Software Development Kit"
|
||||
},
|
||||
"requestPending": {
|
||||
"message": "Request pending"
|
||||
"message": "Запит в очікуванні"
|
||||
},
|
||||
"firstLogin": {
|
||||
"message": "First login"
|
||||
"message": "Перший вхід"
|
||||
},
|
||||
"trusted": {
|
||||
"message": "Trusted"
|
||||
"message": "Надійний"
|
||||
},
|
||||
"needsApproval": {
|
||||
"message": "Needs approval"
|
||||
"message": "Потребує підтвердження"
|
||||
},
|
||||
"devices": {
|
||||
"message": "Devices"
|
||||
"message": "Пристрої"
|
||||
},
|
||||
"accessAttemptBy": {
|
||||
"message": "Access attempt by $EMAIL$",
|
||||
"message": "Спроба доступу з $EMAIL$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3640,28 +3640,28 @@
|
||||
}
|
||||
},
|
||||
"confirmAccess": {
|
||||
"message": "Confirm access"
|
||||
"message": "Підтвердити доступ"
|
||||
},
|
||||
"denyAccess": {
|
||||
"message": "Deny access"
|
||||
"message": "Заборонити доступ"
|
||||
},
|
||||
"time": {
|
||||
"message": "Time"
|
||||
"message": "Час"
|
||||
},
|
||||
"deviceType": {
|
||||
"message": "Device Type"
|
||||
"message": "Тип пристрою"
|
||||
},
|
||||
"loginRequest": {
|
||||
"message": "Login request"
|
||||
"message": "Запит входу"
|
||||
},
|
||||
"thisRequestIsNoLongerValid": {
|
||||
"message": "This request is no longer valid."
|
||||
"message": "Цей запит більше недійсний."
|
||||
},
|
||||
"areYouTryingToAccessYourAccount": {
|
||||
"message": "Are you trying to access your account?"
|
||||
"message": "Ви намагаєтесь отримати доступ до свого облікового запису?"
|
||||
},
|
||||
"logInConfirmedForEmailOnDevice": {
|
||||
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
|
||||
"message": "Підтверджено вхід для $EMAIL$ на $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
@ -3674,16 +3674,16 @@
|
||||
}
|
||||
},
|
||||
"youDeniedALogInAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
|
||||
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте ввійти з пристроєм знову."
|
||||
},
|
||||
"loginRequestHasAlreadyExpired": {
|
||||
"message": "Login request has already expired."
|
||||
"message": "Термін дії запиту на вхід завершився."
|
||||
},
|
||||
"justNow": {
|
||||
"message": "Just now"
|
||||
"message": "Щойно"
|
||||
},
|
||||
"requestedXMinutesAgo": {
|
||||
"message": "Requested $MINUTES$ minutes ago",
|
||||
"message": "Запитано $MINUTES$ хвилин тому",
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"content": "$1",
|
||||
@ -4601,7 +4601,7 @@
|
||||
}
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"message": "Copy $FIELD$, $CIPHERNAME$",
|
||||
"message": "Копіювати $FIELD$, $CIPHERNAME$",
|
||||
"description": "Title for a button that copies a field value to the clipboard.",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="tw-mr-[5px] tw-mt-1">
|
||||
<div class="tw-me-2 tw-mt-1">
|
||||
<button
|
||||
*ngIf="currentAccount$ | async as currentAccount; else defaultButton"
|
||||
type="button"
|
||||
|
||||
@ -5,7 +5,6 @@ import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.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";
|
||||
@ -13,6 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutService,
|
||||
|
||||
@ -25,7 +25,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
@ -33,6 +32,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { CollectionView } from "../../content/components/common-types";
|
||||
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
||||
@ -20,7 +18,7 @@ interface NotificationQueueMessage {
|
||||
|
||||
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
||||
type: "change";
|
||||
cipherId: string;
|
||||
cipherId: CipherView["id"];
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
@ -60,23 +58,10 @@ type LockedVaultPendingNotificationsData = {
|
||||
target: string;
|
||||
};
|
||||
|
||||
type AtRiskPasswordNotificationsData = {
|
||||
activeUserId: UserId;
|
||||
cipher: CipherView;
|
||||
securityTask: SecurityTask;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
type AdjustNotificationBarMessageData = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
type ChangePasswordMessageData = {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type AddLoginMessageData = {
|
||||
username: string;
|
||||
password: string;
|
||||
@ -92,10 +77,7 @@ type NotificationBackgroundExtensionMessage = {
|
||||
command: string;
|
||||
data?: Partial<LockedVaultPendingNotificationsData> &
|
||||
Partial<AdjustNotificationBarMessageData> &
|
||||
Partial<ChangePasswordMessageData> &
|
||||
Partial<UnlockVaultMessageData> &
|
||||
Partial<AtRiskPasswordNotificationsData>;
|
||||
login?: AddLoginMessageData;
|
||||
Partial<UnlockVaultMessageData>;
|
||||
folder?: string;
|
||||
edit?: boolean;
|
||||
details?: AutofillPageDetails;
|
||||
@ -121,18 +103,6 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgOpenAtRiskPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<void>;
|
||||
bgTriggerAddLoginNotification: ({
|
||||
message,
|
||||
sender,
|
||||
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||
bgTriggerChangedPasswordNotification: ({
|
||||
message,
|
||||
sender,
|
||||
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||
bgTriggerAtRiskPasswordNotification: ({
|
||||
message,
|
||||
sender,
|
||||
}: BackgroundOnMessageHandlerParams) => Promise<boolean>;
|
||||
bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void;
|
||||
bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
bgOpenAddEditVaultItemPopout: ({
|
||||
@ -162,7 +132,6 @@ export {
|
||||
NotificationQueueMessageItem,
|
||||
LockedVaultPendingNotificationsData,
|
||||
AdjustNotificationBarMessageData,
|
||||
ChangePasswordMessageData,
|
||||
UnlockVaultMessageData,
|
||||
AddLoginMessageData,
|
||||
NotificationBackgroundExtensionMessage,
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
|
||||
export type NotificationTypeData = {
|
||||
@ -8,6 +11,12 @@ export type NotificationTypeData = {
|
||||
launchTimestamp?: number;
|
||||
};
|
||||
|
||||
export type LoginSecurityTaskInfo = {
|
||||
securityTask: SecurityTask;
|
||||
cipher: CipherView;
|
||||
uri: ModifyLoginCipherFormData["uri"];
|
||||
};
|
||||
|
||||
export type WebsiteOriginsWithFields = Map<chrome.tabs.Tab["id"], Set<string>>;
|
||||
|
||||
export type ActiveFormSubmissionRequests = Set<chrome.webRequest.ResourceRequest["requestId"]>;
|
||||
@ -19,19 +28,12 @@ export type ModifyLoginCipherFormData = {
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export type ModifyLoginCipherFormDataForTab = Map<
|
||||
chrome.tabs.Tab["id"],
|
||||
{ uri: string; username: string; password: string; newPassword: string }
|
||||
>;
|
||||
export type ModifyLoginCipherFormDataForTab = Map<chrome.tabs.Tab["id"], ModifyLoginCipherFormData>;
|
||||
|
||||
export type OverlayNotificationsExtensionMessage = {
|
||||
command: string;
|
||||
uri?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
newPassword?: string;
|
||||
details?: AutofillPageDetails;
|
||||
};
|
||||
} & ModifyLoginCipherFormData;
|
||||
|
||||
type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage };
|
||||
type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender };
|
||||
|
||||
@ -245,6 +245,7 @@ export type OverlayBackgroundExtensionMessageHandlers = {
|
||||
editedCipher: () => void;
|
||||
deletedCipher: () => void;
|
||||
bgSaveCipher: () => void;
|
||||
updateOverlayCiphers: () => void;
|
||||
fido2AbortRequest: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
|
||||
};
|
||||
|
||||
|
||||
@ -3,17 +3,18 @@ import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { ExtensionCommand } from "@bitwarden/common/autofill/constants";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@ -38,6 +39,7 @@ import {
|
||||
LockedVaultPendingNotificationsData,
|
||||
NotificationBackgroundExtensionMessage,
|
||||
} from "./abstractions/notification.background";
|
||||
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
|
||||
import NotificationBackground from "./notification.background";
|
||||
|
||||
jest.mock("rxjs", () => {
|
||||
@ -58,13 +60,21 @@ describe("NotificationBackground", () => {
|
||||
const collectionService = mock<CollectionService>();
|
||||
let activeAccountStatusMock$: BehaviorSubject<AuthenticationStatus>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
const policyService = mock<DefaultPolicyService>();
|
||||
const policyAppliesToUser$ = new BehaviorSubject<boolean>(true);
|
||||
const policyService = mock<PolicyService>({
|
||||
policyAppliesToUser$: jest.fn().mockReturnValue(policyAppliesToUser$),
|
||||
});
|
||||
const folderService = mock<FolderService>();
|
||||
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
||||
const enableChangedPasswordPromptMock$ = new BehaviorSubject(true);
|
||||
const userNotificationSettingsService = mock<UserNotificationSettingsServiceAbstraction>();
|
||||
userNotificationSettingsService.enableChangedPasswordPrompt$ = enableChangedPasswordPromptMock$;
|
||||
|
||||
const domainSettingsService = mock<DomainSettingsService>();
|
||||
const environmentService = mock<EnvironmentService>();
|
||||
const logService = mock<LogService>();
|
||||
const selectedThemeMock$ = new BehaviorSubject(ThemeTypes.Light);
|
||||
const themeStateService = mock<ThemeStateService>();
|
||||
themeStateService.selectedTheme$ = selectedThemeMock$;
|
||||
const configService = mock<ConfigService>();
|
||||
const accountService = mock<AccountService>();
|
||||
const organizationService = mock<OrganizationService>();
|
||||
@ -164,7 +174,7 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification bar extension message handlers", () => {
|
||||
describe("notification bar extension message handlers and triggers", () => {
|
||||
beforeEach(() => {
|
||||
notificationBackground.init();
|
||||
});
|
||||
@ -283,7 +293,12 @@ describe("NotificationBackground", () => {
|
||||
let pushAddLoginToQueueSpy: jest.SpyInstance;
|
||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||
|
||||
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
|
||||
username: "test",
|
||||
password: "password",
|
||||
uri: "https://example.com",
|
||||
newPassword: null,
|
||||
};
|
||||
beforeEach(() => {
|
||||
tab = createChromeTabMock();
|
||||
sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
@ -304,43 +319,34 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the user is logged out", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
|
||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the login data does not contain a valid url", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "" },
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled();
|
||||
expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the user with a locked vault has disabled the login notification", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled();
|
||||
@ -349,16 +355,12 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to add the login if the user with an unlocked vault has disabled the login notification", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(false);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
@ -367,10 +369,7 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to change the password for an existing login if the user has disabled changing the password notification", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false);
|
||||
@ -378,8 +377,7 @@ describe("NotificationBackground", () => {
|
||||
mock<CipherView>({ login: { username: "test", password: "oldPassword" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
@ -389,18 +387,14 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to change the password for an existing login if the password has not changed", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: { username: "test", password: "password", url: "https://example.com" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled();
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
@ -409,48 +403,55 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("adds the login to the queue if the user has a locked account", async () => {
|
||||
const login = { username: "test", password: "password", url: "https://example.com" };
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login,
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab, true);
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith(
|
||||
"example.com",
|
||||
{
|
||||
url: data.uri,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
},
|
||||
sender.tab,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds the login to the queue if the user has an unlocked account and the login is new", async () => {
|
||||
const login = {
|
||||
username: undefined,
|
||||
password: "password",
|
||||
url: "https://example.com",
|
||||
} as any;
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login,
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
username: null,
|
||||
};
|
||||
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockReturnValueOnce(true);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
mock<CipherView>({ login: { username: "anotherTestUsername", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab);
|
||||
expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith(
|
||||
"example.com",
|
||||
{
|
||||
url: data.uri,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
},
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => {
|
||||
const login = { username: "tEsT", password: "password", url: "https://example.com" };
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login,
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
username: "tEsT",
|
||||
};
|
||||
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true);
|
||||
getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true);
|
||||
@ -461,13 +462,12 @@ describe("NotificationBackground", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerAddLoginNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
login.password,
|
||||
data.password,
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
@ -478,6 +478,12 @@ describe("NotificationBackground", () => {
|
||||
let sender: chrome.runtime.MessageSender;
|
||||
let pushChangePasswordToQueueSpy: jest.SpyInstance;
|
||||
let getAllDecryptedForUrlSpy: jest.SpyInstance;
|
||||
const mockModifyLoginCipherFormData: ModifyLoginCipherFormData = {
|
||||
username: null,
|
||||
uri: null,
|
||||
password: "currentPassword",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
tab = createChromeTabMock();
|
||||
@ -490,69 +496,51 @@ describe("NotificationBackground", () => {
|
||||
});
|
||||
|
||||
it("skips attempting to add the change password message to the queue if the passed url is not valid", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: { newPassword: "newPassword", currentPassword: "currentPassword", url: "" },
|
||||
};
|
||||
const data: ModifyLoginCipherFormData = mockModifyLoginCipherFormData;
|
||||
|
||||
sendMockExtensionMessage(message);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds a change password message to the queue if the user does not have an unlocked account", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
null,
|
||||
"example.com",
|
||||
message.data?.newPassword,
|
||||
data?.newPassword,
|
||||
sender.tab,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("skips adding a change password message to the queue if the multiple ciphers exist for the passed URL and the current password is not found within the list of ciphers", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
mock<CipherView>({ login: { username: "test", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips adding a change password message if more than one existing cipher is found with a matching password ", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@ -560,21 +548,16 @@ describe("NotificationBackground", () => {
|
||||
mock<CipherView>({ login: { username: "test2", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds a change password message to the queue if a single cipher matches the passed current password", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
currentPassword: "currentPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@ -584,24 +567,20 @@ describe("NotificationBackground", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
message.data?.newPassword,
|
||||
data?.newPassword,
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
|
||||
it("skips adding a change password message if no current password is passed in the message and more than one cipher is found for a url", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@ -609,20 +588,17 @@ describe("NotificationBackground", () => {
|
||||
mock<CipherView>({ login: { username: "test2", password: "password" } }),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(getAllDecryptedForUrlSpy).toHaveBeenCalled();
|
||||
expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds a change password message to the queue if no current password is passed with the message, but a single cipher is matched for the uri", async () => {
|
||||
const message: NotificationBackgroundExtensionMessage = {
|
||||
command: "bgTriggerChangedPasswordNotification",
|
||||
data: {
|
||||
newPassword: "newPassword",
|
||||
url: "https://example.com",
|
||||
},
|
||||
const data: ModifyLoginCipherFormData = {
|
||||
...mockModifyLoginCipherFormData,
|
||||
uri: "https://example.com",
|
||||
password: null,
|
||||
};
|
||||
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
|
||||
getAllDecryptedForUrlSpy.mockResolvedValueOnce([
|
||||
@ -632,13 +608,12 @@ describe("NotificationBackground", () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
await flushPromises();
|
||||
await notificationBackground.triggerChangedPasswordNotification(data, tab);
|
||||
|
||||
expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith(
|
||||
"cipher-id",
|
||||
"example.com",
|
||||
message.data?.newPassword,
|
||||
data?.newPassword,
|
||||
sender.tab,
|
||||
);
|
||||
});
|
||||
|
||||
@ -41,7 +41,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { SecurityTaskType } from "@bitwarden/common/vault/tasks/enums";
|
||||
import { SecurityTaskStatus, SecurityTaskType } from "@bitwarden/common/vault/tasks/enums";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task";
|
||||
|
||||
// FIXME (PM-22628): Popup imports are forbidden in background
|
||||
@ -68,14 +68,17 @@ import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
ChangePasswordMessageData,
|
||||
AddLoginMessageData,
|
||||
NotificationQueueMessageItem,
|
||||
LockedVaultPendingNotificationsData,
|
||||
NotificationBackgroundExtensionMessage,
|
||||
NotificationBackgroundExtensionMessageHandlers,
|
||||
} from "./abstractions/notification.background";
|
||||
import { NotificationTypeData } from "./abstractions/overlay-notifications.background";
|
||||
import {
|
||||
LoginSecurityTaskInfo,
|
||||
ModifyLoginCipherFormData,
|
||||
NotificationTypeData,
|
||||
} from "./abstractions/overlay-notifications.background";
|
||||
import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.background";
|
||||
|
||||
export default class NotificationBackground {
|
||||
@ -91,12 +94,6 @@ export default class NotificationBackground {
|
||||
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
|
||||
bgAdjustNotificationBar: ({ message, sender }) =>
|
||||
this.handleAdjustNotificationBarMessage(message, sender),
|
||||
bgTriggerAddLoginNotification: ({ message, sender }) =>
|
||||
this.triggerAddLoginNotification(message, sender),
|
||||
bgTriggerChangedPasswordNotification: ({ message, sender }) =>
|
||||
this.triggerChangedPasswordNotification(message, sender),
|
||||
bgTriggerAtRiskPasswordNotification: ({ message, sender }) =>
|
||||
this.triggerAtRiskPasswordNotification(message, sender),
|
||||
bgCloseNotificationBar: ({ message, sender }) =>
|
||||
this.handleCloseNotificationBarMessage(message, sender),
|
||||
bgOpenAtRiskPasswords: ({ message, sender }) =>
|
||||
@ -286,6 +283,62 @@ export default class NotificationBackground {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a security task for this cipher at login, return the task, cipher view, and uri.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
* @param activeUserId - The currently logged in user ID
|
||||
*/
|
||||
private async getSecurityTaskAndCipherForLoginData(
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
activeUserId: UserId,
|
||||
): Promise<LoginSecurityTaskInfo | null> {
|
||||
const tasks: SecurityTask[] = await this.getSecurityTasks(activeUserId);
|
||||
if (!tasks?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlCiphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
|
||||
modifyLoginData.uri,
|
||||
activeUserId,
|
||||
);
|
||||
if (!urlCiphers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const securityTaskForLogin = urlCiphers.reduce(
|
||||
(taskInfo: LoginSecurityTaskInfo | null, cipher: CipherView) => {
|
||||
if (
|
||||
// exit early if info was found already
|
||||
taskInfo ||
|
||||
// exit early if the cipher was deleted
|
||||
cipher.deletedDate ||
|
||||
// exit early if the entered login info doesn't match an existing cipher
|
||||
modifyLoginData.username !== cipher.login.username ||
|
||||
modifyLoginData.password !== cipher.login.password
|
||||
) {
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
// Find the first security task for the cipherId belonging to the entered login
|
||||
const cipherSecurityTask = tasks.find(
|
||||
({ cipherId, status }) =>
|
||||
cipher.id === cipherId && // match security task cipher id to url cipher id
|
||||
status === SecurityTaskStatus.Pending, // security task has not been completed
|
||||
);
|
||||
|
||||
if (cipherSecurityTask) {
|
||||
return { securityTask: cipherSecurityTask, cipher, uri: modifyLoginData.uri };
|
||||
}
|
||||
|
||||
return taskInfo;
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
return securityTaskForLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active user server config from the config service.
|
||||
*/
|
||||
@ -302,6 +355,10 @@ export default class NotificationBackground {
|
||||
return flagValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current authentication status of the user.
|
||||
* @returns Promise<AuthenticationStatus> - The current authentication status of the user.
|
||||
*/
|
||||
private async getAuthStatus() {
|
||||
return await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
}
|
||||
@ -400,11 +457,32 @@ export default class NotificationBackground {
|
||||
* @param sender - The contextual sender of the message
|
||||
*/
|
||||
async triggerAtRiskPasswordNotification(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const { activeUserId, securityTask, cipher } = message.data;
|
||||
const domain = Utils.getDomain(sender.tab.url);
|
||||
const flag = await this.getNotificationFlag();
|
||||
if (!flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (!activeUserId) {
|
||||
return false;
|
||||
}
|
||||
const loginSecurityTaskInfo = await this.getSecurityTaskAndCipherForLoginData(
|
||||
data,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
if (!loginSecurityTaskInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { securityTask, cipher } = loginSecurityTaskInfo;
|
||||
const domain = Utils.getDomain(tab.url);
|
||||
const passwordChangeUri =
|
||||
await new TemporaryNotificationChangeLoginService().getChangePasswordUrl(cipher);
|
||||
|
||||
@ -418,7 +496,7 @@ export default class NotificationBackground {
|
||||
.pipe(getOrganizationById(securityTask.organizationId)),
|
||||
);
|
||||
|
||||
this.removeTabFromNotificationQueue(sender.tab);
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const queueMessage: NotificationQueueMessageItem = {
|
||||
domain,
|
||||
@ -426,12 +504,12 @@ export default class NotificationBackground {
|
||||
type: NotificationQueueMessageType.AtRiskPassword,
|
||||
passwordChangeUri,
|
||||
organizationName: organization.name,
|
||||
tab: sender.tab,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS),
|
||||
};
|
||||
this.notificationQueue.push(queueMessage);
|
||||
await this.checkNotificationQueue(sender.tab);
|
||||
await this.checkNotificationQueue(tab);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -444,17 +522,22 @@ export default class NotificationBackground {
|
||||
* @param sender - The contextual sender of the message
|
||||
*/
|
||||
async triggerAddLoginNotification(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const login = {
|
||||
url: data.uri,
|
||||
username: data.username,
|
||||
password: data.password || data.newPassword,
|
||||
};
|
||||
|
||||
const authStatus = await this.getAuthStatus();
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const loginInfo = message.login;
|
||||
const normalizedUsername = loginInfo.username ? loginInfo.username.toLowerCase() : "";
|
||||
const loginDomain = Utils.getDomain(loginInfo.url);
|
||||
const normalizedUsername = login.username ? login.username.toLowerCase() : "";
|
||||
const loginDomain = Utils.getDomain(login.url);
|
||||
if (loginDomain == null) {
|
||||
return false;
|
||||
}
|
||||
@ -463,7 +546,7 @@ export default class NotificationBackground {
|
||||
|
||||
if (authStatus === AuthenticationStatus.Locked) {
|
||||
if (addLoginIsEnabled) {
|
||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab, true);
|
||||
await this.pushAddLoginToQueue(loginDomain, login, tab, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -476,12 +559,12 @@ export default class NotificationBackground {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId);
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(login.url, activeUserId);
|
||||
const usernameMatches = ciphers.filter(
|
||||
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
|
||||
);
|
||||
if (addLoginIsEnabled && usernameMatches.length === 0) {
|
||||
await this.pushAddLoginToQueue(loginDomain, loginInfo, sender.tab);
|
||||
await this.pushAddLoginToQueue(loginDomain, login, tab);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -490,14 +573,9 @@ export default class NotificationBackground {
|
||||
if (
|
||||
changePasswordIsEnabled &&
|
||||
usernameMatches.length === 1 &&
|
||||
usernameMatches[0].login.password !== loginInfo.password
|
||||
usernameMatches[0].login.password !== login.password
|
||||
) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
usernameMatches[0].id,
|
||||
loginDomain,
|
||||
loginInfo.password,
|
||||
sender.tab,
|
||||
);
|
||||
await this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, login.password, tab);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -535,23 +613,22 @@ export default class NotificationBackground {
|
||||
* @param sender - The contextual sender of the message
|
||||
*/
|
||||
async triggerChangedPasswordNotification(
|
||||
message: NotificationBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
const changeData = message.data as ChangePasswordMessageData;
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const changeData = {
|
||||
url: data.uri,
|
||||
currentPassword: data.password,
|
||||
newPassword: data.newPassword,
|
||||
};
|
||||
|
||||
const loginDomain = Utils.getDomain(changeData.url);
|
||||
if (loginDomain == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
null,
|
||||
loginDomain,
|
||||
changeData.newPassword,
|
||||
sender.tab,
|
||||
true,
|
||||
);
|
||||
await this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -575,7 +652,7 @@ export default class NotificationBackground {
|
||||
id = ciphers[0].id;
|
||||
}
|
||||
if (id != null) {
|
||||
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, sender.tab);
|
||||
await this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
@ -27,9 +24,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
|
||||
describe("OverlayNotificationsBackground", () => {
|
||||
let logService: MockProxy<LogService>;
|
||||
let notificationBackground: NotificationBackground;
|
||||
let taskService: TaskService;
|
||||
let accountService: AccountService;
|
||||
let cipherService: CipherService;
|
||||
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
|
||||
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
|
||||
let overlayNotificationsBackground: OverlayNotificationsBackground;
|
||||
@ -38,9 +32,6 @@ describe("OverlayNotificationsBackground", () => {
|
||||
jest.useFakeTimers();
|
||||
logService = mock<LogService>();
|
||||
notificationBackground = mock<NotificationBackground>();
|
||||
taskService = mock<TaskService>();
|
||||
accountService = mock<AccountService>();
|
||||
cipherService = mock<CipherService>();
|
||||
getEnableChangedPasswordPromptSpy = jest
|
||||
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
|
||||
.mockResolvedValue(true);
|
||||
@ -50,9 +41,6 @@ describe("OverlayNotificationsBackground", () => {
|
||||
overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
logService,
|
||||
notificationBackground,
|
||||
taskService,
|
||||
accountService,
|
||||
cipherService,
|
||||
);
|
||||
await overlayNotificationsBackground.init();
|
||||
});
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, Subject, switchMap, timer } from "rxjs";
|
||||
import { Subject, switchMap, timer } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar";
|
||||
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
|
||||
|
||||
import {
|
||||
@ -25,12 +20,6 @@ import {
|
||||
} from "./abstractions/overlay-notifications.background";
|
||||
import NotificationBackground from "./notification.background";
|
||||
|
||||
type LoginSecurityTaskInfo = {
|
||||
securityTask: SecurityTask;
|
||||
cipher: CipherView;
|
||||
uri: ModifyLoginCipherFormData["uri"];
|
||||
};
|
||||
|
||||
export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface {
|
||||
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
|
||||
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
|
||||
@ -39,6 +28,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
|
||||
private readonly formSubmissionRequestMethods: Set<string> = new Set(["POST", "PUT", "PATCH"]);
|
||||
private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = {
|
||||
generatedPasswordFilled: ({ message, sender }) =>
|
||||
this.storeModifiedLoginFormData(message, sender),
|
||||
formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender),
|
||||
collectPageDetailsResponse: ({ message, sender }) =>
|
||||
this.handleCollectPageDetailsResponse(message, sender),
|
||||
@ -47,9 +38,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private notificationBackground: NotificationBackground,
|
||||
private taskService: TaskService,
|
||||
private accountService: AccountService,
|
||||
private cipherService: CipherService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -274,8 +262,8 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
const modifyLoginData = this.modifyLoginCipherFormData.get(tabId);
|
||||
return (
|
||||
!modifyLoginData ||
|
||||
!this.shouldAttemptAddLoginNotification(modifyLoginData) ||
|
||||
!this.shouldAttemptChangedPasswordNotification(modifyLoginData)
|
||||
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Add) ||
|
||||
!this.shouldAttemptNotification(modifyLoginData, NotificationTypes.Change)
|
||||
);
|
||||
};
|
||||
|
||||
@ -381,7 +369,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
return;
|
||||
}
|
||||
|
||||
await this.triggerNotificationInit(requestId, modifyLoginData, tab);
|
||||
await this.processNotifications(requestId, modifyLoginData, tab);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -401,171 +389,86 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
const handleWebNavigationOnCompleted = async () => {
|
||||
chrome.webNavigation.onCompleted.removeListener(handleWebNavigationOnCompleted);
|
||||
const tab = await BrowserApi.getTab(tabId);
|
||||
await this.triggerNotificationInit(requestId, modifyLoginData, tab);
|
||||
await this.processNotifications(requestId, modifyLoginData, tab);
|
||||
};
|
||||
chrome.webNavigation.onCompleted.addListener(handleWebNavigationOnCompleted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the add login or change password notification based on the modified login form data
|
||||
* and the tab details. This will trigger the notification to be displayed to the user.
|
||||
* This method attempts to trigger the add login, change password, or at-risk password notifications
|
||||
* based on the modified login data and the tab details.
|
||||
*
|
||||
* @param requestId - The details of the web response
|
||||
* @param modifyLoginData - The modified login form data
|
||||
* @param tab - The tab details
|
||||
*/
|
||||
private triggerNotificationInit = async (
|
||||
private processNotifications = async (
|
||||
requestId: chrome.webRequest.ResourceRequest["requestId"],
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
config: { skippable: NotificationType[] } = { skippable: [] },
|
||||
) => {
|
||||
let result: string;
|
||||
if (this.shouldAttemptChangedPasswordNotification(modifyLoginData)) {
|
||||
// These notifications are temporarily setup as "messages" to the notification background.
|
||||
// This will be structured differently in a future refactor.
|
||||
const success = await this.notificationBackground.triggerChangedPasswordNotification(
|
||||
{
|
||||
command: "bgChangedPassword",
|
||||
data: {
|
||||
url: modifyLoginData.uri,
|
||||
currentPassword: modifyLoginData.password,
|
||||
newPassword: modifyLoginData.newPassword,
|
||||
},
|
||||
},
|
||||
{ tab },
|
||||
);
|
||||
if (!success) {
|
||||
result = "Unqualified changedPassword notification attempt.";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.shouldAttemptAddLoginNotification(modifyLoginData)) {
|
||||
const success = await this.notificationBackground.triggerAddLoginNotification(
|
||||
{
|
||||
command: "bgTriggerAddLoginNotification",
|
||||
login: {
|
||||
url: modifyLoginData.uri,
|
||||
username: modifyLoginData.username,
|
||||
password: modifyLoginData.password || modifyLoginData.newPassword,
|
||||
},
|
||||
},
|
||||
{ tab },
|
||||
);
|
||||
if (!success) {
|
||||
result = "Unqualified addLogin notification attempt.";
|
||||
}
|
||||
}
|
||||
|
||||
const shouldGetTasks =
|
||||
(await this.notificationBackground.getNotificationFlag()) && !modifyLoginData.newPassword;
|
||||
|
||||
if (shouldGetTasks) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (activeUserId) {
|
||||
const loginSecurityTaskInfo = await this.getSecurityTaskAndCipherForLoginData(
|
||||
modifyLoginData,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
if (loginSecurityTaskInfo) {
|
||||
await this.notificationBackground.triggerAtRiskPasswordNotification(
|
||||
{
|
||||
command: "bgTriggerAtRiskPasswordNotification",
|
||||
data: {
|
||||
activeUserId,
|
||||
cipher: loginSecurityTaskInfo.cipher,
|
||||
securityTask: loginSecurityTaskInfo.securityTask,
|
||||
},
|
||||
},
|
||||
{ tab },
|
||||
);
|
||||
} else {
|
||||
result = "Unqualified atRiskPassword notification attempt.";
|
||||
}
|
||||
}
|
||||
}
|
||||
this.clearCompletedWebRequest(requestId, tab);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the change password notification should be triggered.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
*/
|
||||
private shouldAttemptChangedPasswordNotification = (
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
) => {
|
||||
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the add login notification should be triggered.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
*/
|
||||
private shouldAttemptAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => {
|
||||
return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword);
|
||||
};
|
||||
|
||||
/**
|
||||
* If there is a security task for this cipher at login, return the task, cipher view, and uri.
|
||||
*
|
||||
* @param modifyLoginData - The modified login form data
|
||||
* @param activeUserId - The currently logged in user ID
|
||||
*/
|
||||
private async getSecurityTaskAndCipherForLoginData(
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
activeUserId: UserId,
|
||||
): Promise<LoginSecurityTaskInfo | null> {
|
||||
const tasks: SecurityTask[] = await this.notificationBackground.getSecurityTasks(activeUserId);
|
||||
if (!tasks?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlCiphers: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
|
||||
modifyLoginData.uri,
|
||||
activeUserId,
|
||||
);
|
||||
if (!urlCiphers?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const securityTaskForLogin = urlCiphers.reduce(
|
||||
(taskInfo: LoginSecurityTaskInfo | null, cipher: CipherView) => {
|
||||
if (
|
||||
// exit early if info was found already
|
||||
taskInfo ||
|
||||
// exit early if the cipher was deleted
|
||||
cipher.deletedDate ||
|
||||
// exit early if the entered login info doesn't match an existing cipher
|
||||
modifyLoginData.username !== cipher.login.username ||
|
||||
modifyLoginData.password !== cipher.login.password
|
||||
) {
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
// Find the first security task for the cipherId belonging to the entered login
|
||||
const cipherSecurityTask = tasks.find(
|
||||
({ cipherId, status }) =>
|
||||
cipher.id === cipherId && // match security task cipher id to url cipher id
|
||||
status === SecurityTaskStatus.Pending, // security task has not been completed
|
||||
);
|
||||
|
||||
if (cipherSecurityTask) {
|
||||
return { securityTask: cipherSecurityTask, cipher, uri: modifyLoginData.uri };
|
||||
}
|
||||
|
||||
return taskInfo;
|
||||
const notificationCandidates = [
|
||||
{
|
||||
type: NotificationTypes.Change,
|
||||
trigger: this.notificationBackground.triggerChangedPasswordNotification,
|
||||
},
|
||||
null,
|
||||
{
|
||||
type: NotificationTypes.Add,
|
||||
trigger: this.notificationBackground.triggerAddLoginNotification,
|
||||
},
|
||||
{
|
||||
type: NotificationTypes.AtRiskPassword,
|
||||
trigger: this.notificationBackground.triggerAtRiskPasswordNotification,
|
||||
},
|
||||
].filter(
|
||||
(candidate) =>
|
||||
this.shouldAttemptNotification(modifyLoginData, candidate.type) ||
|
||||
config.skippable.includes(candidate.type),
|
||||
);
|
||||
|
||||
return securityTaskForLogin;
|
||||
}
|
||||
const results: string[] = [];
|
||||
for (const { trigger, type } of notificationCandidates) {
|
||||
const success = await trigger.bind(this.notificationBackground)(modifyLoginData, tab);
|
||||
if (success) {
|
||||
results.push(`Success: ${type}`);
|
||||
break;
|
||||
} else {
|
||||
results.push(`Unqualified ${type} notification attempt.`);
|
||||
}
|
||||
}
|
||||
|
||||
this.clearCompletedWebRequest(requestId, tab.id);
|
||||
return results.join(" ");
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the add login notification should be attempted based on the modified login form data.
|
||||
* @param modifyLoginData modified login form data
|
||||
* @param notificationType The type of notification to be triggered
|
||||
* @returns true if the notification should be attempted, false otherwise
|
||||
*/
|
||||
private shouldAttemptNotification = (
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
notificationType: NotificationType,
|
||||
): boolean => {
|
||||
switch (notificationType) {
|
||||
case NotificationTypes.Change:
|
||||
return modifyLoginData?.newPassword && !modifyLoginData.username;
|
||||
case NotificationTypes.Add:
|
||||
return (
|
||||
modifyLoginData?.username && !!(modifyLoginData.password || modifyLoginData.newPassword)
|
||||
);
|
||||
case NotificationTypes.AtRiskPassword:
|
||||
return !modifyLoginData.newPassword;
|
||||
case NotificationTypes.Unlock:
|
||||
// Unlock notifications are handled separately and do not require form data
|
||||
return false;
|
||||
default:
|
||||
this.logService.error(`Unknown notification type: ${notificationType}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the completed web request and removes the modified login form data for the tab.
|
||||
@ -575,11 +478,11 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
*/
|
||||
private clearCompletedWebRequest = (
|
||||
requestId: chrome.webRequest.ResourceRequest["requestId"],
|
||||
tab: chrome.tabs.Tab,
|
||||
tabId: chrome.tabs.Tab["id"],
|
||||
) => {
|
||||
this.activeFormSubmissionRequests.delete(requestId);
|
||||
this.modifyLoginCipherFormData.delete(tab.id);
|
||||
this.websiteOriginsWithFields.delete(tab.id);
|
||||
this.modifyLoginCipherFormData.delete(tabId);
|
||||
this.websiteOriginsWithFields.delete(tabId);
|
||||
this.setupWebRequestsListeners();
|
||||
};
|
||||
|
||||
|
||||
@ -49,7 +49,6 @@ import {
|
||||
MAX_SUB_FRAME_DEPTH,
|
||||
RedirectFocusDirection,
|
||||
} from "../enums/autofill-overlay.enum";
|
||||
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service";
|
||||
import {
|
||||
@ -71,6 +70,7 @@ import {
|
||||
triggerWebRequestOnCompletedEvent,
|
||||
} from "../spec/testing-utils";
|
||||
|
||||
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
|
||||
import {
|
||||
FocusedFieldData,
|
||||
InlineMenuPosition,
|
||||
@ -2076,7 +2076,7 @@ describe("OverlayBackground", () => {
|
||||
const tab = createChromeTabMock({ id: 2 });
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab, frameId: 100 });
|
||||
let focusedFieldData: FocusedFieldData;
|
||||
let formData: InlineMenuFormFieldData;
|
||||
let formData: ModifyLoginCipherFormData;
|
||||
|
||||
beforeEach(async () => {
|
||||
await initOverlayElementPorts();
|
||||
@ -3651,6 +3651,18 @@ describe("OverlayBackground", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a message to the tab to store modify login change when a password is generated", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
|
||||
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(400);
|
||||
await flushPromises();
|
||||
|
||||
expect(tabsSendMessageSpy.mock.lastCall[1].command).toBe("generatedPasswordModifyLogin");
|
||||
});
|
||||
|
||||
it("filters the page details to only include the new password fields before filling", async () => {
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
|
||||
await flushPromises();
|
||||
@ -3663,31 +3675,6 @@ describe("OverlayBackground", () => {
|
||||
allowTotpAutofill: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("opens the inline menu for fields that fill a generated password", async () => {
|
||||
jest.useFakeTimers();
|
||||
const formData = {
|
||||
uri: "https://example.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
tabsSendMessageSpy.mockImplementation((_tab, message) => {
|
||||
if (message.command === "getInlineMenuFormFieldData") {
|
||||
return Promise.resolve(formData);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
const openInlineMenuSpy = jest.spyOn(overlayBackground as any, "openInlineMenu");
|
||||
|
||||
sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey });
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(400);
|
||||
await flushPromises();
|
||||
|
||||
expect(openInlineMenuSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -69,7 +69,6 @@ import {
|
||||
MAX_SUB_FRAME_DEPTH,
|
||||
} from "../enums/autofill-overlay.enum";
|
||||
import AutofillField from "../models/autofill-field";
|
||||
import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service";
|
||||
import { AutofillService, PageDetail } from "../services/abstractions/autofill.service";
|
||||
import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service";
|
||||
import {
|
||||
@ -82,6 +81,7 @@ import {
|
||||
} from "../utils";
|
||||
|
||||
import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background";
|
||||
import { ModifyLoginCipherFormData } from "./abstractions/overlay-notifications.background";
|
||||
import {
|
||||
BuildCipherDataParams,
|
||||
CloseInlineMenuMessage,
|
||||
@ -191,6 +191,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
editedCipher: () => this.updateOverlayCiphers(),
|
||||
deletedCipher: () => this.updateOverlayCiphers(),
|
||||
bgSaveCipher: () => this.updateOverlayCiphers(),
|
||||
updateOverlayCiphers: () => this.updateOverlayCiphers(),
|
||||
fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender.tab.id),
|
||||
};
|
||||
private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = {
|
||||
@ -1812,7 +1813,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
/**
|
||||
* Triggers a fill of the generated password into the current tab. Will trigger
|
||||
* a focus of the last focused field after filling the password.
|
||||
* a focus of the last focused field after filling the password.
|
||||
*
|
||||
* @param port - The port of the sender
|
||||
*/
|
||||
@ -1856,10 +1857,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
});
|
||||
|
||||
globalThis.setTimeout(async () => {
|
||||
if (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)) {
|
||||
await this.openInlineMenu(port.sender, true);
|
||||
}
|
||||
}, 300);
|
||||
await BrowserApi.tabSendMessage(
|
||||
port.sender.tab,
|
||||
{
|
||||
command: "generatedPasswordModifyLogin",
|
||||
},
|
||||
{
|
||||
frameId: this.focusedFieldData.frameId || 0,
|
||||
},
|
||||
);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1890,7 +1897,9 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
*
|
||||
* @param tab - The tab to get the form field data from
|
||||
*/
|
||||
private async getInlineMenuFormFieldData(tab: chrome.tabs.Tab): Promise<InlineMenuFormFieldData> {
|
||||
private async getInlineMenuFormFieldData(
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<ModifyLoginCipherFormData> {
|
||||
return await BrowserApi.tabSendMessage(
|
||||
tab,
|
||||
{
|
||||
|
||||
@ -20,10 +20,8 @@ export default class TabsBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.updateCurrentTabData();
|
||||
this.setupTabEventListeners();
|
||||
void this.updateCurrentTabData();
|
||||
void this.setupTabEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import AutofillPageDetails from "../models/autofill-page-details";
|
||||
@ -122,7 +120,7 @@ class AutofillInit implements AutofillInitInterface {
|
||||
* @param {AutofillExtensionMessage} message
|
||||
*/
|
||||
private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) {
|
||||
if ((document.defaultView || window).location.href !== pageDetailsUrl) {
|
||||
if ((document.defaultView || window).location.href !== pageDetailsUrl || !fillScript) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -177,7 +175,7 @@ class AutofillInit implements AutofillInitInterface {
|
||||
message: AutofillExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response?: any) => void,
|
||||
): boolean => {
|
||||
): boolean | null => {
|
||||
const command: string = message.command;
|
||||
const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command);
|
||||
if (!handler) {
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { setupExtensionDisconnectAction } from "../utils";
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
@ -9,7 +7,7 @@ if (document.readyState === "loading") {
|
||||
}
|
||||
|
||||
function loadAutofiller() {
|
||||
let pageHref: string = null;
|
||||
let pageHref: null | string = null;
|
||||
let filledThisHref = false;
|
||||
let delayFillTimeout: number;
|
||||
let doFillInterval: number | NodeJS.Timeout;
|
||||
@ -51,9 +49,7 @@ function loadAutofiller() {
|
||||
sender: "autofiller",
|
||||
};
|
||||
|
||||
// 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
|
||||
chrome.runtime.sendMessage(msg);
|
||||
void chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
||||
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
||||
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||
@ -11,7 +9,7 @@ import AutofillInit from "./autofill-init";
|
||||
|
||||
(function (windowContext) {
|
||||
if (!windowContext.bitwardenAutofillInit) {
|
||||
let inlineMenuContentService: AutofillInlineMenuContentService;
|
||||
let inlineMenuContentService: undefined | AutofillInlineMenuContentService;
|
||||
if (globalThis.self === globalThis.top) {
|
||||
inlineMenuContentService = new AutofillInlineMenuContentService();
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
|
||||
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
||||
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||
@ -20,7 +18,7 @@ import AutofillInit from "./autofill-init";
|
||||
inlineMenuFieldQualificationService,
|
||||
);
|
||||
|
||||
let overlayNotificationsContentService: OverlayNotificationsContentService;
|
||||
let overlayNotificationsContentService: undefined | OverlayNotificationsContentService;
|
||||
if (globalThis.self === globalThis.top) {
|
||||
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
||||
}
|
||||
@ -29,7 +27,7 @@ import AutofillInit from "./autofill-init";
|
||||
domQueryService,
|
||||
domElementVisibilityService,
|
||||
autofillOverlayContentService,
|
||||
null,
|
||||
undefined,
|
||||
overlayNotificationsContentService,
|
||||
);
|
||||
setupAutofillInitDisconnectAction(windowContext);
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service";
|
||||
import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service";
|
||||
import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service";
|
||||
@ -12,8 +10,8 @@ import AutofillInit from "./autofill-init";
|
||||
|
||||
(function (windowContext) {
|
||||
if (!windowContext.bitwardenAutofillInit) {
|
||||
let inlineMenuContentService: AutofillInlineMenuContentService;
|
||||
let overlayNotificationsContentService: OverlayNotificationsContentService;
|
||||
let inlineMenuContentService: undefined | AutofillInlineMenuContentService;
|
||||
let overlayNotificationsContentService: undefined | OverlayNotificationsContentService;
|
||||
if (globalThis.self === globalThis.top) {
|
||||
inlineMenuContentService = new AutofillInlineMenuContentService();
|
||||
overlayNotificationsContentService = new OverlayNotificationsContentService();
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
import path, { dirname, join } from "path";
|
||||
import { createRequire } from "module";
|
||||
import { dirname, join, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import type { StorybookConfig } from "@storybook/web-components-webpack5";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
|
||||
|
||||
const currentFile = fileURLToPath(import.meta.url);
|
||||
const currentDirectory = dirname(currentFile);
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const getAbsolutePath = (value: string): string =>
|
||||
dirname(require.resolve(join(value, "package.json")));
|
||||
|
||||
@ -43,7 +50,7 @@ const config: StorybookConfig = {
|
||||
if (config.resolve) {
|
||||
config.resolve.plugins = [
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: path.resolve(__dirname, "../../../../../tsconfig.json"),
|
||||
configFile: resolve(currentDirectory, "../../../../../tsconfig.json"),
|
||||
}),
|
||||
] as any;
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { Spinner } from "../icons";
|
||||
|
||||
export type ActionButtonProps = {
|
||||
buttonText: string | TemplateResult;
|
||||
dataTestId?: string;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
theme: Theme;
|
||||
@ -17,6 +18,7 @@ export type ActionButtonProps = {
|
||||
|
||||
export function ActionButton({
|
||||
buttonText,
|
||||
dataTestId,
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
theme,
|
||||
@ -32,6 +34,7 @@ export function ActionButton({
|
||||
return html`
|
||||
<button
|
||||
class=${actionButtonStyles({ disabled, fullWidth, isLoading, theme })}
|
||||
data-testid="${dataTestId}"
|
||||
title=${buttonText}
|
||||
type="button"
|
||||
@click=${handleButtonClick}
|
||||
|
||||
@ -25,7 +25,6 @@ It is designed with accessibility and responsive design in mind.
|
||||
## Installation and Setup
|
||||
|
||||
1. Ensure you have the necessary dependencies installed:
|
||||
|
||||
- `lit`: Used to render the component.
|
||||
- `@emotion/css`: Used for styling the component.
|
||||
|
||||
|
||||
@ -25,7 +25,6 @@ handling, and a disabled state. The component is optimized for accessibility and
|
||||
## Installation and Setup
|
||||
|
||||
1. Ensure you have the necessary dependencies installed:
|
||||
|
||||
- `lit`: Used to render the component.
|
||||
- `@emotion/css`: Used for styling the component.
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ a close icon for visual clarity. The component is designed to be intuitive and a
|
||||
## Installation and Setup
|
||||
|
||||
1. Ensure you have the necessary dependencies installed:
|
||||
|
||||
- `lit`: Used to render the component.
|
||||
- `@emotion/css`: Used for styling the component.
|
||||
|
||||
|
||||
@ -25,7 +25,6 @@ or settings where inline editing is required.
|
||||
## Installation and Setup
|
||||
|
||||
1. Ensure you have the necessary dependencies installed:
|
||||
|
||||
- `lit`: Used to render the component.
|
||||
- `@emotion/css`: Used for styling the component.
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ import { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar";
|
||||
import { getNotificationTestId } from "../../../../../notification/bar";
|
||||
import {
|
||||
AtRiskNotification,
|
||||
AtRiskNotificationProps,
|
||||
@ -30,8 +32,10 @@ export default {
|
||||
},
|
||||
} as Meta<AtRiskNotificationProps>;
|
||||
|
||||
const Template = (args: AtRiskNotificationProps) => AtRiskNotification({ ...args });
|
||||
|
||||
const Template = (args: AtRiskNotificationProps) => {
|
||||
const notificationTestId = getNotificationTestId(NotificationTypes.AtRiskPassword);
|
||||
return AtRiskNotification({ ...args, notificationTestId });
|
||||
};
|
||||
export const Default: StoryObj<AtRiskNotificationProps> = {
|
||||
render: Template,
|
||||
};
|
||||
|
||||
@ -18,11 +18,13 @@ export type AtRiskNotificationProps = NotificationBarIframeInitData & {
|
||||
handleCloseNotification: (e: Event) => void;
|
||||
} & {
|
||||
i18n: I18n;
|
||||
notificationTestId: string;
|
||||
};
|
||||
|
||||
export function AtRiskNotification({
|
||||
handleCloseNotification,
|
||||
i18n,
|
||||
notificationTestId,
|
||||
theme = ThemeTypes.Light,
|
||||
params,
|
||||
}: AtRiskNotificationProps) {
|
||||
@ -33,7 +35,7 @@ export function AtRiskNotification({
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class=${atRiskNotificationContainerStyles(theme)}>
|
||||
<div data-testid="${notificationTestId}" class=${atRiskNotificationContainerStyles(theme)}>
|
||||
${NotificationHeader({
|
||||
handleCloseNotification,
|
||||
i18n,
|
||||
|
||||
@ -26,6 +26,7 @@ export function AtRiskNotificationFooter({
|
||||
open(passwordChangeUri, "_blank");
|
||||
},
|
||||
buttonText: AdditionalTasksButtonContent({ buttonText: i18n.changePassword, theme }),
|
||||
dataTestId: "change-password-button",
|
||||
theme,
|
||||
fullWidth: false,
|
||||
})}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
(function () {
|
||||
// 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
|
||||
chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" });
|
||||
void chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" });
|
||||
})();
|
||||
|
||||
@ -200,7 +200,7 @@ export function getNotificationTestId(
|
||||
[NotificationTypes.Unlock]: "unlock-notification-bar",
|
||||
[NotificationTypes.Add]: "save-notification-bar",
|
||||
[NotificationTypes.Change]: "update-notification-bar",
|
||||
[NotificationTypes.AtRiskPassword]: "at-risk-password-notification-bar",
|
||||
[NotificationTypes.AtRiskPassword]: "at-risk-notification-bar",
|
||||
}[notificationType];
|
||||
}
|
||||
|
||||
@ -287,6 +287,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
type: notificationBarIframeInitData.type as NotificationType,
|
||||
theme: resolvedTheme,
|
||||
i18n,
|
||||
notificationTestId,
|
||||
params: initData.params,
|
||||
handleCloseNotification,
|
||||
}),
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { ModifyLoginCipherFormData } from "../../background/abstractions/overlay-notifications.background";
|
||||
import { SubFrameOffsetData } from "../../background/abstractions/overlay.background";
|
||||
import { AutofillExtensionMessageParam } from "../../content/abstractions/autofill-init";
|
||||
import AutofillField from "../../models/autofill-field";
|
||||
@ -8,13 +9,6 @@ export type SubFrameDataFromWindowMessage = SubFrameOffsetData & {
|
||||
subFrameDepth: number;
|
||||
};
|
||||
|
||||
export type InlineMenuFormFieldData = {
|
||||
uri: string;
|
||||
username: string;
|
||||
password: string;
|
||||
newPassword: string;
|
||||
};
|
||||
|
||||
export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||
[key: string]: CallableFunction;
|
||||
addNewVaultItemFromOverlay: ({ message }: AutofillExtensionMessageParam) => void;
|
||||
@ -32,7 +26,7 @@ export type AutofillOverlayContentExtensionMessageHandlers = {
|
||||
destroyAutofillInlineMenuListeners: () => void;
|
||||
getInlineMenuFormFieldData: ({
|
||||
message,
|
||||
}: AutofillExtensionMessageParam) => Promise<InlineMenuFormFieldData>;
|
||||
}: AutofillExtensionMessageParam) => Promise<ModifyLoginCipherFormData>;
|
||||
};
|
||||
|
||||
export interface AutofillOverlayContentService {
|
||||
|
||||
@ -50,6 +50,15 @@ export class AutoFillConstants {
|
||||
|
||||
static readonly SearchFieldNames: string[] = ["search", "query", "find", "go"];
|
||||
|
||||
static readonly NewEmailFieldKeywords: string[] = [
|
||||
"new-email",
|
||||
"newemail",
|
||||
"new email",
|
||||
"neue e-mail",
|
||||
];
|
||||
|
||||
static readonly NewsletterFormNames: string[] = ["newsletter"];
|
||||
|
||||
static readonly FieldIgnoreList: string[] = ["captcha", "findanything", "forgot"];
|
||||
|
||||
static readonly PasswordFieldExcludeList: string[] = [
|
||||
|
||||
@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
|
||||
import AutofillInit from "../content/autofill-init";
|
||||
import {
|
||||
AutofillOverlayElement,
|
||||
@ -1750,6 +1751,29 @@ describe("AutofillOverlayContentService", () => {
|
||||
});
|
||||
|
||||
describe("extension onMessage handlers", () => {
|
||||
describe("generatedPasswordModifyLogin", () => {
|
||||
it("relays a message regarding password generation to store modified login data", async () => {
|
||||
const formFieldData: ModifyLoginCipherFormData = {
|
||||
newPassword: "newPassword",
|
||||
password: "password",
|
||||
uri: "http://localhost/",
|
||||
username: "username",
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(autofillOverlayContentService as any, "getFormFieldData")
|
||||
.mockResolvedValue(formFieldData);
|
||||
|
||||
sendMockExtensionMessage({
|
||||
command: "generatedPasswordModifyLogin",
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const resolvedValue = await sendExtensionMessageSpy.mock.calls[0][1];
|
||||
expect(resolvedValue).toEqual(formFieldData);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addNewVaultItemFromOverlay message handler", () => {
|
||||
it("skips sending the message if the overlay list is not visible", async () => {
|
||||
jest
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from "@bitwarden/common/autofill/constants";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
|
||||
import { ModifyLoginCipherFormData } from "../background/abstractions/overlay-notifications.background";
|
||||
import {
|
||||
FocusedFieldData,
|
||||
NewCardCipherData,
|
||||
@ -48,7 +49,6 @@ import {
|
||||
import {
|
||||
AutofillOverlayContentExtensionMessageHandlers,
|
||||
AutofillOverlayContentService as AutofillOverlayContentServiceInterface,
|
||||
InlineMenuFormFieldData,
|
||||
SubFrameDataFromWindowMessage,
|
||||
} from "./abstractions/autofill-overlay-content.service";
|
||||
import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service";
|
||||
@ -95,6 +95,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
destroyAutofillInlineMenuListeners: () => this.destroy(),
|
||||
getInlineMenuFormFieldData: ({ message }) =>
|
||||
this.handleGetInlineMenuFormFieldDataMessage(message),
|
||||
generatedPasswordModifyLogin: () => this.sendGeneratedPasswordModifyLogin(),
|
||||
};
|
||||
private readonly loginFieldQualifiers: Record<string, CallableFunction> = {
|
||||
[AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField,
|
||||
@ -235,6 +236,13 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On password generation, send form field data i.e. modified login data
|
||||
*/
|
||||
sendGeneratedPasswordModifyLogin = async () => {
|
||||
await this.sendExtensionMessage("generatedPasswordFilled", this.getFormFieldData());
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats any found user filled fields for a login cipher and sends a message
|
||||
* to the background script to add a new cipher.
|
||||
@ -637,7 +645,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
/**
|
||||
* Returns the form field data used for add login and change password notifications.
|
||||
*/
|
||||
private getFormFieldData = (): InlineMenuFormFieldData => {
|
||||
private getFormFieldData = (): ModifyLoginCipherFormData => {
|
||||
return {
|
||||
uri: globalThis.document.URL,
|
||||
username: this.userFilledFields["username"]?.value || "",
|
||||
|
||||
@ -213,9 +213,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
this.autofillScriptPortsSet.delete(port);
|
||||
});
|
||||
|
||||
// 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.injectAutofillScriptsInAllTabs();
|
||||
void this.injectAutofillScriptsInAllTabs();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -470,9 +468,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
await this.cipherService.updateLastUsedDate(options.cipher.id, activeAccount.id);
|
||||
}
|
||||
|
||||
// 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
|
||||
BrowserApi.tabSendMessage(
|
||||
void BrowserApi.tabSendMessage(
|
||||
tab,
|
||||
{
|
||||
command: options.autoSubmitLogin ? "triggerAutoSubmitLogin" : "fillForm",
|
||||
@ -502,9 +498,10 @@ export default class AutofillService implements AutofillServiceInterface {
|
||||
);
|
||||
|
||||
if (didAutofill) {
|
||||
// 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.eventCollectionService.collect(EventType.Cipher_ClientAutofilled, options.cipher.id);
|
||||
await this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientAutofilled,
|
||||
options.cipher.id,
|
||||
);
|
||||
if (totp !== null) {
|
||||
return totp;
|
||||
} else {
|
||||
|
||||
@ -58,6 +58,8 @@ export class InlineMenuFieldQualificationService
|
||||
"neue e-mail",
|
||||
"pwdcheck",
|
||||
];
|
||||
private newEmailFieldKeywords = new Set(AutoFillConstants.NewEmailFieldKeywords);
|
||||
private newsletterFormKeywords = new Set(AutoFillConstants.NewsletterFormNames);
|
||||
private updatePasswordFieldKeywords = [
|
||||
"update password",
|
||||
"change password",
|
||||
@ -152,6 +154,61 @@ export class InlineMenuFieldQualificationService
|
||||
private totpFieldAutocompleteValue = "one-time-code";
|
||||
private premiumEnabled = false;
|
||||
|
||||
/**
|
||||
* Validates the provided field to indicate if the field is a new email field used for account creation/registration.
|
||||
*
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
private isExplicitIdentityEmailField(field: AutofillField): boolean {
|
||||
const matchFieldAttributeValues = [field.type, field.htmlName, field.htmlID, field.placeholder];
|
||||
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
||||
if (!matchFieldAttributeValues[attrIndex]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let keywordIndex = 0; keywordIndex < matchFieldAttributeValues.length; keywordIndex++) {
|
||||
if (this.newEmailFieldKeywords.has(matchFieldAttributeValues[attrIndex])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provided form to indicate if the form is related to newsletter registration.
|
||||
*
|
||||
* @param parentForm - The form to validate
|
||||
*/
|
||||
private isNewsletterForm(parentForm: any): boolean {
|
||||
if (!parentForm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matchFieldAttributeValues = [
|
||||
parentForm.type,
|
||||
parentForm.htmlName,
|
||||
parentForm.htmlID,
|
||||
parentForm.placeholder,
|
||||
];
|
||||
|
||||
for (let attrIndex = 0; attrIndex < matchFieldAttributeValues.length; attrIndex++) {
|
||||
const attrValue = matchFieldAttributeValues[attrIndex];
|
||||
if (!attrValue || typeof attrValue !== "string") {
|
||||
continue;
|
||||
}
|
||||
const attrValueLower = attrValue.toLowerCase();
|
||||
for (const keyword of this.newsletterFormKeywords) {
|
||||
if (attrValueLower.includes(keyword.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
void Promise.all([
|
||||
sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"),
|
||||
@ -300,7 +357,11 @@ export class InlineMenuFieldQualificationService
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues);
|
||||
return (
|
||||
// Recognize explicit identity email fields (like id="new-email")
|
||||
this.isFieldForIdentityEmail(field) ||
|
||||
this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -397,6 +458,12 @@ export class InlineMenuFieldQualificationService
|
||||
): boolean {
|
||||
// If the provided field is set with an autocomplete of "username", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a username field.
|
||||
|
||||
// Exclude non-login email field from being treated as a login username field
|
||||
if (this.isExplicitIdentityEmailField(field)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.fieldContainsAutocompleteValues(field, this.loginUsernameAutocompleteValues)) {
|
||||
const newPasswordFieldsInPageDetails = pageDetails.fields.filter(
|
||||
(field) => field.viewable && this.isNewPasswordField(field),
|
||||
@ -415,6 +482,10 @@ export class InlineMenuFieldQualificationService
|
||||
const parentForm = pageDetails.forms[field.form];
|
||||
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isCurrentPasswordField);
|
||||
|
||||
if (this.isNewsletterForm(parentForm)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the field is not structured within a form, we need to identify if the field is used in conjunction
|
||||
// with a password field. If that's the case, then we should assume that it is a form field element.
|
||||
if (!parentForm) {
|
||||
@ -822,9 +893,14 @@ export class InlineMenuFieldQualificationService
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
isFieldForIdentityEmail = (field: AutofillField): boolean => {
|
||||
if (this.isExplicitIdentityEmailField(field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(field, this.emailAutocompleteValue) ||
|
||||
field.type === "email"
|
||||
field.type === "email" ||
|
||||
field.htmlName === "email"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@ -15,7 +13,7 @@ const IdleInterval = 60 * 5; // 5 minutes
|
||||
|
||||
export default class IdleBackground {
|
||||
private idle: typeof chrome.idle | typeof browser.idle | null;
|
||||
private idleTimer: number | NodeJS.Timeout = null;
|
||||
private idleTimer: null | number | NodeJS.Timeout = null;
|
||||
private idleState = "active";
|
||||
|
||||
constructor(
|
||||
@ -80,9 +78,8 @@ export default class IdleBackground {
|
||||
globalThis.clearTimeout(this.idleTimer);
|
||||
this.idleTimer = null;
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.idle.queryState(IdleInterval, (state: string) => {
|
||||
|
||||
void this.idle?.queryState(IdleInterval, (state: string) => {
|
||||
if (state !== this.idleState) {
|
||||
this.idleState = state;
|
||||
handler(state);
|
||||
|
||||
@ -14,8 +14,6 @@ import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
LogoutReason,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
@ -82,6 +80,8 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import {
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@ -898,6 +898,7 @@ export default class MainBackground {
|
||||
this.accountService,
|
||||
this.logService,
|
||||
this.cipherEncryptionService,
|
||||
this.messagingService,
|
||||
);
|
||||
this.folderService = new FolderService(
|
||||
this.keyService,
|
||||
@ -1257,9 +1258,6 @@ export default class MainBackground {
|
||||
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
|
||||
this.logService,
|
||||
this.notificationBackground,
|
||||
this.taskService,
|
||||
this.accountService,
|
||||
this.cipherService,
|
||||
);
|
||||
|
||||
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
|
||||
@ -1374,6 +1372,7 @@ export default class MainBackground {
|
||||
this.badgeService = new BadgeService(
|
||||
this.stateProvider,
|
||||
new DefaultBadgeBrowserApi(this.platformUtilsService),
|
||||
this.logService,
|
||||
);
|
||||
this.authStatusBadgeUpdaterService = new AuthStatusBadgeUpdaterService(
|
||||
this.badgeService,
|
||||
|
||||
@ -48,7 +48,7 @@ export default class RuntimeBackground {
|
||||
private platformUtilsService: BrowserPlatformUtilsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private processReloadSerivce: ProcessReloadServiceAbstraction,
|
||||
private processReloadService: ProcessReloadServiceAbstraction,
|
||||
private environmentService: BrowserEnvironmentService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
@ -241,7 +241,7 @@ export default class RuntimeBackground {
|
||||
await closeUnlockPopout();
|
||||
}
|
||||
|
||||
this.processReloadSerivce.cancelProcessReload();
|
||||
this.processReloadService.cancelProcessReload();
|
||||
|
||||
if (item) {
|
||||
await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId);
|
||||
|
||||
@ -4,10 +4,8 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@ -3,10 +3,8 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
BiometricsService,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.7.0",
|
||||
"version": "2025.7.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.7.0",
|
||||
"version": "2025.7.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||
|
||||
import { BadgeIcon, IconPaths } from "./icon";
|
||||
|
||||
@ -13,6 +16,8 @@ export interface RawBadgeState {
|
||||
}
|
||||
|
||||
export interface BadgeBrowserApi {
|
||||
activeTab$: Observable<chrome.tabs.TabActiveInfo | undefined>;
|
||||
|
||||
setState(state: RawBadgeState, tabId?: number): Promise<void>;
|
||||
getTabs(): Promise<number[]>;
|
||||
}
|
||||
@ -21,6 +26,10 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi {
|
||||
private badgeAction = BrowserApi.getBrowserAction();
|
||||
private sidebarAction = BrowserApi.getSidebarAction(self);
|
||||
|
||||
activeTab$ = fromChromeEvent(chrome.tabs.onActivated).pipe(
|
||||
map(([tabActiveInfo]) => tabActiveInfo),
|
||||
);
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async setState(state: RawBadgeState, tabId?: number): Promise<void> {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { Subscription } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { FakeAccountService, FakeStateProvider } from "@bitwarden/common/spec";
|
||||
|
||||
import { RawBadgeState } from "./badge-browser-api";
|
||||
@ -13,6 +15,7 @@ import { MockBadgeBrowserApi } from "./test/mock-badge-browser-api";
|
||||
describe("BadgeService", () => {
|
||||
let badgeApi: MockBadgeBrowserApi;
|
||||
let stateProvider: FakeStateProvider;
|
||||
let logService!: MockProxy<LogService>;
|
||||
let badgeService!: BadgeService;
|
||||
|
||||
let badgeServiceSubscription: Subscription;
|
||||
@ -20,8 +23,9 @@ describe("BadgeService", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi = new MockBadgeBrowserApi();
|
||||
stateProvider = new FakeStateProvider(new FakeAccountService({}));
|
||||
logService = mock<LogService>();
|
||||
|
||||
badgeService = new BadgeService(stateProvider, badgeApi);
|
||||
badgeService = new BadgeService(stateProvider, badgeApi, logService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -34,14 +38,10 @@ describe("BadgeService", () => {
|
||||
describe("given a single tab is open", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = [1];
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
// This relies on the state provider to auto-emit
|
||||
it("sets default values on startup", async () => {
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
});
|
||||
|
||||
it("sets provided state when no other state has been set", async () => {
|
||||
const state: BadgeState = {
|
||||
text: "text",
|
||||
@ -52,7 +52,6 @@ describe("BadgeService", () => {
|
||||
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(state);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(state);
|
||||
});
|
||||
|
||||
@ -63,7 +62,6 @@ describe("BadgeService", () => {
|
||||
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
|
||||
});
|
||||
|
||||
@ -82,7 +80,6 @@ describe("BadgeService", () => {
|
||||
backgroundColor: "#fff",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
expect(badgeApi.generalState).toEqual(expectedState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
|
||||
});
|
||||
|
||||
@ -105,7 +102,6 @@ describe("BadgeService", () => {
|
||||
backgroundColor: "#aaa",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
expect(badgeApi.generalState).toEqual(expectedState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
|
||||
});
|
||||
|
||||
@ -126,7 +122,6 @@ describe("BadgeService", () => {
|
||||
backgroundColor: "#fff",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
expect(badgeApi.generalState).toEqual(expectedState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
|
||||
});
|
||||
|
||||
@ -147,7 +142,6 @@ describe("BadgeService", () => {
|
||||
await badgeService.clearState("state-3");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
|
||||
});
|
||||
|
||||
@ -167,7 +161,6 @@ describe("BadgeService", () => {
|
||||
backgroundColor: "#fff",
|
||||
icon: DefaultBadgeState.icon,
|
||||
};
|
||||
expect(badgeApi.generalState).toEqual(expectedState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
|
||||
});
|
||||
|
||||
@ -190,26 +183,20 @@ describe("BadgeService", () => {
|
||||
backgroundColor: "#fff",
|
||||
icon: BadgeIcon.Unlocked,
|
||||
};
|
||||
expect(badgeApi.generalState).toEqual(expectedState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given multiple tabs are open", () => {
|
||||
const tabId = 1;
|
||||
const tabIds = [1, 2, 3];
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
it("sets default values for each tab on startup", async () => {
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
for (const tabId of tabIds) {
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
|
||||
}
|
||||
});
|
||||
|
||||
it("sets state for each tab when no other state has been set", async () => {
|
||||
const state: BadgeState = {
|
||||
text: "text",
|
||||
@ -220,11 +207,10 @@ describe("BadgeService", () => {
|
||||
await badgeService.setState("state-name", BadgeStatePriority.Default, state);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(state);
|
||||
expect(badgeApi.specificStates).toEqual({
|
||||
1: state,
|
||||
2: state,
|
||||
3: state,
|
||||
2: undefined,
|
||||
3: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -236,6 +222,7 @@ describe("BadgeService", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = [tabId];
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@ -249,7 +236,6 @@ describe("BadgeService", () => {
|
||||
await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(state);
|
||||
});
|
||||
|
||||
@ -260,7 +246,6 @@ describe("BadgeService", () => {
|
||||
await badgeService.setState("state-name", BadgeStatePriority.Default, state, tabId);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
|
||||
});
|
||||
|
||||
@ -279,11 +264,6 @@ describe("BadgeService", () => {
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual({
|
||||
...DefaultBadgeState,
|
||||
text: "text",
|
||||
icon: BadgeIcon.Locked,
|
||||
});
|
||||
expect(badgeApi.specificStates[tabId]).toEqual({
|
||||
text: "text",
|
||||
backgroundColor: "#fff",
|
||||
@ -316,7 +296,6 @@ describe("BadgeService", () => {
|
||||
backgroundColor: "#fff",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
|
||||
});
|
||||
|
||||
@ -354,7 +333,6 @@ describe("BadgeService", () => {
|
||||
backgroundColor: "#aaa",
|
||||
icon: BadgeIcon.Locked,
|
||||
};
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(expectedState);
|
||||
});
|
||||
|
||||
@ -377,11 +355,6 @@ describe("BadgeService", () => {
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual({
|
||||
text: "override",
|
||||
backgroundColor: "#aaa",
|
||||
icon: DefaultBadgeState.icon,
|
||||
});
|
||||
expect(badgeApi.specificStates[tabId]).toEqual({
|
||||
text: "override",
|
||||
backgroundColor: "#aaa",
|
||||
@ -411,7 +384,6 @@ describe("BadgeService", () => {
|
||||
await badgeService.clearState("state-2");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual({
|
||||
text: "text",
|
||||
backgroundColor: "#fff",
|
||||
@ -451,7 +423,6 @@ describe("BadgeService", () => {
|
||||
await badgeService.clearState("state-3");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual(DefaultBadgeState);
|
||||
});
|
||||
|
||||
@ -476,7 +447,6 @@ describe("BadgeService", () => {
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual({
|
||||
text: "text",
|
||||
backgroundColor: "#fff",
|
||||
@ -513,7 +483,6 @@ describe("BadgeService", () => {
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(DefaultBadgeState);
|
||||
expect(badgeApi.specificStates[tabId]).toEqual({
|
||||
text: "text",
|
||||
backgroundColor: "#fff",
|
||||
@ -523,14 +492,16 @@ describe("BadgeService", () => {
|
||||
});
|
||||
|
||||
describe("given multiple tabs are open", () => {
|
||||
const tabId = 1;
|
||||
const tabIds = [1, 2, 3];
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
it("sets tab-specific state for provided tab and general state for the others", async () => {
|
||||
it("sets tab-specific state for provided tab", async () => {
|
||||
const generalState: BadgeState = {
|
||||
text: "general-text",
|
||||
backgroundColor: "general-color",
|
||||
@ -550,11 +521,10 @@ describe("BadgeService", () => {
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(badgeApi.generalState).toEqual(generalState);
|
||||
expect(badgeApi.specificStates).toEqual({
|
||||
[tabIds[0]]: { ...specificState, backgroundColor: "general-color" },
|
||||
[tabIds[1]]: generalState,
|
||||
[tabIds[2]]: generalState,
|
||||
[tabIds[1]]: undefined,
|
||||
[tabIds[2]]: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import {
|
||||
defer,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
mergeMap,
|
||||
pairwise,
|
||||
startWith,
|
||||
Subscription,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
BADGE_MEMORY,
|
||||
GlobalState,
|
||||
@ -39,6 +39,7 @@ export class BadgeService {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private badgeApi: BadgeBrowserApi,
|
||||
private logService: LogService,
|
||||
) {
|
||||
this.states = this.stateProvider.getGlobal(BADGE_STATES);
|
||||
}
|
||||
@ -48,52 +49,47 @@ export class BadgeService {
|
||||
* Without this the service will not be able to update the badge state.
|
||||
*/
|
||||
startListening(): Subscription {
|
||||
const initialSetup$ = defer(async () => {
|
||||
const openTabs = await this.badgeApi.getTabs();
|
||||
await this.badgeApi.setState(DefaultBadgeState);
|
||||
for (const tabId of openTabs) {
|
||||
await this.badgeApi.setState(DefaultBadgeState, tabId);
|
||||
}
|
||||
});
|
||||
|
||||
return initialSetup$
|
||||
.pipe(
|
||||
switchMap(() => this.states.state$),
|
||||
return combineLatest({
|
||||
states: this.states.state$.pipe(
|
||||
startWith({}),
|
||||
distinctUntilChanged(),
|
||||
map((states) => new Set(states ? Object.values(states) : [])),
|
||||
pairwise(),
|
||||
map(([previous, current]) => {
|
||||
const [removed, added] = difference(previous, current);
|
||||
return { states: current, removed, added };
|
||||
return { all: current, removed, added };
|
||||
}),
|
||||
filter(({ removed, added }) => removed.size > 0 || added.size > 0),
|
||||
mergeMap(async ({ states, removed, added }) => {
|
||||
const changed = [...removed, ...added];
|
||||
const changedTabIds = new Set(
|
||||
changed.map((s) => s.tabId).filter((tabId) => tabId !== undefined),
|
||||
);
|
||||
const onlyTabSpecificStatesChanged = changed.every((s) => s.tabId != undefined);
|
||||
if (onlyTabSpecificStatesChanged) {
|
||||
// If only tab-specific states changed then we only need to update those specific tabs.
|
||||
for (const tabId of changedTabIds) {
|
||||
const newState = this.calculateState(states, tabId);
|
||||
await this.badgeApi.setState(newState, tabId);
|
||||
}
|
||||
),
|
||||
activeTab: this.badgeApi.activeTab$.pipe(startWith(undefined)),
|
||||
})
|
||||
.pipe(
|
||||
concatMap(async ({ states, activeTab }) => {
|
||||
const changed = [...states.removed, ...states.added];
|
||||
|
||||
// If the active tab wasn't changed, we don't need to update the badge.
|
||||
if (!changed.some((s) => s.tabId === activeTab?.tabId || s.tabId === undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are any general states that changed then we need to update all tabs.
|
||||
const openTabs = await this.badgeApi.getTabs();
|
||||
const generalState = this.calculateState(states);
|
||||
await this.badgeApi.setState(generalState);
|
||||
for (const tabId of openTabs) {
|
||||
const newState = this.calculateState(states, tabId);
|
||||
await this.badgeApi.setState(newState, tabId);
|
||||
try {
|
||||
const state = this.calculateState(states.all, activeTab?.tabId);
|
||||
await this.badgeApi.setState(state, activeTab?.tabId);
|
||||
} catch (error) {
|
||||
// This usually happens when the user opens a popout because of how the browser treats it
|
||||
// as a tab in the same window but then won't let you set the badge state for it.
|
||||
this.logService.warning("Failed to set badge state", error);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
.subscribe({
|
||||
error: (err: unknown) => {
|
||||
this.logService.error(
|
||||
"Fatal error in badge service observable, badge will fail to update",
|
||||
err,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,10 +1,22 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { BadgeBrowserApi, RawBadgeState } from "../badge-browser-api";
|
||||
|
||||
export class MockBadgeBrowserApi implements BadgeBrowserApi {
|
||||
private _activeTab$ = new BehaviorSubject<chrome.tabs.TabActiveInfo | undefined>(undefined);
|
||||
activeTab$ = this._activeTab$.asObservable();
|
||||
|
||||
specificStates: Record<number, RawBadgeState> = {};
|
||||
generalState?: RawBadgeState;
|
||||
tabs: number[] = [];
|
||||
|
||||
setActiveTab(tabId: number) {
|
||||
this._activeTab$.next({
|
||||
tabId,
|
||||
windowId: 1,
|
||||
});
|
||||
}
|
||||
|
||||
setState(state: RawBadgeState, tabId?: number): Promise<void> {
|
||||
if (tabId !== undefined) {
|
||||
this.specificStates[tabId] = state;
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
<!--
|
||||
end padding is less than start padding to prioritize visual alignment when icon buttons are used at the end of the end slot.
|
||||
other elements used at the end of the end slot may need to add their own margin/padding to achieve visual alignment.
|
||||
-->
|
||||
<header
|
||||
class="tw-p-3 bit-compact:tw-p-2 tw-pl-4 bit-compact:tw-pl-3 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
|
||||
class="tw-py-3 bit-compact:tw-py-2 tw-pe-1 bit-compact:tw-pe-0.5 tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid"
|
||||
[ngClass]="{
|
||||
'tw-bg-background-alt tw-border-transparent':
|
||||
this.background === 'alt' && !pageContentScrolled(),
|
||||
'tw-bg-background tw-border-secondary-300':
|
||||
(this.background === 'alt' && pageContentScrolled()) || this.background === 'default',
|
||||
'tw-ps-4 bit-compact:tw-ps-3': !showBackButton,
|
||||
'tw-ps-1 bit-compact:tw-ps-0': showBackButton,
|
||||
}"
|
||||
>
|
||||
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
||||
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||
<button
|
||||
class="-tw-ml-1"
|
||||
bitIconButton="bwi-angle-left"
|
||||
type="button"
|
||||
*ngIf="showBackButton"
|
||||
|
||||
@ -117,7 +117,7 @@ class MockPopoutButtonComponent {}
|
||||
@Component({
|
||||
selector: "mock-current-account",
|
||||
template: `
|
||||
<button class="tw-bg-transparent tw-border-none" type="button">
|
||||
<button class="tw-bg-transparent tw-border-none tw-p-0 tw-me-1" type="button">
|
||||
<bit-avatar text="Ash Ketchum" size="small"></bit-avatar>
|
||||
</button>
|
||||
`,
|
||||
@ -654,7 +654,7 @@ export const WithVirtualScrollChild: Story = {
|
||||
<bit-section>
|
||||
@defer (on immediate) {
|
||||
<bit-item-group aria-label="Mock Vault Items">
|
||||
<cdk-virtual-scroll-viewport itemSize="61" bitScrollLayout>
|
||||
<cdk-virtual-scroll-viewport itemSize="59" bitScrollLayout>
|
||||
<bit-item *cdkVirtualFor="let item of data; index as i">
|
||||
<button type="button" bit-item-content>
|
||||
<i
|
||||
|
||||
@ -50,8 +50,8 @@ describe("LocalBackedSessionStorage", () => {
|
||||
const result = await sut.get("test");
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted");
|
||||
(expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted"));
|
||||
});
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
@ -69,8 +69,8 @@ describe("LocalBackedSessionStorage", () => {
|
||||
const result = await sut.get("test");
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted");
|
||||
(expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted"));
|
||||
});
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
|
||||
@ -33,7 +33,6 @@ import {
|
||||
import {
|
||||
LockService,
|
||||
LoginEmailService,
|
||||
PinServiceAbstraction,
|
||||
SsoUrlService,
|
||||
LogoutService,
|
||||
} from "@bitwarden/auth/common";
|
||||
@ -67,6 +66,7 @@ import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/a
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
VaultTimeoutService,
|
||||
VaultTimeoutStringType,
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */ = {isa = PBXBuildFile; fileRef = 03100CAE291891F4008E14EF /* encrypt-worker.js */; };
|
||||
55BC93932CB4268A008CA4C6 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 55BC93922CB4268A008CA4C6 /* assets */; };
|
||||
55E0374D2577FA6B00979016 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E0374C2577FA6B00979016 /* AppDelegate.swift */; };
|
||||
55E037502577FA6B00979016 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55E0374E2577FA6B00979016 /* Main.storyboard */; };
|
||||
@ -53,7 +52,6 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
03100CAE291891F4008E14EF /* encrypt-worker.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "encrypt-worker.js"; path = "../../../build/encrypt-worker.js"; sourceTree = "<group>"; };
|
||||
5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftAppKit.tbd; path = usr/lib/swift/libswiftAppKit.tbd; sourceTree = SDKROOT; };
|
||||
55BC93922CB4268A008CA4C6 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../build/assets; sourceTree = "<group>"; };
|
||||
55E037482577FA6B00979016 /* desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -155,7 +153,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
55BC93922CB4268A008CA4C6 /* assets */,
|
||||
03100CAE291891F4008E14EF /* encrypt-worker.js */,
|
||||
55E037702577FA6F00979016 /* popup */,
|
||||
55E037712577FA6F00979016 /* background.js */,
|
||||
55E037722577FA6F00979016 /* images */,
|
||||
@ -272,7 +269,6 @@
|
||||
55E037802577FA6F00979016 /* background.html in Resources */,
|
||||
55E0377A2577FA6F00979016 /* background.js in Resources */,
|
||||
55E037792577FA6F00979016 /* popup in Resources */,
|
||||
03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */,
|
||||
55BC93932CB4268A008CA4C6 /* assets in Resources */,
|
||||
55E0377C2577FA6F00979016 /* notification in Resources */,
|
||||
55E0377E2577FA6F00979016 /* vendor.js in Resources */,
|
||||
|
||||
@ -10,7 +10,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@ -163,7 +163,7 @@ describe("OpenAttachmentsComponent", () => {
|
||||
it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => {
|
||||
cipherView.organizationId = "888-333-333";
|
||||
org.productTierType = ProductTierType.Free;
|
||||
org.id = cipherView.organizationId;
|
||||
org.id = cipherView.organizationId as OrganizationId;
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
@ -173,7 +173,7 @@ describe("OpenAttachmentsComponent", () => {
|
||||
it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => {
|
||||
cipherView.organizationId = "888-333-333";
|
||||
org.productTierType = ProductTierType.Families;
|
||||
org.id = cipherView.organizationId;
|
||||
org.id = cipherView.organizationId as OrganizationId;
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
<value>Bitwarden Lösenordshanterare</value>
|
||||
</data>
|
||||
<data name="Summary" xml:space="preserve">
|
||||
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, passkeys, och känslig information.</value>
|
||||
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, inloggningsnycklar och känslig information.</value>
|
||||
</data>
|
||||
<data name="Description" xml:space="preserve">
|
||||
<value>Erkänd som den bästa lösenordshanteraren av PCMag, WIRED, The Verge, CNET, G2 och många fler!
|
||||
@ -169,7 +169,7 @@ End-to-end krypterade lösningar för hantering av referenser från Bitwarden g
|
||||
</value>
|
||||
</data>
|
||||
<data name="AssetTitle" xml:space="preserve">
|
||||
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, passkeys, och känslig information.</value>
|
||||
<value>Hemma, på jobbet eller på resande fot säkrar Bitwarden enkelt alla dina lösenord, inloggningsnycklar och känslig information.</value>
|
||||
</data>
|
||||
<data name="ScreenshotSync" xml:space="preserve">
|
||||
<value>Synkronisera och kom åt ditt valv från flera enheter</value>
|
||||
|
||||
@ -1 +0,0 @@
|
||||
v20
|
||||
@ -1,13 +1,20 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserConfirmRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { EncString } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { Response } from "../../models/response";
|
||||
|
||||
@ -17,6 +24,8 @@ export class ConfirmCommand {
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private configService: ConfigService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
|
||||
@ -60,6 +69,11 @@ export class ConfirmCommand {
|
||||
const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey);
|
||||
const req = new OrganizationUserConfirmRequest();
|
||||
req.key = key.encryptedString;
|
||||
if (
|
||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
||||
) {
|
||||
req.defaultUserCollectionName = await this.getEncryptedDefaultUserCollectionName(orgKey);
|
||||
}
|
||||
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||
options.organizationId,
|
||||
id,
|
||||
@ -70,6 +84,12 @@ export class ConfirmCommand {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getEncryptedDefaultUserCollectionName(orgKey: OrgKey): Promise<EncString> {
|
||||
const defaultCollectionName = this.i18nService.t("myItems");
|
||||
const encrypted = await this.encryptService.encryptString(defaultCollectionName, orgKey);
|
||||
return encrypted.encryptedString;
|
||||
}
|
||||
}
|
||||
|
||||
class Options {
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { SelectionReadOnly } from "../selection-read-only";
|
||||
|
||||
export class OrganizationCollectionRequest extends CollectionExport {
|
||||
static template(): OrganizationCollectionRequest {
|
||||
const req = new OrganizationCollectionRequest();
|
||||
req.organizationId = "00000000-0000-0000-0000-000000000000";
|
||||
req.organizationId = "00000000-0000-0000-0000-000000000000" as OrganizationId;
|
||||
req.name = "Collection name";
|
||||
req.externalId = null;
|
||||
req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()];
|
||||
|
||||
@ -170,7 +170,7 @@ export class EditCommand {
|
||||
let folderView = await folder.decrypt();
|
||||
folderView = FolderExport.toView(req, folderView);
|
||||
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
|
||||
const userKey = await this.keyService.getUserKey(activeUserId);
|
||||
const encFolder = await this.folderService.encrypt(folderView, userKey);
|
||||
try {
|
||||
const folder = await this.folderApiService.save(encFolder, activeUserId);
|
||||
|
||||
@ -215,5 +215,8 @@
|
||||
},
|
||||
"youHaveBeenLoggedOut": {
|
||||
"message": "You have been logged out."
|
||||
},
|
||||
"myItems": {
|
||||
"message": "My Items"
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +131,8 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.keyService,
|
||||
this.serviceContainer.encryptService,
|
||||
this.serviceContainer.organizationUserApiService,
|
||||
this.serviceContainer.configService,
|
||||
this.serviceContainer.i18nService,
|
||||
);
|
||||
this.restoreCommand = new RestoreCommand(
|
||||
this.serviceContainer.cipherService,
|
||||
|
||||
@ -16,8 +16,6 @@ import {
|
||||
AuthRequestService,
|
||||
LoginStrategyService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
SsoUrlService,
|
||||
AuthRequestApiServiceAbstraction,
|
||||
@ -68,6 +66,8 @@ import { DeviceTrustService } from "@bitwarden/common/key-management/device-trus
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PinService } from "@bitwarden/common/key-management/pin/pin.service.implementation";
|
||||
import {
|
||||
DefaultVaultTimeoutService,
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@ -723,6 +723,7 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
this.logService,
|
||||
this.cipherEncryptionService,
|
||||
this.messagingService,
|
||||
);
|
||||
|
||||
this.folderService = new FolderService(
|
||||
|
||||
@ -149,11 +149,11 @@ export class SendProgram extends BaseProgram {
|
||||
|
||||
private templateCommand(): Command {
|
||||
return new Command("template")
|
||||
.argument("<object>", "Valid objects are: send.text, send.file")
|
||||
.argument("<object>", "Valid objects are: send.text, text, send.file, file")
|
||||
.description("Get json templates for send objects")
|
||||
.action((options: OptionValues) =>
|
||||
this.processResponse(new SendTemplateCommand().run(options.object)),
|
||||
);
|
||||
.action((object: string) => {
|
||||
this.processResponse(new SendTemplateCommand().run(object));
|
||||
});
|
||||
}
|
||||
|
||||
private getCommand(): Command {
|
||||
|
||||
@ -432,6 +432,8 @@ export class VaultProgram extends BaseProgram {
|
||||
this.serviceContainer.keyService,
|
||||
this.serviceContainer.encryptService,
|
||||
this.serviceContainer.organizationUserApiService,
|
||||
this.serviceContainer.configService,
|
||||
this.serviceContainer.i18nService,
|
||||
);
|
||||
const response = await command.run(object, id, cmd);
|
||||
this.processResponse(response);
|
||||
|
||||
@ -180,7 +180,7 @@ export class CreateCommand {
|
||||
|
||||
private async createFolder(req: FolderExport) {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
|
||||
const userKey = await this.keyService.getUserKey(activeUserId);
|
||||
const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey);
|
||||
try {
|
||||
await this.folderApiService.save(folder, activeUserId);
|
||||
|
||||
@ -3,8 +3,8 @@ use std::os::windows::ffi::OsStringExt;
|
||||
|
||||
use windows::Win32::Foundation::{GetLastError, HWND};
|
||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
BlockInput, SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP,
|
||||
KEYEVENTF_UNICODE,
|
||||
SendInput, INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, KEYEVENTF_UNICODE,
|
||||
VIRTUAL_KEY,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW,
|
||||
@ -28,21 +28,31 @@ pub fn get_foreground_window_title() -> std::result::Result<String, ()> {
|
||||
///
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
pub fn type_input(input: Vec<u16>) -> Result<(), ()> {
|
||||
const TAB_KEY: u16 = 9;
|
||||
let mut keyboard_inputs: Vec<INPUT> = Vec::new();
|
||||
|
||||
// Release hotkeys
|
||||
keyboard_inputs.push(build_virtual_key_input(InputKeyPress::Up, 0x12)); // alt
|
||||
keyboard_inputs.push(build_virtual_key_input(InputKeyPress::Up, 0x11)); // ctrl
|
||||
keyboard_inputs.push(build_unicode_input(InputKeyPress::Up, 105)); // i
|
||||
|
||||
for i in input {
|
||||
let next_down_input = build_input(InputKeyPress::Down, i);
|
||||
let next_up_input = build_input(InputKeyPress::Up, i);
|
||||
let next_down_input = if i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Down, i as u8)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Down, i)
|
||||
};
|
||||
let next_up_input = if i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Up, i as u8)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Up, i)
|
||||
};
|
||||
|
||||
keyboard_inputs.push(next_down_input);
|
||||
keyboard_inputs.push(next_up_input);
|
||||
}
|
||||
|
||||
let _ = block_input(true);
|
||||
let result = send_input(keyboard_inputs);
|
||||
let _ = block_input(false);
|
||||
|
||||
result
|
||||
send_input(keyboard_inputs)
|
||||
}
|
||||
|
||||
/// Gets the foreground window handle.
|
||||
@ -103,11 +113,11 @@ enum InputKeyPress {
|
||||
Up,
|
||||
}
|
||||
|
||||
/// A function for easily building keyboard INPUT structs used in SendInput().
|
||||
/// A function for easily building keyboard unicode INPUT structs used in SendInput().
|
||||
///
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
fn build_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
@ -136,14 +146,37 @@ fn build_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
}
|
||||
}
|
||||
|
||||
/// Block keyboard and mouse input events. This prevents the hotkey
|
||||
/// key presses from interfering with the input sent via SendInput().
|
||||
/// A function for easily building keyboard virtual-key INPUT structs used in SendInput().
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-blockinput
|
||||
fn block_input(block: bool) -> Result<(), ()> {
|
||||
match unsafe { BlockInput(block) } {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => Err(()),
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: Default::default(),
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
InputKeyPress::Up => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: KEYEVENTF_KEYUP,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
"module-alias": "2.2.3",
|
||||
"ts-node": "10.9.2",
|
||||
"uuid": "11.1.0",
|
||||
"yargs": "17.7.2"
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.15.3",
|
||||
@ -150,24 +150,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
@ -180,37 +180,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
"string-width": "^7.2.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"wrap-ansi": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
@ -227,9 +209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@ -250,13 +232,16 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
|
||||
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
@ -271,39 +256,36 @@
|
||||
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
@ -388,17 +370,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
|
||||
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
@ -414,30 +396,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"cliui": "^9.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"string-width": "^7.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
"yargs-parser": "^22.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
|
||||
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": "^20.19.0 || ^22.12.0 || >=23"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
"module-alias": "2.2.3",
|
||||
"ts-node": "10.9.2",
|
||||
"uuid": "11.1.0",
|
||||
"yargs": "17.7.2"
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.15.3",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.7.0",
|
||||
"version": "2025.8.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@ -4,7 +4,6 @@ import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
@ -13,6 +12,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
|
||||
@ -9,7 +9,6 @@ import { concatMap, map, pairwise, startWith, switchMap, takeUntil, timeout } fr
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
|
||||
@ -20,6 +19,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
|
||||
@ -24,11 +24,12 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
DESKTOP_SSO_CALLBACK,
|
||||
LogoutReason,
|
||||
@ -476,7 +477,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
case "openLoginApproval":
|
||||
if (message.notificationId != null) {
|
||||
this.dialogService.closeAll();
|
||||
const dialogRef = LoginApprovalComponent.open(this.dialogService, {
|
||||
const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, {
|
||||
notificationId: message.notificationId,
|
||||
});
|
||||
await firstValueFrom(dialogRef.closed);
|
||||
|
||||
@ -91,6 +91,7 @@ export class InitService {
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
||||
await this.autofillService.init();
|
||||
await this.autotypeService.init();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { Router } from "@angular/router";
|
||||
import { Subject, merge } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { LoginApprovalDialogComponentServiceAbstraction } from "@bitwarden/angular/auth/login-approval";
|
||||
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 {
|
||||
@ -31,9 +32,7 @@ import {
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginApprovalComponentServiceAbstraction,
|
||||
LoginEmailService,
|
||||
PinServiceAbstraction,
|
||||
SsoUrlService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@ -50,12 +49,13 @@ import {
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
@ -107,7 +107,7 @@ import {
|
||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||
import { DesktopLoginApprovalDialogComponentService } from "../../auth/login/desktop-login-approval-dialog-component.service";
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
@ -444,8 +444,8 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginApprovalComponentServiceAbstraction,
|
||||
useClass: DesktopLoginApprovalComponentService,
|
||||
provide: LoginApprovalDialogComponentServiceAbstraction,
|
||||
useClass: DesktopLoginApprovalDialogComponentService,
|
||||
deps: [I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
@ -455,17 +455,15 @@ const safeProviders: SafeProvider[] = [
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopAutotypeService,
|
||||
useFactory: (
|
||||
configService: ConfigService,
|
||||
globalStateProvider: GlobalStateProvider,
|
||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
) =>
|
||||
new DesktopAutotypeService(
|
||||
configService,
|
||||
globalStateProvider,
|
||||
platformUtilsService.getDevice() === DeviceType.WindowsDesktop,
|
||||
),
|
||||
deps: [ConfigService, GlobalStateProvider, PlatformUtilsServiceAbstraction],
|
||||
useClass: DesktopAutotypeService,
|
||||
deps: [
|
||||
AccountService,
|
||||
AuthService,
|
||||
CipherServiceAbstraction,
|
||||
ConfigService,
|
||||
GlobalStateProvider,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@ -2,13 +2,13 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { DesktopLoginApprovalComponentService } from "./desktop-login-approval-component.service";
|
||||
import { DesktopLoginApprovalDialogComponentService } from "./desktop-login-approval-dialog-component.service";
|
||||
|
||||
describe("DesktopLoginApprovalComponentService", () => {
|
||||
let service: DesktopLoginApprovalComponentService;
|
||||
describe("DesktopLoginApprovalDialogComponentService", () => {
|
||||
let service: DesktopLoginApprovalDialogComponentService;
|
||||
let i18nService: MockProxy<I18nServiceAbstraction>;
|
||||
let originalIpc: any;
|
||||
|
||||
@ -31,12 +31,12 @@ describe("DesktopLoginApprovalComponentService", () => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DesktopLoginApprovalComponentService,
|
||||
DesktopLoginApprovalDialogComponentService,
|
||||
{ provide: I18nServiceAbstraction, useValue: i18nService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DesktopLoginApprovalComponentService);
|
||||
service = TestBed.inject(DesktopLoginApprovalDialogComponentService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -54,7 +54,7 @@ describe("DesktopLoginApprovalComponentService", () => {
|
||||
const message = `Confirm access attempt for ${email}`;
|
||||
const closeText = "Close";
|
||||
|
||||
const loginApprovalComponent = { email } as LoginApprovalComponent;
|
||||
const loginApprovalDialogComponent = { email } as LoginApprovalDialogComponent;
|
||||
i18nService.t.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case "accountAccessRequested":
|
||||
@ -71,18 +71,20 @@ describe("DesktopLoginApprovalComponentService", () => {
|
||||
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(false);
|
||||
jest.spyOn(ipc.auth, "loginRequest").mockResolvedValue();
|
||||
|
||||
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
|
||||
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
|
||||
|
||||
expect(ipc.auth.loginRequest).toHaveBeenCalledWith(title, message, closeText);
|
||||
});
|
||||
|
||||
it("does not call ipc.auth.loginRequest when window is visible", async () => {
|
||||
const loginApprovalComponent = { email: "test@bitwarden.com" } as LoginApprovalComponent;
|
||||
const loginApprovalDialogComponent = {
|
||||
email: "test@bitwarden.com",
|
||||
} as LoginApprovalDialogComponent;
|
||||
|
||||
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(true);
|
||||
jest.spyOn(ipc.auth, "loginRequest");
|
||||
|
||||
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalComponent.email);
|
||||
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
|
||||
|
||||
expect(ipc.auth.loginRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
@ -1,13 +1,15 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular";
|
||||
import { LoginApprovalComponentServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
DefaultLoginApprovalDialogComponentService,
|
||||
LoginApprovalDialogComponentServiceAbstraction,
|
||||
} from "@bitwarden/angular/auth/login-approval";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@Injectable()
|
||||
export class DesktopLoginApprovalComponentService
|
||||
extends DefaultLoginApprovalComponentService
|
||||
implements LoginApprovalComponentServiceAbstraction
|
||||
export class DesktopLoginApprovalDialogComponentService
|
||||
extends DefaultLoginApprovalDialogComponentService
|
||||
implements LoginApprovalDialogComponentServiceAbstraction
|
||||
{
|
||||
constructor(private i18nService: I18nServiceAbstraction) {
|
||||
super();
|
||||
@ -1,33 +1,70 @@
|
||||
import { autotype } from "@bitwarden/desktop-napi";
|
||||
import { ipcMain, globalShortcut } from "electron";
|
||||
|
||||
import { DesktopAutotypeService } from "../services/desktop-autotype.service";
|
||||
import { autotype } from "@bitwarden/desktop-napi";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
|
||||
|
||||
export class MainDesktopAutotypeService {
|
||||
constructor(private desktopAutotypeService: DesktopAutotypeService) {}
|
||||
keySequence: string = "Alt+CommandOrControl+I";
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private windowMain: WindowMain,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
this.desktopAutotypeService.autotypeEnabled$.subscribe((enabled) => {
|
||||
if (enabled) {
|
||||
ipcMain.on("autofill.configureAutotype", (event, data) => {
|
||||
if (data.enabled === true && !globalShortcut.isRegistered(this.keySequence)) {
|
||||
this.enableAutotype();
|
||||
} else {
|
||||
} else if (data.enabled === false && globalShortcut.isRegistered(this.keySequence)) {
|
||||
this.disableAutotype();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completeAutotypeRequest", (event, data) => {
|
||||
const { response } = data;
|
||||
|
||||
if (
|
||||
stringIsNotUndefinedNullAndEmpty(response.username) &&
|
||||
stringIsNotUndefinedNullAndEmpty(response.password)
|
||||
) {
|
||||
this.doAutotype(response.username, response.password);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: this will call into desktop native code
|
||||
private enableAutotype() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Enabling Autotype...");
|
||||
disableAutotype() {
|
||||
if (globalShortcut.isRegistered(this.keySequence)) {
|
||||
globalShortcut.unregister(this.keySequence);
|
||||
}
|
||||
|
||||
const result = autotype.getForegroundWindowTitle();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Window Title: " + result);
|
||||
this.logService.info("Autotype disabled.");
|
||||
}
|
||||
|
||||
// TODO: this will call into desktop native code
|
||||
private disableAutotype() {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Disabling Autotype...");
|
||||
private enableAutotype() {
|
||||
const result = globalShortcut.register(this.keySequence, () => {
|
||||
const windowTitle = autotype.getForegroundWindowTitle();
|
||||
|
||||
this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", {
|
||||
windowTitle,
|
||||
});
|
||||
});
|
||||
|
||||
result
|
||||
? this.logService.info("Autotype enabled.")
|
||||
: this.logService.info("Enabling autotype failed.");
|
||||
}
|
||||
|
||||
private doAutotype(username: string, password: string) {
|
||||
const inputPattern = username + "\t" + password;
|
||||
const inputArray = new Array<number>(inputPattern.length);
|
||||
|
||||
for (let i = 0; i < inputPattern.length; i++) {
|
||||
inputArray[i] = inputPattern.charCodeAt(i);
|
||||
}
|
||||
|
||||
autotype.typeInput(inputArray);
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,4 +127,43 @@ export default {
|
||||
},
|
||||
);
|
||||
},
|
||||
configureAutotype: (enabled: boolean) => {
|
||||
ipcRenderer.send("autofill.configureAutotype", { enabled });
|
||||
},
|
||||
listenAutotypeRequest: (
|
||||
fn: (
|
||||
windowTitle: string,
|
||||
completeCallback: (
|
||||
error: Error | null,
|
||||
response: { username?: string; password?: string },
|
||||
) => void,
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
"autofill.listenAutotypeRequest",
|
||||
(
|
||||
event,
|
||||
data: {
|
||||
windowTitle: string;
|
||||
},
|
||||
) => {
|
||||
const { windowTitle } = data;
|
||||
|
||||
fn(windowTitle, (error, response) => {
|
||||
if (error) {
|
||||
ipcRenderer.send("autofill.completeError", {
|
||||
windowTitle,
|
||||
error: error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ipcRenderer.send("autofill.completeAutotypeRequest", {
|
||||
windowTitle,
|
||||
response,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
import { combineLatest, map, Observable, of } from "rxjs";
|
||||
import { combineLatest, filter, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
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 {
|
||||
GlobalStateProvider,
|
||||
AUTOTYPE_SETTINGS_DISK,
|
||||
KeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
export const AUTOTYPE_ENABLED = new KeyDefinition<boolean>(
|
||||
AUTOTYPE_SETTINGS_DISK,
|
||||
@ -20,28 +28,83 @@ export class DesktopAutotypeService {
|
||||
autotypeEnabled$: Observable<boolean> = of(false);
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private configService: ConfigService,
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private isWindows: boolean,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {
|
||||
if (this.isWindows) {
|
||||
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
|
||||
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
|
||||
const firstCipher = possibleCiphers?.at(0);
|
||||
|
||||
return callback(null, {
|
||||
username: firstCipher?.login?.username,
|
||||
password: firstCipher?.login?.password,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) {
|
||||
this.autotypeEnabled$ = combineLatest([
|
||||
this.autotypeEnabledState.state$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
switchMap((userId) => this.authService.authStatusFor$(userId)),
|
||||
),
|
||||
]).pipe(
|
||||
map(
|
||||
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag]) =>
|
||||
autotypeEnabled && windowsDesktopAutotypeFeatureFlag,
|
||||
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag, authStatus]) =>
|
||||
autotypeEnabled &&
|
||||
windowsDesktopAutotypeFeatureFlag &&
|
||||
authStatus == AuthenticationStatus.Unlocked,
|
||||
),
|
||||
);
|
||||
|
||||
this.autotypeEnabled$.subscribe((enabled) => {
|
||||
ipc.autofill.configureAutotype(enabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
async setAutotypeEnabledState(enabled: boolean): Promise<void> {
|
||||
await this.autotypeEnabledState.update(() => enabled, {
|
||||
shouldUpdate: (currentlyEnabled) => currentlyEnabled !== enabled,
|
||||
});
|
||||
}
|
||||
|
||||
async matchCiphersToWindowTitle(windowTitle: string): Promise<CipherView[]> {
|
||||
const URI_PREFIX = "APP:";
|
||||
windowTitle = windowTitle.toLowerCase();
|
||||
|
||||
const ciphers = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) => this.cipherService.cipherViews$(userId)),
|
||||
),
|
||||
);
|
||||
|
||||
const possibleCiphers = ciphers.filter((c) => {
|
||||
return (
|
||||
c.login?.username &&
|
||||
c.login?.password &&
|
||||
c.deletedDate == null &&
|
||||
c.login?.uris.some((u) => {
|
||||
if (u.uri?.indexOf(URI_PREFIX) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uri = u.uri.substring(4).toLowerCase();
|
||||
|
||||
return windowTitle.indexOf(uri) > -1;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return possibleCiphers;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
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 { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@ -2,11 +2,9 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { combineLatest, defer, map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
@ -3027,9 +3027,6 @@
|
||||
"message": "Toggle character count",
|
||||
"description": "'Character count' describes a feature that displays a number next to each character of the password."
|
||||
},
|
||||
"areYouTryingToAccessYourAccount": {
|
||||
"message": "Are you trying to access your account?"
|
||||
},
|
||||
"accessAttemptBy": {
|
||||
"message": "Access attempt by $EMAIL$",
|
||||
"placeholders": {
|
||||
@ -3039,6 +3036,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"loginRequestApprovedForEmailOnDevice": {
|
||||
"message": "Login request approved for $EMAIL$ on $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "name@example.com"
|
||||
},
|
||||
"device": {
|
||||
"content": "$2",
|
||||
"example": "Web app - Chrome"
|
||||
}
|
||||
}
|
||||
},
|
||||
"youDeniedLoginAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this was you, try to log in with the device again."
|
||||
},
|
||||
"webApp": {
|
||||
"message": "Web app"
|
||||
},
|
||||
"mobile": {
|
||||
"message": "Mobile",
|
||||
"description": "Mobile app"
|
||||
},
|
||||
"extension": {
|
||||
"message": "Extension",
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
"message": "Desktop",
|
||||
"description": "Desktop app"
|
||||
},
|
||||
"cli": {
|
||||
"message": "CLI"
|
||||
},
|
||||
"sdk": {
|
||||
"message": "SDK",
|
||||
"description": "Software Development Kit"
|
||||
},
|
||||
"server": {
|
||||
"message": "Server"
|
||||
},
|
||||
"loginRequest": {
|
||||
"message": "Login request"
|
||||
},
|
||||
"deviceType": {
|
||||
"message": "Device Type"
|
||||
},
|
||||
@ -3054,22 +3095,6 @@
|
||||
"denyAccess": {
|
||||
"message": "Deny access"
|
||||
},
|
||||
"logInConfirmedForEmailOnDevice": {
|
||||
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "name@example.com"
|
||||
},
|
||||
"device": {
|
||||
"content": "$2",
|
||||
"example": "iOS"
|
||||
}
|
||||
}
|
||||
},
|
||||
"youDeniedALogInAttemptFromAnotherDevice": {
|
||||
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
|
||||
},
|
||||
"justNow": {
|
||||
"message": "Just now"
|
||||
},
|
||||
@ -3595,7 +3620,7 @@
|
||||
},
|
||||
"uriMatchDefaultStrategyHint": {
|
||||
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||
@ -4041,6 +4066,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"showMore": {
|
||||
"message": "Show more"
|
||||
},
|
||||
"showLess": {
|
||||
"message": "Show less"
|
||||
},
|
||||
"enableAutotype": {
|
||||
"message": "Enable Autotype"
|
||||
},
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
"message": "Rechercher dans le coffre"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "Réinitialiser la recherche"
|
||||
},
|
||||
"addItem": {
|
||||
"message": "Ajouter un élément"
|
||||
@ -1443,7 +1443,7 @@
|
||||
"description": "Copy credit card security code (CVV)"
|
||||
},
|
||||
"cardNumber": {
|
||||
"message": "card number"
|
||||
"message": "numéro de carte"
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Adhésion Premium"
|
||||
@ -3575,11 +3575,11 @@
|
||||
"description": "Link to match detection docs on warning dialog for advance match strategy"
|
||||
},
|
||||
"uriAdvancedOption": {
|
||||
"message": "Advanced options",
|
||||
"message": "Options avancées",
|
||||
"description": "Advanced option placeholder for uri option component"
|
||||
},
|
||||
"warningCapitalized": {
|
||||
"message": "Warning",
|
||||
"message": "Avertissement",
|
||||
"description": "Warning (should maintain locale-relevant capitalization)"
|
||||
},
|
||||
"success": {
|
||||
@ -4007,9 +4007,9 @@
|
||||
}
|
||||
},
|
||||
"enableAutotype": {
|
||||
"message": "Enable Autotype"
|
||||
"message": "Activer le type automatique"
|
||||
},
|
||||
"enableAutotypeDescription": {
|
||||
"message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut."
|
||||
"message": "Bitwarden ne valide pas les emplacements d'entrée, assurez-vous d'être dans la bonne fenêtre et le bon champ avant d'utiliser le raccourci."
|
||||
}
|
||||
}
|
||||
|
||||
@ -4007,7 +4007,7 @@
|
||||
}
|
||||
},
|
||||
"enableAutotype": {
|
||||
"message": "Enable Autotype"
|
||||
"message": "Omogući automatski unos"
|
||||
},
|
||||
"enableAutotypeDescription": {
|
||||
"message": "Bitwarden ne provjerava lokacije unosa, prije korištenja prečaca provjeri da si u pravom prozoru i polju."
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
"message": "Претражи сеф"
|
||||
},
|
||||
"resetSearch": {
|
||||
"message": "Reset search"
|
||||
"message": "Ресетовати претрагу"
|
||||
},
|
||||
"addItem": {
|
||||
"message": "Додај ставку"
|
||||
@ -576,7 +576,7 @@
|
||||
"message": "Копирај потврдни код (TOTP)"
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"message": "Copy $FIELD$, $CIPHERNAME$",
|
||||
"message": "Копирај $FIELD$, $CIPHERNAME$",
|
||||
"description": "Title for a button that copies a field value to the clipboard.",
|
||||
"placeholders": {
|
||||
"field": {
|
||||
@ -1443,7 +1443,7 @@
|
||||
"description": "Copy credit card security code (CVV)"
|
||||
},
|
||||
"cardNumber": {
|
||||
"message": "card number"
|
||||
"message": "број картице"
|
||||
},
|
||||
"premiumMembership": {
|
||||
"message": "Премијум чланство"
|
||||
@ -4007,9 +4007,9 @@
|
||||
}
|
||||
},
|
||||
"enableAutotype": {
|
||||
"message": "Enable Autotype"
|
||||
"message": "Упали ауто-унос"
|
||||
},
|
||||
"enableAutotypeDescription": {
|
||||
"message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut."
|
||||
"message": "Bitwarden не потврђује локације уноса, будите сигурни да сте у добром прозору и поље пре употребе пречице."
|
||||
}
|
||||
}
|
||||
|
||||
@ -3033,7 +3033,7 @@
|
||||
}
|
||||
},
|
||||
"youDeniedALogInAttemptFromAnotherDevice": {
|
||||
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте увійти з пристроєм знову."
|
||||
"message": "Ви відхилили спробу входу з іншого пристрою. Якщо це були дійсно ви, спробуйте ввійти з пристроєм знову."
|
||||
},
|
||||
"justNow": {
|
||||
"message": "Щойно"
|
||||
|
||||
@ -37,7 +37,6 @@ import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-
|
||||
import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service";
|
||||
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
|
||||
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutotypeService } from "./autofill/services/desktop-autotype.service";
|
||||
import { DesktopBiometricsService } from "./key-management/biometrics/desktop.biometrics.service";
|
||||
import { MainBiometricsIPCListener } from "./key-management/biometrics/main-biometrics-ipc.listener";
|
||||
import { MainBiometricsService } from "./key-management/biometrics/main-biometrics.service";
|
||||
@ -48,7 +47,6 @@ import { PowerMonitorMain } from "./main/power-monitor.main";
|
||||
import { TrayMain } from "./main/tray.main";
|
||||
import { UpdaterMain } from "./main/updater.main";
|
||||
import { WindowMain } from "./main/window.main";
|
||||
import { SlimConfigService } from "./platform/config/slim-config.service";
|
||||
import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main";
|
||||
import { ClipboardMain } from "./platform/main/clipboard.main";
|
||||
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
|
||||
@ -307,13 +305,22 @@ export class Main {
|
||||
void this.nativeAutofillMain.init();
|
||||
|
||||
this.mainDesktopAutotypeService = new MainDesktopAutotypeService(
|
||||
new DesktopAutotypeService(
|
||||
new SlimConfigService(this.environmentService, globalStateProvider),
|
||||
globalStateProvider,
|
||||
process.platform === "win32",
|
||||
),
|
||||
this.logService,
|
||||
this.windowMain,
|
||||
);
|
||||
this.mainDesktopAutotypeService.init();
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(() => {
|
||||
this.mainDesktopAutotypeService.init();
|
||||
})
|
||||
.catch((reason) => {
|
||||
this.logService.error("Error initializing Autotype.", reason);
|
||||
});
|
||||
|
||||
app.on("will-quit", () => {
|
||||
this.mainDesktopAutotypeService.disableAutotype();
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.7.0",
|
||||
"version": "2025.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.7.1",
|
||||
"version": "2025.8.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.7.0",
|
||||
"version": "2025.8.0",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@ -1,66 +0,0 @@
|
||||
import { combineLatest, map, Observable, throwError } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import {
|
||||
FeatureFlag,
|
||||
FeatureFlagValueType,
|
||||
getFeatureFlagValue,
|
||||
} from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { ServerSettings } from "@bitwarden/common/platform/models/domain/server-settings";
|
||||
import { GLOBAL_SERVER_CONFIGURATIONS } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
/*
|
||||
NOT FOR GENERAL USE
|
||||
|
||||
If you have more uses for the config service in the main process,
|
||||
please reach out to platform.
|
||||
*/
|
||||
export class SlimConfigService implements ConfigService {
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
) {}
|
||||
|
||||
serverConfig$: Observable<ServerConfig> = throwError(() => {
|
||||
return new Error("Method not implemented.");
|
||||
});
|
||||
serverSettings$: Observable<ServerSettings> = throwError(() => {
|
||||
return new Error("Method not implemented.");
|
||||
});
|
||||
cloudRegion$: Observable<Region> = throwError(() => {
|
||||
return new Error("Method not implemented.");
|
||||
});
|
||||
getFeatureFlag$<Flag extends FeatureFlag>(key: Flag): Observable<FeatureFlagValueType<Flag>> {
|
||||
return combineLatest([
|
||||
this.environmentService.environment$,
|
||||
this.globalStateProvider.get(GLOBAL_SERVER_CONFIGURATIONS).state$,
|
||||
]).pipe(
|
||||
map(([environment, serverConfigMap]) =>
|
||||
getFeatureFlagValue(serverConfigMap?.[environment.getApiUrl()], key),
|
||||
),
|
||||
);
|
||||
}
|
||||
userCachedFeatureFlag$<Flag extends FeatureFlag>(
|
||||
key: Flag,
|
||||
userId: UserId,
|
||||
): Observable<FeatureFlagValueType<Flag>> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
getFeatureFlag<Flag extends FeatureFlag>(key: Flag): Promise<FeatureFlagValueType<Flag>> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer): Observable<boolean> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
ensureConfigFetched(): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@ -98,3 +98,11 @@ export function cleanUserAgent(userAgent: string): string {
|
||||
.replace(userAgentItem("Bitwarden", " "), "")
|
||||
.replace(userAgentItem("Electron", " "), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the provided string is not undefined, not null, and not empty.
|
||||
* Otherwise, returns `false`.
|
||||
*/
|
||||
export function stringIsNotUndefinedNullAndEmpty(str: string): boolean {
|
||||
return str?.length > 0;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user