From 46068ff012806faa03aac4b521657c214025086a Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 16 Dec 2024 08:39:21 -0500 Subject: [PATCH 01/57] Slight codeowners change for ssh agent (#12405) --- .github/CODEOWNERS | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b50cb807448..e38277877bb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -105,7 +105,8 @@ apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev - +# SSH Agent +apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-dev @bitwarden/wg-ssh-keys ## Component Library ## .storybook @bitwarden/team-design-system @@ -138,9 +139,6 @@ apps/cli/src/locales/en/messages.json apps/desktop/src/locales/en/messages.json apps/web/src/locales/en/messages.json -## Ssh agent temporary co-codeowner -apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-platform-dev @bitwarden/wg-ssh-keys @bitwarden/team-autofill-dev - ## BRE team owns these workflows ## .github/workflows/brew-bump-desktop.yml @bitwarden/dept-bre .github/workflows/deploy-web.yml @bitwarden/dept-bre From 9e507f6aae1cf0e3cf2af7c55b866b80129bfbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Mon, 16 Dec 2024 08:59:36 -0500 Subject: [PATCH 02/57] Autosync the updated translations (#12394) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 15 + apps/browser/src/_locales/az/messages.json | 17 +- apps/browser/src/_locales/be/messages.json | 15 + apps/browser/src/_locales/bg/messages.json | 21 +- apps/browser/src/_locales/bn/messages.json | 15 + apps/browser/src/_locales/bs/messages.json | 15 + apps/browser/src/_locales/ca/messages.json | 15 + apps/browser/src/_locales/cs/messages.json | 17 +- apps/browser/src/_locales/cy/messages.json | 15 + apps/browser/src/_locales/da/messages.json | 17 +- apps/browser/src/_locales/de/messages.json | 21 +- apps/browser/src/_locales/el/messages.json | 15 + apps/browser/src/_locales/en_GB/messages.json | 15 + apps/browser/src/_locales/en_IN/messages.json | 15 + apps/browser/src/_locales/es/messages.json | 27 +- apps/browser/src/_locales/et/messages.json | 15 + apps/browser/src/_locales/eu/messages.json | 15 + apps/browser/src/_locales/fa/messages.json | 15 + apps/browser/src/_locales/fi/messages.json | 17 +- apps/browser/src/_locales/fil/messages.json | 15 + apps/browser/src/_locales/fr/messages.json | 17 +- apps/browser/src/_locales/gl/messages.json | 15 + apps/browser/src/_locales/he/messages.json | 15 + apps/browser/src/_locales/hi/messages.json | 15 + apps/browser/src/_locales/hr/messages.json | 15 + apps/browser/src/_locales/hu/messages.json | 17 +- apps/browser/src/_locales/id/messages.json | 15 + apps/browser/src/_locales/it/messages.json | 15 + apps/browser/src/_locales/ja/messages.json | 19 +- apps/browser/src/_locales/ka/messages.json | 15 + apps/browser/src/_locales/km/messages.json | 15 + apps/browser/src/_locales/kn/messages.json | 15 + apps/browser/src/_locales/ko/messages.json | 1323 +++++++++-------- apps/browser/src/_locales/lt/messages.json | 15 + apps/browser/src/_locales/lv/messages.json | 17 +- apps/browser/src/_locales/ml/messages.json | 15 + apps/browser/src/_locales/mr/messages.json | 15 + apps/browser/src/_locales/my/messages.json | 15 + apps/browser/src/_locales/nb/messages.json | 15 + apps/browser/src/_locales/ne/messages.json | 15 + apps/browser/src/_locales/nl/messages.json | 15 + apps/browser/src/_locales/nn/messages.json | 15 + apps/browser/src/_locales/or/messages.json | 15 + apps/browser/src/_locales/pl/messages.json | 61 +- apps/browser/src/_locales/pt_BR/messages.json | 15 + apps/browser/src/_locales/pt_PT/messages.json | 17 +- apps/browser/src/_locales/ro/messages.json | 15 + apps/browser/src/_locales/ru/messages.json | 17 +- apps/browser/src/_locales/si/messages.json | 15 + apps/browser/src/_locales/sk/messages.json | 17 +- apps/browser/src/_locales/sl/messages.json | 15 + apps/browser/src/_locales/sr/messages.json | 15 + apps/browser/src/_locales/sv/messages.json | 15 + apps/browser/src/_locales/te/messages.json | 15 + apps/browser/src/_locales/th/messages.json | 15 + apps/browser/src/_locales/tr/messages.json | 15 + apps/browser/src/_locales/uk/messages.json | 17 +- apps/browser/src/_locales/vi/messages.json | 15 + apps/browser/src/_locales/zh_CN/messages.json | 21 +- apps/browser/src/_locales/zh_TW/messages.json | 15 + 60 files changed, 1608 insertions(+), 702 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index a71021e7ded..725eef3bd07 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "هوية التعبئة التلقائية" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "إنشاء كلمة مرور (تم النسخ)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "ملء بيانات الاعتماد لـ", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index a1783a91ef8..4b83341dce5 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Kimliyi avto-doldur" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Parol yarat (kopyalandı)" }, @@ -3174,7 +3181,7 @@ "message": "Bütün giriş seçimlərinə bax" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Bütün giriş seçimlərinə bax" }, "notificationSentDevice": { "message": "Cihazınıza bir bildiriş göndərildi." @@ -3580,6 +3587,14 @@ "message": "Hesabınızın kilidini açın, yeni bir pəncərədə açılır", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Kimlik məlumatlarını doldur", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 17bcdec4560..3010bb6b6c6 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Аўтазапаўненне асабістых даных" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Генерыраваць пароль (з капіяваннем)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index b76b72001fe..56d2ce80ea1 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Самопопълваща се самоличност" }, + "fillVerificationCode": { + "message": "Попълване на кода за потвърждаване" + }, + "fillVerificationCodeAria": { + "message": "Попълване на кода за потвърждаване", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Генериране на парола (копирана)" }, @@ -3174,7 +3181,7 @@ "message": "Вижте всички възможности за вписване" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Вижте всички възможности за вписване" }, "notificationSentDevice": { "message": "Към устройството Ви е изпратено известие." @@ -3580,6 +3587,14 @@ "message": "Отклюване на регистрацията, отваря се в нов прозорец", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Код за потвърждение на еднократната времево-ограничена парола", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Оставащо време преди изтичането на текущия код", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Попълване на данните за", "description": "Screen reader text for when overlay item is in focused" @@ -4890,10 +4905,10 @@ "message": "Генерирана парола" }, "compactMode": { - "message": "Compact mode" + "message": "Компактен режим" }, "beta": { - "message": "Beta" + "message": "Бета" }, "extensionWidth": { "message": "Ширина на разширението" diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index d37499d6748..a03e86b860b 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "পাসওয়ার্ড তৈরি করুন (অনুলিপিকৃত)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 4d024d25e77..97997f677b2 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index bc351b0faa2..9ca06f2b50a 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Emplena automàticament l'identitat" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Genera contrasenya (copiada)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Ompliu les credencials per a", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index f6ad593bd53..b74fe0c9cfd 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Automaticky vyplnit identitu" }, + "fillVerificationCode": { + "message": "Vyplnit ověřovací kód" + }, + "fillVerificationCodeAria": { + "message": "Vyplnit ověřovací kód", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Vygenerovat heslo a zkopírovat do schránky" }, @@ -3174,7 +3181,7 @@ "message": "Zobrazit všechny volby přihlášení" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Zobrazit všechny volby přihlášení" }, "notificationSentDevice": { "message": "Na Vaše zařízení bylo odesláno oznámení." @@ -3580,6 +3587,14 @@ "message": "Odemknout účet, otevře se v novém okně", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Ověřovací kód TOTP", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Zbývající čas před vypršením aktuálního TOTP", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Vyplnit přihlašovací údaje pro", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 5d2a0a2e019..3d3866097f2 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Llenwi hunaniaeth" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Cynhyrchu cyfrinair (wedi'i gopïo)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 5407b8298a0..78229082235 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autoudfyld identitet" }, + "fillVerificationCode": { + "message": "Udfyld bekræftelseskode" + }, + "fillVerificationCodeAria": { + "message": "Udfyld bekræftelseskode", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generér adgangskode (kopieret)" }, @@ -3174,7 +3181,7 @@ "message": "Vis alle indlogningsmuligheder" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Vis alle indlogningsmuligheder" }, "notificationSentDevice": { "message": "En notifikation er sendt til din enhed." @@ -3580,6 +3587,14 @@ "message": "Oplås kontoen, åbnes i et nyt vindue", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Tidsbaseret engangs adgangskodebekræftelseskode", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Resterende tid før udløb af aktuel TOTP", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Angiv legitimationsoplysninger for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index e1873f0c3d8..098af701cd6 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Identität automatisch ausfüllen" }, + "fillVerificationCode": { + "message": "Verifizierungscode eingeben" + }, + "fillVerificationCodeAria": { + "message": "Verifizierungscode eingeben", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Passwort generieren (kopiert)" }, @@ -1320,10 +1327,10 @@ "message": "Gib den 6-stelligen Verifizierungscode aus deiner Authenticator App ein." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Authentifizierungs-Timeout" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Die Authentifizierungssitzung ist abgelaufen. Bitte starte den Anmeldeprozess neu." }, "enterVerificationCodeEmail": { "message": "Gib den 6-stelligen Bestätigungscode ein, der an $EMAIL$ gesendet wurde.", @@ -3174,7 +3181,7 @@ "message": "Alle Anmeldeoptionen anzeigen" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Alle Anmeldeoptionen anzeigen" }, "notificationSentDevice": { "message": "Eine Benachrichtigung wurde an dein Gerät gesendet." @@ -3580,6 +3587,14 @@ "message": "Dein Konto entsperren, öffnet sich in einem neuen Fenster", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Zeitbasierter einmaliger Passwort-Verifizierungscode", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Verbleibende Zeit bis zum Ablauf des aktuellen TOTP", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Zugangsdaten ausfüllen für", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index ff14bfe30f2..be853922400 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Αυτόματη συμπλήρωση ταυτότητας" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Δημιουργία κωδικού πρόσβασης (αντιγράφηκε)" }, @@ -3580,6 +3587,14 @@ "message": "Ξεκλείδωμα του λογαριασμού σας, ανοίγει σε νέο παράθυρο", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Συμπλήρωση στοιχείων για", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 5e24a807d0e..13db7b5080b 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Auto-fill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 18b96eb4178..173fe8e788a 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Auto-fill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 14576cd12c5..4b3dc12f684 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -20,7 +20,7 @@ "message": "Crear cuenta" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "¿Nuevo en Bitwarden?" }, "logInWithPasskey": { "message": "Log in with passkey" @@ -29,7 +29,7 @@ "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Bienvenido de nuevo" }, "setAStrongPassword": { "message": "Establece una contraseña fuerte" @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autocompletar identidad" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generar contraseña (copiada)" }, @@ -1478,7 +1485,7 @@ "message": "Display identities as suggestions" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "Mostrar tarjetas como sugerencias" }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" @@ -1517,7 +1524,7 @@ "message": "Los sitios web vulnerados o no confiables pueden explotar el autorelleno al cargar la página." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Más información sobre riesgos" }, "learnMoreAboutAutofill": { "message": "Más información sobre el relleno automático" @@ -1586,7 +1593,7 @@ "message": "Booleano" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Casilla de verificación" }, "cfTypeLinked": { "message": "Vinculado", @@ -2397,7 +2404,7 @@ "message": "Texto" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Texto a compartir" }, "sendTypeFile": { "message": "Archivo" @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Rellenar credenciales para", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 28da8976ddb..4f17a302633 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Täida identiteet" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Genereeri parool (kopeeritakse)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 6dab0b5754a..d98ae43081f 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Auto-bete nortasuna" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Sortu pasahitza (kopiatuta)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 177ca744651..6b88d9e5234 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "پر کردن خودکار هویت" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "ساخت کلمه عبور (کپی شد)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 308f2563ab8..0a2313d7dca 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Automaattitäytä henkilöllisyys" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Luo salasana (leikepöydälle)" }, @@ -3174,7 +3181,7 @@ "message": "Näytä kaikki kirjautumisvaihtoehdot" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Näytä kaikki kirjautumisvaihtoehdot" }, "notificationSentDevice": { "message": "Laitteellesi on lähetetty ilmoitus." @@ -3580,6 +3587,14 @@ "message": "Avaa tilisi lukitus. Avautuu uudessa ikkunassa.", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Täytä kirjautumistiedot kohteesta", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index a4e98843112..33cc408a0d6 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Awtomatikong punan ang pagkakakilanlan" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Maglagay ng Password" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 01997f9ca52..679b6248033 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Saisie automatique de l'identité" }, + "fillVerificationCode": { + "message": "Remplir le code de vérification" + }, + "fillVerificationCodeAria": { + "message": "Remplir le code de vérification", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Générer un mot de passe (copié)" }, @@ -3174,7 +3181,7 @@ "message": "Afficher toutes les options de connexion" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Afficher toutes les options de connexion" }, "notificationSentDevice": { "message": "Une notification a été envoyée à votre appareil." @@ -3580,6 +3587,14 @@ "message": "Déverrouiller votre compte, s'ouvre dans une nouvelle fenêtre", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Code de vérification de mot de passe unique basé sur le temps", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Temps restant avant l'expiration du TOTP actuel", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Remplir les identifiants pour", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index aa0d9a17762..7240d88a587 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Encher automaticamente identidade" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Xerar contrasinal (copiado)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 3c418682003..cb2c8782c9e 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "מילוי פרטי זיהוי אוטומטית" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "צור סיסמה (העתק)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index b4c03a8f829..b14aa89def1 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "स्वचालित पहचान विवरण" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate Password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 80eb68be8ac..4e280dcff43 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Auto-ispuna identiteta" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generiraj lozinku (i kopiraj)" }, @@ -3580,6 +3587,14 @@ "message": "Otključaj račun; otvara se u novom prozoru", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Unesi vjerodajnice za", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 25ea3d440b7..04644910c63 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Automatikus kitöltés személyazonosság" }, + "fillVerificationCode": { + "message": "Ellenőrző kód kitöltése" + }, + "fillVerificationCodeAria": { + "message": "Ellenőrző Kód kitöltése", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Jelszó generálás (másolt)" }, @@ -3174,7 +3181,7 @@ "message": "Összes bejelentkezési opció megtekintése" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Összes bejelentkezési opció megtekintése" }, "notificationSentDevice": { "message": "Egy értesítés lett elküldve az eszközre." @@ -3580,6 +3587,14 @@ "message": "Oldjuk fel a fiók zárolását, új ablakban nyílik meg.", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Időalapú, egyszeri jelszó ellenőrző kód", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "A jelenlegi TOTP lejártáig hátralévő idő", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Töltse kia hitelesítő adatokat", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 8a341c59b8d..7de07f17f10 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identitas" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Membuat Kata Sandi (tersalin)" }, @@ -3580,6 +3587,14 @@ "message": "Buka akun Anda, membukanya di jendela baru", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Isi tanda pengenal untuk", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index ff2326809a8..1261106cf32 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Riempi automaticamente identità" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Genera password e copiala" }, @@ -3580,6 +3587,14 @@ "message": "Sblocca il tuo account, apri in una nuova finestra", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Riempi le credenziali per", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index fa12570fc50..f0f80b745af 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "自動入力 ID" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "パスワードを生成 (コピー)" }, @@ -1475,10 +1482,10 @@ "message": "フォームフィールドに自動入力の候補を表示する" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "ID を候補として表示する" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "カードを候補として表示する" }, "showInlineMenuOnIconSelectionLabel": { "message": "アイコンが選択されているときに候補を表示する" @@ -3580,6 +3587,14 @@ "message": "アカウントのロックを解除し、新しいウィンドウで開く", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "資格情報を入力:", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index ed909731d78..33ccbdbb1a1 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 6aa17c1d7e3..779ff917578 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 40fc910b088..8b21fc61e56 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "ಪಾಸ್ವರ್ಡ್ ರಚಿಸಿ (ನಕಲಿಸಲಾಗಿದೆ)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 0b527149d15..91207cd0e34 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -20,16 +20,16 @@ "message": "계정 만들기" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Bitwarden을 처음 이용하시나요?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "패스키를 사용하여 로그인하기" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "통합인증(SSO) 사용하기" }, "welcomeBack": { - "message": "Welcome back" + "message": "돌아온 것을 환영합니다." }, "setAStrongPassword": { "message": "비밀번호 설정" @@ -81,10 +81,10 @@ "message": "마스터 비밀번호 힌트 (선택)" }, "joinOrganization": { - "message": "Join organization" + "message": "\"조직\"에 가입하기" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "$ORGANIZATIONNAME$에 참가하기", "placeholders": { "organizationName": { "content": "$1", @@ -93,7 +93,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "마지막으로, 마스터 비밀번호를 설정하여 조직에 참가하십시오" }, "tab": { "message": "탭" @@ -120,7 +120,7 @@ "message": "비밀번호 복사" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "암호 복사" }, "copyNote": { "message": "메모 복사" @@ -153,16 +153,16 @@ "message": "운전면허 번호 복사" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "개인 키 복사" }, "copyPublicKey": { - "message": "Copy public key" + "message": "공개 키 복사" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "핑거프린트 복사" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "$FIELD$ 복사", "placeholders": { "field": { "content": "$1", @@ -171,13 +171,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "웹사이트 복사" }, "copyNotes": { - "message": "Copy notes" + "message": "노트 복사" }, "fill": { - "message": "Fill", + "message": "채우기", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "신원 자동 완성" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "비밀번호 생성 및 클립보드에 복사" }, @@ -232,16 +239,16 @@ "message": "항목 추가" }, "accountEmail": { - "message": "Account email" + "message": "계정 이메일" }, "requestHint": { - "message": "Request hint" + "message": "힌트 요청" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "마스터 비밀번호 힌트 얻기" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "계정 이메일 주소를 입력하세요. 그 주소로 비밀번호 힌트가 전송될 것 입니다." }, "passwordHint": { "message": "비밀번호 힌트" @@ -274,7 +281,7 @@ "message": "마스터 비밀번호 변경" }, "continueToWebApp": { - "message": "웹 앱에서 계속하시겠용?" + "message": "웹 앱에서 계속하시겠나요?" }, "continueToWebAppDesc": { "message": "웹 앱에서 Bitwarden 계정의 더 많은 기능을 탐색해보세요." @@ -289,7 +296,7 @@ "message": "브라우저 확장 스토어로 이동하시겠습니까?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "다른 사람들이 Bitwarden이 적합한지 알 수 있도록 도와주세요. 당신의 브라우저 확장 스토어로 방문하여 별점을 남겨주세요." }, "changeMasterPasswordOnWebConfirmation": { "message": "Bitwarden 웹 앱에서 마스터 비밀번호를 변경할 수 있습니다." @@ -315,37 +322,37 @@ "message": "정보" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Bitwarden에 대한 더 많은 정보" }, "continueToBitwardenDotCom": { "message": "bitwarden.com 으로 이동할까요?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "비지니스용 Bitwarden" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Bitwarden 인증 도구" }, "continueToAuthenticatorPageDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" + "message": "Bitwarden 인증 도구를 사용하면, 인증키를 저장하고, 2단계 인증을 위한 TOTP 코드를 생성할 수 있습니다. 자세한 내용은 bitwarden.com 사이트에서 확인해주세요." }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Bitwarden 보안 매니저" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Bitwarden 보안 매니저를 이용하여, 개발자의 기밀을 안전하게 저장하고, 관리하고, 공유하세요. 자세한 내용은 bitwarden.com 사이트에서 확인해주요." }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "Passwordless.dev와 함께, 기존의 비밀번호 로그인 방식으로 부터 벗어나, 매끄럽고 안전한 로그인 경험을 만들어보세요. 자세한 내용은 bitwarden.com 사이트에서 확인해주요" }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "무료 bitwarden 가족 플랜" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "무료 Bitwarden 가족 플랜을 이용하실 수 있습니다. 오늘 웹앱에서 이 혜택을 사용하세요." }, "version": { "message": "버전" @@ -366,22 +373,22 @@ "message": "폴더 편집" }, "newFolder": { - "message": "New folder" + "message": "새 폴더" }, "folderName": { - "message": "Folder name" + "message": "폴더 이름" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "상위 폴더 이름 뒤에 \"/\"를 추가하여 폴더를 계층적으로 구성합니다. 예: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "추가된 폴더가 없습니다." }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "폴더를 만들어 보관함의 항목들을 정리해보세요" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "정말로 이 폴더를 영구적으로 삭제하시겠습니까?" }, "deleteFolder": { "message": "폴더 삭제" @@ -424,7 +431,7 @@ "message": "유일무이하고 강력한 비밀번호를 자동으로 생성합니다." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden 웹 앱" }, "importItems": { "message": "항목 가져오기" @@ -436,7 +443,7 @@ "message": "비밀번호 생성" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "암호 생성" }, "regeneratePassword": { "message": "비밀번호 재생성" @@ -467,11 +474,11 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "포함", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "대문자 포함", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -479,7 +486,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "소문자 포함", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -487,7 +494,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "숫자 포함", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -495,7 +502,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "특수 문자 포함", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -526,11 +533,11 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "모호한 문자 사용 안 함", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "기업 정책에 따른 요구사항들이 당신의 생성기 옵션들에 적용되었습니다.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -567,16 +574,16 @@ "message": "즐겨찾기 해제" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "항목이 즐겨찾기에 추가되었습니다." }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "항목이 즐겨찾기에서 삭제되었습니다." }, "notes": { "message": "메모" }, "privateNote": { - "message": "Private note" + "message": "개인 메모" }, "note": { "message": "메모" @@ -600,7 +607,7 @@ "message": "웹사이트 열기" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "$ITEMNAME$ 웹사이드 열기", "placeholders": { "itemname": { "content": "$1", @@ -633,7 +640,7 @@ "message": "세션 만료" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "보관함 시간초과" }, "otherOptions": { "message": "기타 옵션" @@ -654,13 +661,13 @@ "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "당신의 보관함이 잠겼습니다." }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "당신의 계정이 잠겼습니다." }, "or": { - "message": "or" + "message": "또는" }, "unlock": { "message": "잠금 해제" @@ -685,7 +692,7 @@ "message": "보관함 시간 제한" }, "vaultTimeout1": { - "message": "Timeout" + "message": "시간초과" }, "lockNow": { "message": "지금 잠그기" @@ -739,16 +746,16 @@ "message": "보안" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "마스터 비밀번호 확정" }, "masterPassword": { - "message": "Master password" + "message": "마스터 비밀번호" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "마스터 비밀번호는 잊어버려도 복구할 수 없습니다!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "마스터 비밀번호 힌트" }, "errorOccurred": { "message": "오류가 발생했습니다" @@ -782,10 +789,10 @@ "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "계정 생성이 완료되었습니다!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "로그인이 이미 되어있습니다." }, "youSuccessfullyLoggedIn": { "message": "로그인에 성공했습니다." @@ -800,7 +807,7 @@ "message": "인증 코드는 반드시 입력해야 합니다." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "인증이 너무 오래 걸리거나 취소되었습니다. 다시 시도하여 주십시오." }, "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" @@ -828,16 +835,16 @@ "message": "현재 웹페이지에서 QR 코드 스캔하기" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "간편하게 2단계 인증을 만들기" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden은 2단계 인증 코드들을 저장하고, 채워넣을 수 있습니다. 키를 복사하여 이 필드에 붙여넣으세요." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden은 2단계 인증 코드들을 저장하고, 채워넣을 수 있습니다. 카메라 아이콘을 선택하고, 이 웹사이드의 인증 도구 QR코드를 스크린샷을 찍거나, 키를 복사하여 이 필드에 붙여넣으세요." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "인증 도구에 대해 더 알아보기" }, "copyTOTP": { "message": "인증서 키 (TOTP) 복사" @@ -846,7 +853,7 @@ "message": "로그아웃됨" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "계정이 로그아웃 되었습니다." }, "loginExpired": { "message": "로그인 세션이 만료되었습니다." @@ -855,19 +862,19 @@ "message": "로그인" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Bitwarden에 로그인" }, "restartRegistration": { - "message": "Restart registration" + "message": "등록 재시작" }, "expiredLink": { "message": "만료된 링크" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "등록 재시작 혹은 다시 로그인을 해주시길 바랍니다" }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "계정을 이미 가지고 계실수도 있습니다." }, "logOutConfirmation": { "message": "정말 로그아웃하시겠습니까?" @@ -891,10 +898,10 @@ "message": "2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Bitwarden 웹 앱에 2단계 인증을 설정하여, 당신의 계정을 좀 더 안전하게 만드세요." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "웹 앱으로 진행하나요?" }, "editedFolder": { "message": "폴더 편집함" @@ -981,7 +988,7 @@ "message": "로그인을 추가할 건지 물어보기" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "보관함 옵션들을 저장하기" }, "addLoginNotificationDesc": { "message": "\"로그인 추가 알림\"을 사용하면 새 로그인을 사용할 때마다 보관함에 그 로그인을 추가할 것인지 물어봅니다." @@ -990,22 +997,22 @@ "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "message": "보관함 보기에서 카드 자동완성 제안를 표시" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy autofill." + "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "message": "보관함 보기에서 신원들의 자동완성 제안을 표시" }, "showIdentitiesCurrentTab": { - "message": "Show identities on Tab page" + "message": "탭 페이지에 신원들을 표시" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy autofill." + "message": "간편한 자동완성을 위해 탭에 신원 항목들을 나열" }, "clearClipboard": { "message": "클립보드 비우기", @@ -1043,7 +1050,7 @@ "message": "예, 지금 변경하겠습니다." }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the autofill request." + "message": "Bitwarden 보관함을 잠금 해제 하여 자동완성 요청을 완료하세요." }, "notificationUnlock": { "message": "잠금 해제" @@ -1052,13 +1059,13 @@ "message": "추가 옵션" }, "enableContextMenuItem": { - "message": "Show context menu options" + "message": "문맥 매뉴 옵션 표시" }, "contextMenuItemDesc": { - "message": "Use a secondary click to access password generation and matching logins for the website." + "message": "우클릭을 사용하여, 비밀번호 생성과 웹사이트 로그인 매칭에 접근하세요" }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "우클릭을 사용하여, 웹사이트의 비밀번호 생성과 사용가능한 로그인들에 접근하세요. 모든 로그인 된 계정에 적용됩니다." }, "defaultUriMatchDetection": { "message": "기본 URI 일치 인식", @@ -1089,7 +1096,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "~(으)로부터 내보내기" }, "exportVault": { "message": "보관함 내보내기" @@ -1098,19 +1105,19 @@ "message": "파일 형식" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "이 파일 내보내기는 비밀번호로 보호될 것이며, 파일을 해독하기 위해서는 파일 비밀번호가 필요합니다." }, "filePassword": { "message": "파일 비밀번호" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "이 비밀번호는 이 파일을 파일 내보내거나, 가져오는데 사용됩니다." }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으로 가져오기를 제한해보세요. " }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "파일 비밀번호를 설정하여, 내보내기를 암호화하고, 해독에 그 파일 비밀번호를 사용하는 Bitwarden계정에 가져오세요." }, "exportTypeHeading": { "message": "내보내기 유형" @@ -1119,14 +1126,14 @@ "message": "계정 제한됨" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "파일 비밀번호와 파일 비밀번호 확인이 일치하지 않습니다." }, "warning": { "message": "경고", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "경고", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1148,7 +1155,7 @@ "message": "공유됨" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "비지니스용 Bitwarden은 조직을 사용하여 보관함 항목들을 다른 사람과 공유할 수 있게 해줍니다. 자세한 내용은 bitwarden.com 사이트에서 확인해주세요" }, "moveToOrganization": { "message": "조직으로 이동하기" @@ -1209,7 +1216,7 @@ "message": "파일" }, "fileToShare": { - "message": "File to share" + "message": "공유할 파일" }, "selectFile": { "message": "파일을 선택하세요." @@ -1221,7 +1228,7 @@ "message": "기능 사용할 수 없음" }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "암호화 키 마이그레이션이 필요합니다. 웹 볼트를 통해 로그인하여 암호화 키를 업데이트하세요." }, "premiumMembership": { "message": "프리미엄 멤버십" @@ -1245,10 +1252,10 @@ "message": "1GB의 암호화된 파일 저장소." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "비상 접근" }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "YubiKey나 Duo와 같은 독점적인 2단계 로그인 옵션" }, "ppremiumSignUpReports": { "message": "보관함을 안전하게 유지하기 위한 암호 위생, 계정 상태, 데이터 유출 보고서" @@ -1269,7 +1276,7 @@ "message": "bitwarden.com 웹 보관함에서 프리미엄 멤버십을 구입할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "Bitwarden 웹 앱의 계정 설정에서 프리미엄에 대한 결제를 할 수 있습니다." }, "premiumCurrentMember": { "message": "프리미엄 사용자입니다!" @@ -1278,7 +1285,7 @@ "message": "Bitwarden을 지원해 주셔서 감사합니다." }, "premiumFeatures": { - "message": "Upgrade to Premium and receive:" + "message": "프리미엄으로 업그래이드 하고 받기: " }, "premiumPrice": { "message": "이 모든 기능을 연 $PRICE$에 이용하실 수 있습니다!", @@ -1290,7 +1297,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "이 모든 기능을 연 $PRICE$에 이용하실 수 있습니다!", "placeholders": { "price": { "content": "$1", @@ -1319,6 +1326,12 @@ "enterVerificationCodeApp": { "message": "인증 앱에서 6자리 인증 코드를 입력하세요." }, + "authenticationTimeout": { + "message": "인증 시간 초과" + }, + "authenticationSessionTimedOut": { + "message": "인증 세션 시간이 초과 되었습니다. 다시 로그인을 시작해주세요." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ 주소로 전송된 6자리 인증 코드를 입력하세요.", "placeholders": { @@ -1383,17 +1396,17 @@ "message": "인증 앱" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Bitwarden 인증같은 인증 앱을 통해 코드를 생성하여 입력해주세요", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "YubiKey OTP 보안 키" }, "yubiKeyDesc": { "message": "YubiKey를 사용하여 사용자의 계정에 접근합니다. YubiKey 4, 4 Nano, 4C 및 NEO 기기를 사용할 수 있습니다." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Duo Security에서 생성한 코드를 입력하세요", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1410,7 +1423,7 @@ "message": "이메일" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "이메일로 전송된 코드를 입력하세요." }, "selfHostedEnvironment": { "message": "자체 호스팅 환경" @@ -1419,13 +1432,13 @@ "message": "온-프레미스 Bitwarden이 호스팅되고 있는 서버의 기본 URL을 지정하세요." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "온-프레미스 Bitwarden이 호스팅되고 있는 서버의 기본 URL을 지정하세요. 예: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "고급 구성의 경우 각 서비스의 기본 URL을 독립적으로 지정할 수 있습니다." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "기본 서버 URL이나 최소한 하나의 사용자 지정 환경을 추가해야 합니다." }, "customEnvironment": { "message": "사용자 지정 환경" @@ -1437,7 +1450,7 @@ "message": "서버 URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "자체 호스트 서버 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1463,28 +1476,28 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "자동 완성 제안" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "양식 필드에 자동 완성 제안 표시" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "신원를 제안으로 표시" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "카드를 제안으로 표시" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "아이콘을 선택하면 제안이 표시됩니다." }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "로그인한 모든 계정에 적용" }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "충돌을 방지하기 위해 브라우저의 기본 암호 관리 설정을 해제합니다." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "브라우저 설정 편집" }, "autofillOverlayVisibilityOff": { "message": "끄기", @@ -1499,7 +1512,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "페이지 로드 시 자동 완성" }, "enableAutoFillOnPageLoad": { "message": "페이지 로드 시 자동 완성 사용" @@ -1511,10 +1524,10 @@ "message": "취약하거나 신뢰할 수 없는 웹사이트 페이지 로드 시 자동 완성이 악용될 수 있습니다." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "위험에 대해 자세히 알아보기" }, "learnMoreAboutAutofill": { - "message": "Learn more about autofill" + "message": "자동 완정에 대해 자세히 할아보기" }, "defaultAutoFillOnPageLoad": { "message": "로그인 항목에 대한 기본 자동 완성 설정" @@ -1541,13 +1554,13 @@ "message": "사이드바에서 보관함 열기" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "현재 웹사이트에 마지막으로 사용된 로그인을 자동 채우기" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "현재 웹사이트에 마지막으로 사용된 카드를 자동 채우기" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "현재 웹사이트에 마지막으로 사용된 신원을 자동 채우기" }, "commandGeneratePasswordDesc": { "message": "새 무작위 비밀번호를 만들고 클립보드에 복사합니다." @@ -1580,7 +1593,7 @@ "message": "참 / 거짓" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "체크박스" }, "cfTypeLinked": { "message": "연결됨", @@ -1600,7 +1613,7 @@ "message": "웹사이트 아이콘 표시하기" }, "faviconDesc": { - "message": "Show a recognizable image next to each login." + "message": "로그인 정보 옆에 식별용 이미지를 표시합니다." }, "faviconDescAlt": { "message": "각 로그인 정보 옆에 인식할 수 있는 이미지를 표시합니다. 모든 로그인된 계정에 적용됩니다." @@ -1765,10 +1778,10 @@ "message": "신원" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH 키" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "새 $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1777,7 +1790,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "$TYPE$ 수정", "placeholders": { "type": { "content": "$1", @@ -1786,7 +1799,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "$TYPE$ 보기", "placeholders": { "type": { "content": "$1", @@ -1798,13 +1811,13 @@ "message": "비밀번호 변경 기록" }, "generatorHistory": { - "message": "Generator history" + "message": "생성기 기록" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "생성기 기록 지우기" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "계속하면 모든 항목이 생성기 기록에서 영구적으로 삭제됩니다. 계속하시겠습니까?" }, "back": { "message": "뒤로" @@ -1813,7 +1826,7 @@ "message": "컬렉션" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ 컬렉션", "placeholders": { "count": { "content": "$1", @@ -1843,7 +1856,7 @@ "message": "보안 메모" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH 키" }, "clear": { "message": "삭제", @@ -1869,7 +1882,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "기본 도메인 (추천)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1923,13 +1936,13 @@ "message": "비밀번호가 없습니다." }, "clearHistory": { - "message": "Clear history" + "message": "기록 지우기" }, "nothingToShow": { - "message": "Nothing to show" + "message": "항목 없음" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "최근에 생성한 것이 없습니다" }, "remove": { "message": "제거" @@ -1990,16 +2003,16 @@ "message": "PIN 코드를 사용하여 잠금 해제" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "PIN 설정" }, "setYourPinButton": { - "message": "Set PIN" + "message": "PIN 설정" }, "setYourPinCode": { "message": "Bitwarden 잠금해제에 사용될 PIN 코드를 설정합니다. 이 애플리케이션에서 완전히 로그아웃할 경우 PIN 설정이 초기화됩니다." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "PIN은 마스터 비밀번호 대신 Bitwarden 잠금해제에 사용됩니다. Bitwarden에서 완전히 로그아웃하면 PIN이 재설정됩니다." }, "pinRequired": { "message": "PIN 코드가 필요합니다." @@ -2014,7 +2027,7 @@ "message": "생체 인식을 사용하여 잠금 해제" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "마스터 비밀번호로 잠금 해제" }, "awaitDesktop": { "message": "데스크톱으로부터의 확인을 대기 중" @@ -2026,7 +2039,7 @@ "message": "브라우저 다시 시작 시 마스터 비밀번호로 잠금" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "브라우저 다시 시작 시 마스터 비밀번호가 필요합니다" }, "selectOneCollection": { "message": "반드시 하나 이상의 컬렉션을 선택해야 합니다." @@ -2041,33 +2054,33 @@ "message": "하나 이상의 단체 정책이 생성기 규칙에 영항을 미치고 있습니다." }, "passwordGenerator": { - "message": "Password generator" + "message": "비밀번호 생성기" }, "usernameGenerator": { - "message": "Username generator" + "message": "사용자 이름 생성기" }, "useThisPassword": { - "message": "Use this password" + "message": "이 비밀번호 사용" }, "useThisUsername": { - "message": "Use this username" + "message": "이 사용자 이름 사용" }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "보안 비밀번호가 생성되었습니다! 웹사이트에서 비밀번호를 업데이트하는 것도 잊지 마세요." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "생성기를 사용하세요", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "강력한 고유 비밀번호를 만들기 위해서는", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { "message": "보관함 시간 제한 초과시 동작" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "시간초과 시 행동" }, "lock": { "message": "잠금", @@ -2096,7 +2109,7 @@ "message": "복원된 항목" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "이미 계정이 있으신가요?" }, "vaultTimeoutLogOutConfirmation": { "message": "로그아웃하면 보관함에 대한 모든 접근이 제거되며 시간 제한을 초과하면 온라인 인증을 요구합니다. 정말로 이 설정을 사용하시겠습니까?" @@ -2108,7 +2121,7 @@ "message": "자동 완성 및 저장" }, "fillAndSave": { - "message": "Fill and save" + "message": "채우기 및 저장" }, "autoFillSuccessAndSavedUri": { "message": "항목을 자동 완성하고 URI를 저장함" @@ -2117,16 +2130,16 @@ "message": "항목을 자동 완성함" }, "insecurePageWarning": { - "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." + "message": "경고: 이 페이지는 보안이 해제된 HTTP 페이지이며, 제출한 모든 정보는 다른 사람이 보고 변경할 수 있습니다. 이 로그인은 원래 보안(HTTPS) 페이지에 저장되었습니다." }, "insecurePageWarningFillPrompt": { - "message": "Do you still wish to fill this login?" + "message": "여전히 이 로그인을 채우시겠습니까?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." + "message": "양식은 저장된 로그인의 URI가 아닌 다른 도메인에서 호스팅됩니다. 그래도 자동 완성을 사용하시려면 OK, 아니라면 취소 버튼을 선택해주세요." }, "autofillIframeWarningTip": { - "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", + "message": "향후 이 경고를 방지하려면 이 URI인 $HOSTNAME$(을)를 Bitwarden로그인 항목에 저장하세요.", "placeholders": { "hostname": { "content": "$1", @@ -2189,25 +2202,25 @@ "message": "새 마스터 비밀번호가 정책 요구 사항을 따르지 않습니다." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "Email 받은 편지함을 통해 Bitwarden의 조언, 공지사항 및 연구 기회들을 얻어보세요" }, "unsubscribe": { - "message": "Unsubscribe" + "message": "구독 취소" }, "atAnyTime": { - "message": "at any time." + "message": "언제든지" }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "계속하면 다음에 동의하게 됩니다" }, "and": { - "message": "and" + "message": "그리고" }, "acceptPolicies": { "message": "이 박스를 체크하면 다음에 동의하는 것으로 간주됩니다:" }, "acceptPoliciesRequired": { - "message": "Terms of Service and Privacy Policy have not been acknowledged." + "message": "서비스 약관 및 개인 정보 보호 정책을 확인하지 않았습니다." }, "termsOfService": { "message": "서비스 약관" @@ -2222,10 +2235,10 @@ "message": "확인" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "엑세스 토큰 새로고침 오류" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "새로 고침 토큰이나 API 키를 찾을 수 없습니다. 로그아웃하고 다시 로그인해 주세요" }, "desktopSyncVerificationTitle": { "message": "데스크톱과의 동기화 인증" @@ -2264,10 +2277,10 @@ "message": "계정이 일치하지 않음" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "생체인식 키 불일치" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "생체 인식 잠금 해제에 실패했습니다. 생체 인식 비밀 키가 보관함 잠금 해제에 실패했습니다. 생체 인식을 다시 설정해 보세요." }, "biometricsNotEnabledTitle": { "message": "생체 인식이 활성화되지 않음" @@ -2282,22 +2295,22 @@ "message": "이 기기에서는 생체 인식이 지원되지 않습니다." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "사용자 잠금 또는 로그아웃" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "데스크톱 애플리케이션에서 이 사용자의 잠금을 해제하고 다시 시도해 주세요." }, "biometricsNotAvailableTitle": { - "message": "Biometric unlock unavailable" + "message": "생체 인식 잠금 해제 사용 불가" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "생체 인식 잠금 해제는 현재 사용할 수 없습니다. 나중에 다시 시도해 주세요." }, "biometricsFailedTitle": { - "message": "Biometrics failed" + "message": "생체 인식 실패" }, "biometricsFailedDesc": { - "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + "message": "생체 인식을 완료할 수 없습니다. 마스터 비밀번호를 사용하거나 로그아웃하는 것을 고려하세요. 이 문제가 계속되면 Bitwarden 지원팀에 문의해 주세요." }, "nativeMessaginPermissionErrorTitle": { "message": "권한이 부여되지 않음" @@ -2318,10 +2331,10 @@ "message": "조직의 정책이 소유권 설정에 영향을 미치고 있습니다." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "조직 정책으로 인해 개별 보관함으로 항목을 가져오는 것이 차단되었습니다." }, "domainsTitle": { - "message": "Domains", + "message": "도메인", "description": "A category title describing the concept of web domains" }, "excludedDomains": { @@ -2331,10 +2344,10 @@ "message": "Bitwarden은 이 도메인들에 대해 로그인 정보를 저장할 것인지 묻지 않습니다. 페이지를 새로고침해야 변경된 내용이 적용됩니다." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "BItwarden은 로그인한 모든 계정에 대해 이러한 도메인에 대한 로그인 세부 정보를 저장하도록 요청하지 않습니다. 변경 사항을 적용하려면 페이지를 새로 고쳐야 합니다" }, "websiteItemLabel": { - "message": "Website $number$ (URI)", + "message": "웹사이트 $number$ (URI)", "placeholders": { "number": { "content": "$1", @@ -2352,17 +2365,17 @@ } }, "excludedDomainsSavedSuccess": { - "message": "Excluded domain changes saved" + "message": "제외된 도메인 변경 사항 저장됨" }, "limitSendViews": { - "message": "Limit views" + "message": "제한 보기" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "제한에 도달한 후에는 아무도 이 전송을 볼 수 없습니다.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "남은 $ACCESSCOUNT$ 횟수", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2372,26 +2385,26 @@ } }, "send": { - "message": "Send", + "message": "보내기", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Send details", + "message": "보내기 세부 정보", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { - "message": "Send 검색", + "message": " Send 검색", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "addSend": { - "message": "Send 추가", + "message": " Send 추가", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { "message": "텍스트" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "공유할 텍스트" }, "sendTypeFile": { "message": "파일" @@ -2401,7 +2414,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "기본적으로 텍스트 숨기기" }, "maxAccessCountReached": { "message": "최대 접근 횟수 도달", @@ -2417,10 +2430,10 @@ "message": "비밀번호로 보호됨" }, "copyLink": { - "message": "Copy link" + "message": "링크 복사" }, "copySendLink": { - "message": "Send 링크 복사", + "message": " Send 링크 복사", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { @@ -2433,7 +2446,7 @@ "message": "비밀번호 제거함" }, "deletedSend": { - "message": "Send 삭제함", + "message": " Send 삭제함", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { @@ -2447,27 +2460,27 @@ "message": "비밀번호를 제거하시겠습니까?" }, "deleteSend": { - "message": "Send 삭제", + "message": " Send 삭제", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendConfirmation": { - "message": "정말 이 Send를 삭제하시겠습니까?", + "message": "정말 이 Send를 삭제하시겠습니까?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "이 Send을 영구적으로 삭제하시겠습니까?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { - "message": "Send 편집", + "message": " Send 편집", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeHeader": { - "message": "어떤 유형의 Send인가요?", + "message": "어떤 유형의 Send인가요?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNameDesc": { - "message": "이 Send의 이름", + "message": "이 Send을 설명할 이름", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFileDesc": { @@ -2477,11 +2490,11 @@ "message": "삭제 날짜" }, "deletionDateDesc": { - "message": "이 Send가 정해진 일시에 영구적으로 삭제됩니다.", + "message": "이 Send가 정해진 일시에 영구적으로 삭제됩니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "이 Send가 이 날짜에 영구적으로 삭제됩니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -2495,7 +2508,7 @@ "message": "1일" }, "days": { - "message": "$DAYS$일", + "message": "$DAYS$ 일", "placeholders": { "days": { "content": "$1", @@ -2518,7 +2531,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "수신자가 이 Send에 액세스할 수 있도록 비밀번호 옵션를 추가합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2563,15 +2576,15 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "Send가 성공적으로 생성되었습니다!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "이 Send는 링크가 있는 누구나 향후 1시간 동안 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "이 전송은 링크가 있는 누구나 향후 $HOURS$ 시간 동안 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2581,11 +2594,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "이 Send은 향후 1일 동안 링크가 있는 누구나 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "이 Send은 향후 $DAYS$일 동안 링크가 있는 누구나 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2595,19 +2608,19 @@ } }, "sendLinkCopied": { - "message": "Send link copied", + "message": "Send 링크 복사됨", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Send 수정함", + "message": "Send 수정됨", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogText": { - "message": "Pop out extension?", + "message": "확장자를 새 창에서 열까요?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out the extension to a new window.", + "message": "파일 Send를 만들려면, 새 창으로 확장자를 열어야 합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -2620,7 +2633,7 @@ "message": "Safari에서 파일을 선택할 경우, 이 배너를 클릭하여 확장 프로그램을 새 창에서 여세요." }, "popOut": { - "message": "Pop out" + "message": "새 창에서 열기" }, "sendFileCalloutHeader": { "message": "시작하기 전에" @@ -2656,7 +2669,7 @@ "message": "받는 사람으로부터 나의 이메일 주소 숨기기" }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "사람들로부터 이메일 주소를 숨기세요." }, "sendOptionsPolicyInEffect": { "message": "하나 이상의 단체 정책이 Send 설정에 영향을 미치고 있습니다." @@ -2674,7 +2687,7 @@ "message": "이메일 인증 필요함" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "이메일 인증됨" }, "emailVerificationRequiredDesc": { "message": "이 기능을 사용하려면 이메일 인증이 필요합니다. 웹 보관함에서 이메일을 인증할 수 있습니다." @@ -2689,10 +2702,10 @@ "message": "최근에 조직 관리자가 마스터 비밀번호를 변경했습니다. 보관함에 액세스하려면 지금 업데이트해야 합니다. 계속하면 현재 세션에서 로그아웃되며 다시 로그인해야 합니다. 다른 장치의 활성 세션은 최대 1시간 동안 계속 활성 상태로 유지될 수 있습니다." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "마스터 비밀번호가 조직 정책 중 하나 이상을 충족하지 못합니다. 보관함에 액세스하려면, 지금 마스터 비밀번호를 업데이트해야 합니다. 계속 진행하면 현재 세션에서 로그아웃되므로, 다시 로그인해야 합니다. 다른 장치에서 활성 세션은 최대 1시간 동안 계속 활성 상태로 유지될 수 있습니다." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "조직에서 신뢰할 수 있는 장치 암호화를 비활성화했습니다. 보관함에 접근하려면 마스터 비밀번호를 설정하세요." }, "resetPasswordPolicyAutoEnroll": { "message": "자동 등록" @@ -2708,15 +2721,15 @@ "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "조직 권한이 업데이트되어 마스터 비밀번호를 설정해야 합니다.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "당신의 조직은 마스터 비밀번호를 설정해야 합니다.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "$TOTAL$ 중에서", "placeholders": { "total": { "content": "$1", @@ -2735,7 +2748,7 @@ "message": "분" }, "vaultTimeoutPolicyAffectingOptions": { - "message": "Enterprise policy requirements have been applied to your timeout options" + "message": "타임아웃 옵션에 기업의 정책 요구 사항이 적용되었습니다" }, "vaultTimeoutPolicyInEffect": { "message": "조직 정책이 보관함 제한 시간에 영향을 미치고 있습니다. 최대 허용 보관함 제한 시간은 $HOURS$시간 $MINUTES$분입니다", @@ -2751,7 +2764,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "최대 $HOURS$시간 $MINUTES$분", "placeholders": { "hours": { "content": "$1", @@ -2764,7 +2777,7 @@ } }, "vaultTimeoutPolicyMaximumError": { - "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "message": "타임아웃이 조직에서 설정한 제한을 초과합니다: 최대 $HOURS$시간 $MINUTES$분", "placeholders": { "hours": { "content": "$1", @@ -2777,7 +2790,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "조직 정책이 보관함 타임아웃에 영향을 미치고 있습니다. 최대 허용 보관함 타임아웃은 최대 $HOURS$시간 $MINUTES$분입니다. 보관함 타임아웃 작업은 $ACTION$으로 설정되어 있습니다.", "placeholders": { "hours": { "content": "$1", @@ -2794,7 +2807,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "조직 정책에 따라 보관함 타임아웃 작업이 $ACTION$으로 설정되었습니다.", "placeholders": { "action": { "content": "$1", @@ -2803,7 +2816,7 @@ } }, "vaultTimeoutTooLarge": { - "message": "Your vault timeout exceeds the restrictions set by your organization." + "message": "보관함 시간 초과가 조직에서 설정한 제한을 초과합니다." }, "vaultExportDisabled": { "message": "보관함 내보내기 비활성화됨" @@ -2851,7 +2864,7 @@ "message": "개인 보관함을 내보내는 중" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "$EMAIL$과 관련된 개별 보관함 항목만 내보냅니다. 조직 보관함 항목은 포함되지 않습니다. 보관함 항목 정보만 내보내며 관련 첨부 파일은 포함되지 않습니다", "placeholders": { "email": { "content": "$1", @@ -2860,10 +2873,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "조직 보관함을 내보내는 중" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "$ORGANIZATION$ 조직과 연관된 조직 보관함만 내보내기됩니다. 개인 보관함이나 다른 조직의 항목은 포함되지 않습니다.", "placeholders": { "organization": { "content": "$1", @@ -2881,10 +2894,10 @@ "message": "아이디 생성" }, "generateEmail": { - "message": "Generate email" + "message": "이메일 생성" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "값은 $MIN$과 $MAX$ 사이여야 합니다", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2898,7 +2911,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " 강력한 비밀번호를 생성하려면 $RECOMMENDED$ 문자 이상을 사용하세요", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2908,7 +2921,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " 강력한 암호를 생성하려면 $RECOMMENDED$ 단어 이상을 사용하세요.", "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": { @@ -2921,17 +2934,17 @@ "message": "아이디 유형" }, "plusAddressedEmail": { - "message": "Plus addressed email", + "message": "추가 이메일", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "이메일 제공업체의 하위 주소 지정 기능을 사용하세요." }, "catchallEmail": { - "message": "Catch-all email" + "message": "Catch-all 이메일 (도메인 상의 어떤 주소로도 전송된 이메일을 받을 수 있는 주소)" }, "catchallEmailDesc": { - "message": "Use your domain's configured catch-all inbox." + "message": "catch-all이 설정된 내 도메인의 메일함을 사용하세요." }, "random": { "message": "무작위" @@ -2955,18 +2968,18 @@ "message": "포워딩된 이메일 별칭" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "외부 포워딩 서비스를 사용해서 이메일 주소 별칭을 만들어보세요." }, "forwarderDomainName": { - "message": "Email domain", + "message": "이메일 도메인", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "선택한 서비스에서 지원하는 도메인 선택", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ 오류: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2980,11 +2993,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Bitwarden에서 생성됨", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "웹사이트: $WEBSITE$. Bitwarden에서 생성됨", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2994,7 +3007,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "잘못된 $SERVICENAME$ API 토큰\n", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3004,7 +3017,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "잘못된 $SERVICENAME$ API 토큰: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3018,7 +3031,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "$SERVICENAME$ 마스크된 이메일 계정 ID를 얻을 수 없습니다.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3028,7 +3041,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "잘못된 $SERVICENAME$ 도메인.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3038,7 +3051,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "잘못된 $SERVICENAME$ URL", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -3048,7 +3061,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "알 수 없는 $SERVICENAME$ 오류가 발생했습니다.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3058,7 +3071,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "알 수 없는 포워더: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3084,13 +3097,13 @@ "message": "프리미엄 구독이 필요합니다" }, "organizationIsDisabled": { - "message": "Organization suspended." + "message": "조직이 중지됨" }, "disabledOrganizationFilterError": { - "message": "Items in suspended Organizations cannot be accessed. Contact your Organization owner for assistance." + "message": "중단된 조직의 항목에 액세스할 수 없습니다. 조직 소유자에게 도움을 요청하세요." }, "loggingInTo": { - "message": "Logging in to $DOMAIN$", + "message": "$DOMAIN$(으)로 로그인", "placeholders": { "domain": { "content": "$1", @@ -3099,13 +3112,13 @@ } }, "settingsEdited": { - "message": "Settings have been edited" + "message": "설정이 편집되었습니다" }, "environmentEditedClick": { - "message": "Click here" + "message": "여기를 클릭하세요." }, "environmentEditedReset": { - "message": "to reset to pre-configured settings" + "message": "사전 구성된 설정으로 재설정하려면" }, "serverVersion": { "message": "서버 버전" @@ -3117,7 +3130,7 @@ "message": "제 3자" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "제 3자 서버 구현에 연결되었습니다. $SERVERNAME$. 공식 서버를 사용하여 버그를 확인하거나 타사 서버에 보고해 주세요.", "placeholders": { "servername": { "content": "$1", @@ -3126,7 +3139,7 @@ } }, "lastSeenOn": { - "message": "last seen on: $DATE$", + "message": "확인된 날짜: $DATE$", "placeholders": { "date": { "content": "$1", @@ -3135,10 +3148,10 @@ } }, "loginWithMasterPassword": { - "message": "Log in with master password" + "message": "마스터 비밀번호로 로그인" }, "loggingInAs": { - "message": "Logging in as" + "message": "다음으로 로그인 중" }, "notYou": { "message": "본인이 아닌가요?" @@ -3150,67 +3163,67 @@ "message": "이메일 기억하기" }, "loginWithDevice": { - "message": "Log in with device" + "message": "기기로 로그인" }, "loginWithDeviceEnabledInfo": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "기기로 로그인하려면 Bitwarden 모바일 앱 설정에서 설정해야 합니다. 다른 방식이 필요하신가요?" }, "fingerprintPhraseHeader": { - "message": "Fingerprint phrase" + "message": "지문 구절" }, "fingerprintMatchInfo": { - "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." + "message": "반드시 보관함이 잠금 해제되었고, 지문 구절이 다른 기기에서 일치하는지 확인해주세요." }, "resendNotification": { - "message": "Resend notification" + "message": "알림 다시 보내기" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "모든 로그인 방식 보기" }, - "viewAllLoginOptions": { + "viewAllLoginOptionsV1": { "message": "View all log in options" }, "notificationSentDevice": { - "message": "A notification has been sent to your device." + "message": "기기에 알림이 전송되었습니다." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "기기에 알림이 전송되었습니다." }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "반드시 계정이 잠금 해제되었고, 지문 구절이 다른 기기에서 일치하는지 확인해주세요." }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "요청이 승인되면 알림을 받게 됩니다" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "다른 옵션이 필요하신가요?" }, "loginInitiated": { - "message": "Login initiated" + "message": "로그인 시작" }, "exposedMasterPassword": { - "message": "Exposed Master Password" + "message": "노출된 마스터 비밀번호" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "데이터 유출이 된 비밀번호임이 발견되었습니다. 계정을 보호하려면 고유한 비밀번호를 사용하세요. 노출된 비밀번호를 사용하시겠습니까?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "취약하고 노출된 마스터 비밀번호" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" + "message": "데이터 유출이 된 약한 비밀번호임이 발견되었습니다. 계정을 보호하려면 강력하고 고유한 비밀번호를 사용하세요. 이 비밀번호를 사용하시겠습니까?" }, "checkForBreaches": { - "message": "Check known data breaches for this password" + "message": "이 비밀번호에 대한 알려진 데이터 유출 확인\n" }, "important": { - "message": "Important:" + "message": "중요:" }, "masterPasswordHint": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "마스터 비밀번호를 잊어버리면 복구할 수 없습니다!\n" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "최소 $LENGTH$ 문자", "placeholders": { "length": { "content": "$1", @@ -3219,13 +3232,13 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on autofill on page load." + "message": "조직 정책에 따라, 페이지 로드 시 자동 완성 기능을 켰습니다." }, "howToAutofill": { - "message": "How to autofill" + "message": "자동 완성 사용법" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "이 화면에서 항목을 선택하거나, 바로 가기 $COMMAND$를 사용하거나, 설정의 다른 옵션을 탐색하세요.", "placeholders": { "command": { "content": "$1", @@ -3234,31 +3247,31 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "이 화면에서 항목을 선택하거나 설정의 다른 옵션을 탐색하세요." }, "gotIt": { - "message": "Got it" + "message": "이해했습니다" }, "autofillSettings": { "message": "자동 완성 설정" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "자동 완성 바로가기" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "바로가기 변경" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "바로가기 관리" }, "autofillShortcut": { "message": "자동 완성 키보드 단축키" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "자동 채우기 로그인 바로 가기가 설정되어 있지 않습니다. 브라우저 설정에서 이 항목을 변경해주세요." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "자동 채우기 로그인 바로 가기는 $COMMAND$입니다. 브라우저 설정의 모든 바로 가기를 관리하세요.", "placeholders": { "command": { "content": "$1", @@ -3267,7 +3280,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default autofill shortcut: $COMMAND$.", + "message": "기본 자동 완성 바로 가기: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -3276,65 +3289,65 @@ } }, "opensInANewWindow": { - "message": "Opens in a new window" + "message": "새 창에서 열립니다" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "향후 로그인을 원활하게 하기 위해 이 기기 기억하기" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "기기 승인이 필요합니다. 아래에서 승인 옵션을 선택하세요:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "기기 승인이 필요합니다." }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "아래에서 승인 옵션을 선택하세요" }, "rememberThisDevice": { "message": "이 기기 기억하기" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "공용 기기를 사용하는 경우 체크 해제" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "다른 장치에서 승인" }, "requestAdminApproval": { - "message": "관리자 승인 필요" + "message": "관리자 인증 필요" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "마스터 비밀번호로 승인" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "조직의 SSO 식별자가 필요합니다" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "계정 만들기" }, "checkYourEmail": { - "message": "Check your email" + "message": "이메일을 확인해주세요" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "이메일로 전송한 링크를 통해" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "계정을 계속 생성하세요." }, "noEmail": { - "message": "No email?" + "message": "이메일이 전송되지 않았나요?" }, "goBack": { - "message": "Go back" + "message": "뒤로 돌아가서" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "이메일 주소를 수정하기" }, "eu": { "message": "EU", "description": "European Union" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "접근이 거부되었습니다. 이 페이지를 볼 권한이 없습니다." }, "general": { "message": "일반" @@ -3349,45 +3362,45 @@ "message": "관리자 승인 필요" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "요청이 관리자에게 전송되었습니다." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "승인되면 알림을 받게 됩니다." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "로그인에 문제가 있나요?" }, "loginApproved": { - "message": "Login approved" + "message": "로그인 승인됨" }, "userEmailMissing": { - "message": "User email missing" + "message": "사용자 이메일 누락" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "활성화된 사용자의 이메일을 찾을 수 없습니다. 로그아웃합니다." }, "deviceTrusted": { - "message": "Device trusted" + "message": "신뢰할 수 있는 장치" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "활성화된 Send없음", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Send를 사용하여 암호화된 정보를 어느 사람과도 안전하게 공유합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { - "message": "Input is required." + "message": "입력이 필요합니다." }, "required": { - "message": "required" + "message": "필수" }, "search": { "message": "검색" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "입력은 최소한 $COUNT$자 이상이어야 합니다.", "placeholders": { "count": { "content": "$1", @@ -3396,7 +3409,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "입력 길이는 $COUNT$자를 초과해서는 안 됩니다.", "placeholders": { "count": { "content": "$1", @@ -3405,7 +3418,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "다음 문자는 허용되지 않습니다: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -3414,7 +3427,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "입력 값은 최소 $MIN$자 이상이어야 합니다.", "placeholders": { "min": { "content": "$1", @@ -3423,7 +3436,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "입력 값은 $MAX$ 자를 초과해서는 안 됩니다.", "placeholders": { "max": { "content": "$1", @@ -3435,14 +3448,14 @@ "message": "하나 이상의 이메일이 유효하지 않습니다." }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "입력에는 공백만 포함해서는 안 됩니다.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "입력이 이메일 주소가 아닙니다" }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "위의 $COUNT$ 필드에 주의가 필요합니다", "placeholders": { "count": { "content": "$1", @@ -3451,10 +3464,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1개의 필드가 주의가 필요합니다." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ 개의 필드가 주의가 필요합니다.", "placeholders": { "count": { "content": "$1", @@ -3463,22 +3476,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- 선택 --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "- 필터링할 유형 --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "옵션을 검색하는 중..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "항목을 찾을 수 없습니다" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "모두 지우기" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$개 더보기", "placeholders": { "quantity": { "content": "$1", @@ -3487,30 +3500,30 @@ } }, "submenu": { - "message": "Submenu" + "message": "하위 메뉴" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "토글이 붕괴됨", "description": "Toggling an expand/collapse state." }, "filelessImport": { - "message": "Import your data to Bitwarden?", + "message": "데이터를 Bitwarden으로 가져오시겠습니까?", "description": "Default notification title for triggering a fileless import." }, "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", + "message": "LastPass 데이터를 보호하고 Bitwarden으로 가져오시겠습니까?", "description": "LastPass specific notification title for triggering a fileless import." }, "lpCancelFilelessImport": { - "message": "Save as unencrypted file", + "message": "암호화되지 않은 파일로 저장", "description": "LastPass specific notification button text for cancelling a fileless import." }, "startFilelessImport": { - "message": "Import to Bitwarden", + "message": "Bitwarden으로 가져오기", "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "가져오는 중...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { @@ -3518,52 +3531,52 @@ "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "가져오는 중 오류가 발생했습니다. 자세한 내용은 콘솔을 확인하세요.", "description": "Notification message for when an import has failed." }, "importNetworkError": { - "message": "Network error encountered during import.", + "message": "가져오기 중에 네트워크 오류가 발생했습니다.", "description": "Notification message for when an import has failed due to a network error." }, "aliasDomain": { - "message": "Alias domain" + "message": "도메인 별칭" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "message": "마스터 비밀번호 재 요청이 있는 항목은 페이지 로드에서 자동으로 채울 수 없습니다. 페이지 로드의 자동 완성이 꺼졌습니다.", "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Autofill on page load set to use default setting.", + "message": "페이지 로드 시 자동 완성이 기본 설정을 사용하도록 설정되었습니다.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Turn off master password re-prompt to edit this field", + "message": "마스터 암호 재 요청을 해제하여 이 필드를 편집합니다", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "사이드 내비게이션 전환" }, "skipToContent": { - "message": "Skip to content" + "message": "콘텐츠로 건너뛰기" }, "bitwardenOverlayButton": { - "message": "Bitwarden autofill menu button", + "message": "Bitwarden 자동 완성 메뉴 버튼", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden autofill menu", + "message": "Bitwarden 자동 완성메뉴 전환", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "Bitwarden 자동 완성 매뉴", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "일치하는 로그인을 보기위해 계정을 잠금해제하세요", "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "계정 잠금을 해제하여 자동 채우기 제안 보기", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -3571,19 +3584,27 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "계정 잠금을 해제하기, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { - "message": "Fill credentials for", + "message": "자격 증명 채우기", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { - "message": "Partial username", + "message": "부분적인 사용자 이름", "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "표시할 항목 없음", "description": "Text to show in overlay if there are no matching items" }, "newItem": { @@ -3591,61 +3612,61 @@ "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { - "message": "Add new vault item", + "message": "새 보관함 항목 추가", "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "새 로그인", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "새 보관함 로그인 항목 추가, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "새 카드", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "새 보관함 카드 항목 추가, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "신규 ID", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "새 보관함 ID 항목 추가, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden autofill menu available. Press the down arrow key to select.", + "message": "Bitwarden 자동 완성 메뉴를 사용할 수 있습니다. 아래쪽 화살표 키를 눌러 선택하세요.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "켜기" }, "ignore": { - "message": "Ignore" + "message": "무시하기" }, "importData": { - "message": "Import data", + "message": "데이터 가져오기", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "가져오기 오류" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "가져오려고 하는 데이터에 문제가 있습니다. 아래에 표시된 파일의 오류를 해결한 뒤 다시 시도해 주세요." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "아래 오류를 해결하고 다시 시도하세요." }, "description": { - "message": "Description" + "message": "설명" }, "importSuccess": { - "message": "Data successfully imported" + "message": "데이터 가져오기 성공" }, "importSuccessNumberOfItems": { "message": "A total of $AMOUNT$ items were imported.", @@ -3660,43 +3681,43 @@ "message": "다시 시도" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "이 작업을 수행하려면 증명이 필요합니다. 계속하려면 PIN을 설정하세요." }, "setPin": { - "message": "Set PIN" + "message": "PIN 설정하기" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "생체 인식을 사용하여 증명하기" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "확인 대기 중" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "생체 인식을 완료할 수 없습니다." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "다른 방법이 필요하신가요?" }, "useMasterPassword": { - "message": "Use master password" + "message": "마스터 비밀번호를 사용하기" }, "usePin": { - "message": "Use PIN" + "message": "PIN 사용하기" }, "useBiometrics": { - "message": "Use biometrics" + "message": "생체 인식 사용하기" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "이메일로 전송된 인증 코드를 입력해주세요" }, "resendCode": { - "message": "Resend code" + "message": "코드 재전송" }, "total": { - "message": "Total" + "message": "합계" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "데이터를 $ORGANIZATION$로 가져오고 있습니다. 데이터를 이 조직의 구성원들과 공유할 수 있습니다. 계속 진행하시겠습니까?", "placeholders": { "organization": { "content": "$1", @@ -3705,19 +3726,19 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "Duo 서비스 연결 중 오류가 발생했습니다. 다른 2단계 로그인 방법을 사용하거나 Duo에 문의하여 도움을 받으세요." }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "듀오를 실행하고 단계를 따라 로그인을 완료하세요" }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "계정에 Duo 2단계 로그인이 필요합니다." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "확장 프로그램을 실행하여 로그인을 완료합니다." }, "popoutExtension": { - "message": "Popout extension" + "message": "확장 프로그램을 새 창에서 열기" }, "launchDuo": { "message": "Duo 실행" @@ -3729,25 +3750,25 @@ "message": "아무것도 가져오지 못했습니다." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "내보내려는 파일을 복호화하던 중 오류가 발생했습니다. 암호화 키가 내보내려는 데이터를 암호화한 키와 일치하지 않습니다." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "파일 비밀번호가 잘못되었습니다. 내보내기 파일을 만들 때 입력한 비밀번호를 사용해 주세요." }, "destination": { - "message": "Destination" + "message": "수신자" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "가져오기 옵션 알아보기" }, "selectImportFolder": { - "message": "Select a folder" + "message": "폴더 선택" }, "selectImportCollection": { - "message": "Select a collection" + "message": "컬렉션 선택" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "가져온 파일의 내용을 $DESTINATION$로 이동하려면 이 옵션을 선택하세요.", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3757,25 +3778,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "파일에 할당되지 않은 항목이 포함되어 있습니다." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "불러올 파일의 포맷 선택" }, "selectImportFile": { - "message": "Select the import file" + "message": "불러올 파일 선택" }, "chooseFile": { - "message": "Choose File" + "message": "파일 선택" }, "noFileChosen": { - "message": "No file chosen" + "message": "선택된 파일 없음" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "또는 가져온 파일 내용 복사/붙여넣기" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ 지침", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3785,25 +3806,25 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "보관함 가져오기 확인" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "이 파일은 비밀번호로 보호받고 있습니다. 데이터를 가져오려면 파일 비밀번호를 입력하세요." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "파일 비밀번호 확인" }, "exportSuccess": { - "message": "Vault data exported" + "message": "보관함 데이터 내보내짐" }, "typePasskey": { "message": "패스키" }, "accessing": { - "message": "Accessing" + "message": "접근 중" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "로그인 완료!" }, "passkeyNotCopied": { "message": "패스키가 복사되지 않습니다" @@ -3815,7 +3836,7 @@ "message": "사이트에서 인증을 요구합니다. 이 기능은 비밀번호가 없는 계정에서는 아직 지원하지 않습니다." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "패스키로 로그인하시겠어요?" }, "passkeyAlreadyExists": { "message": "이미 이 애플리케이션에 해당하는 패스키가 있습니다." @@ -3824,16 +3845,16 @@ "message": "이 애플리케이션에 대한 패스키를 찾을 수 없습니다." }, "noMatchingPasskeyLogin": { - "message": "사이트와 일치하는 로그인이 없습니다." + "message": "이 사이트와 일치하는 로그인이 없습니다." }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "사이트와 일치하는 로그인 없음" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "패스키를 새 로그인으로 검색 또는 저장" }, "confirm": { - "message": "Confirm" + "message": "확인" }, "savePasskey": { "message": "패스키 저장" @@ -3842,10 +3863,10 @@ "message": "새 로그인으로 패스키 저장" }, "chooseCipherForPasskeySave": { - "message": "Choose a login to save this passkey to" + "message": "패스키를 저장할 로그인 선택하기" }, "chooseCipherForPasskeyAuth": { - "message": "Choose a passkey to log in with" + "message": "로그인할 패스키 선택" }, "passkeyItem": { "message": "패스키 항목" @@ -3857,128 +3878,128 @@ "message": "이 항목은 이미 패스키가 있습니다. 정말로 현재 패스키를 덮어쓰시겠어요?" }, "featureNotSupported": { - "message": "Feature not yet supported" + "message": "아직 지원되지 않는 기능" }, "yourPasskeyIsLocked": { "message": "패스키를 사용하려면 인증이 필요합니다. 인증을 진행해주세요." }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "멀티팩터 인증이 취소되었습니다" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "LastPass 데이터를 찾을 수 없습니다" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "잘못된 사용자 이름 또는 비밀번호 입니다." }, "incorrectPassword": { - "message": "Incorrect password" + "message": "잘못된 비밀번호입니다" }, "incorrectCode": { - "message": "Incorrect code" + "message": "잘못된 코드입니다." }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "올바르지 않은 PIN입니다." }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "멀티팩터 인증 실패" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "공유 폴더 포함" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "LastPass 이메일" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "계정 가져오기 중..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "LastPass 멀티팩터 인증 필요" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "인증 앱에서 일회용 비밀번호 입력하기" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "인증 앱에서 로그인 요청을 승인하거나 일회용 비밀번호를 입력하세요" }, "passcode": { - "message": "Passcode" + "message": "비밀번호" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "LastPass 마스터 비밀번호" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "LastPass 인증 필요" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "SSO 인증 대기 중" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "회사 자격 증명을 사용하여 계속 로그인해 주세요." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "도움말 사이트에서 자세한 지침을 확인하세요", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "LastPass에서 직접 가져오기" }, "importFromCSV": { - "message": "Import from CSV" + "message": "CSV에서 가져오기" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "다시 시도하거나 LastPass에서 이메일을 찾아 사용자임을 증명하세요." }, "collection": { - "message": "Collection" + "message": "컬렉션" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "LastPass 계정과 연결된 YubiKey를 컴퓨터의 USB 포트에 삽입한 다음 버튼을 누릅니다." }, "switchAccount": { - "message": "Switch account" + "message": "계정 전환" }, "switchAccounts": { - "message": "Switch accounts" + "message": "계정 전환" }, "switchToAccount": { - "message": "Switch to account" + "message": "계정 전환" }, "activeAccount": { - "message": "Active account" + "message": "계정 활성화" }, "availableAccounts": { - "message": "Available accounts" + "message": "사용 가능한 계정" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "계정 개수 제한에 도달했습니다. 추가로 로그인하려면 다른 계정을 로그아웃 해주세요." }, "active": { - "message": "active" + "message": "활성" }, "locked": { - "message": "locked" + "message": "잠김" }, "unlocked": { - "message": "unlocked" + "message": "잠금 해제됨" }, "server": { - "message": "server" + "message": "서버" }, "hostedAt": { - "message": "hosted at" + "message": "호스팅된" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "기기또는 하드웨어 키를 사용하세요" }, "justOnce": { - "message": "Just once" + "message": "한 번만 알림" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "항상 이 사이트에 대해" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "제외된 도메인에 $DOMAIN$이 추가되었습니다.", "placeholders": { "domain": { "content": "$1", @@ -3987,31 +4008,31 @@ } }, "commonImportFormats": { - "message": "Common formats", + "message": "일반적인 형식", "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "브라우저 설정으로 이동하시겠습니까?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "도움말 센터로 이동하시겠습니까?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "브라우저의 자동 완성 및 비밀번호 관리 설정을 변경합니다.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "브라우저 설정에서 확장 단축키를 보고, 설정할 수 있습니다.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "브라우저의 자동 채우기 및 비밀번호 관리 설정을 변경합니다.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "브라우저 설정에서 확장 단축키를 보고, 설정할 수 있습니다.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { @@ -4019,7 +4040,7 @@ "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "이 옵션을 무시하면 Bitwarden 자동 완성 제안과 브라우저 간에 충돌이 발생할 수 있습니다", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -4027,39 +4048,39 @@ "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Bitwarden을 기본 비밀번호 관리자로 설정할 수 없습니다", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "기본 비밀번호 관리자로 설정하려면 Bitwarden에게 브라우저 개인정보 보호 권한을 부여해야 합니다.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "기본값으로 만들기", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "자격 증명이 성공적으로 저장됨!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "비밀번호 저장됨!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "자격 증명이 성공적으로 업데이트됨!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "비밀번호 업데이트됨!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "자격 증명 저장 중 오류가 발생했습니다. 자세한 내용은 콘솔을 확인하세요.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "성공" }, "removePasskey": { "message": "패스키 제거" @@ -4068,22 +4089,22 @@ "message": "패스키 제거됨" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "자동 완성 제안" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "이 사이트에서 자동으로 작성할 로그인 항목 저장" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "당신의 보관함이 비어있습니다" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "사이트와 일치하는 항목 없음" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "필터 지우기 또는 다른 검색어 시도" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "정보 복사 - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4093,7 +4114,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "메모 복사 - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4103,7 +4124,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "$ITEMNAME$ 의 다른 옵션", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4113,7 +4134,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "다른 옵션 - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4123,7 +4144,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "항목 보기 - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4133,7 +4154,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "자동 완성 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4143,22 +4164,22 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "복사할 값이 없습니다" }, "assignToCollections": { - "message": "Assign to collections" + "message": "컬렉션에 할당하기" }, "copyEmail": { - "message": "Copy email" + "message": "이메일 복사하기" }, "copyPhone": { - "message": "Copy phone" + "message": "전화번호 복사하기" }, "copyAddress": { - "message": "Copy address" + "message": "주소 복사하기" }, "adminConsole": { - "message": "Admin Console" + "message": "관리자 콘솔" }, "accountSecurity": { "message": "계정 보안" @@ -4170,13 +4191,13 @@ "message": "화면 스타일" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "대상 컬렉션을 할당하는 중 오류가 발생했습니다." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "대상 폴더를 할당하는 중 오류가 발생했습니다." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "$NAME$에서 항목 보기", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4186,7 +4207,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "다시 $NAME$로 돌아가기", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4196,10 +4217,10 @@ } }, "new": { - "message": "New" + "message": "새 항목" }, "removeItem": { - "message": "Remove $NAME$", + "message": "$NAME$ 제거", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4209,16 +4230,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "폴더가 없는 항목" }, "itemDetails": { - "message": "Item details" + "message": "항목 세부사항" }, "itemName": { - "message": "Item name" + "message": "항목 이름" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "보기 권한만 있는 컬렉션은 제거할 수 없습니다: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -4227,47 +4248,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "조직이 비활성화되었습니다" }, "owner": { - "message": "Owner" + "message": "소유자" }, "selfOwnershipLabel": { - "message": "You", + "message": "당신", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "비활성화된 조직의 항목에 액세스할 수 없습니다. 조직 소유자에게 도움을 요청하세요." }, "additionalInformation": { - "message": "Additional information" + "message": "추가 정보" }, "itemHistory": { - "message": "Item history" + "message": "항목 기록" }, "lastEdited": { - "message": "Last edited" + "message": "최근 수정 날짜:" }, "ownerYou": { - "message": "Owner: You" + "message": "소유자: 당신" }, "linked": { - "message": "Linked" + "message": "연결됨" }, "copySuccessful": { - "message": "Copy Successful" + "message": "복사 성공" }, "upload": { - "message": "Upload" + "message": "업로드" }, "addAttachment": { - "message": "Add attachment" + "message": "첨부파일 추가" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "최대 파일 크기는 500MB입니다." }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "첨부파일 $NAME$ 삭제", "placeholders": { "name": { "content": "$1", @@ -4276,7 +4297,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "$NAME$ 다운로드", "placeholders": { "name": { "content": "$1", @@ -4285,25 +4306,25 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "정말로 이 첨부파일을 영구적으로 삭제하시겠습니까?" }, "premium": { - "message": "Premium" + "message": "프리미엄" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "무료 조직에서는 첨부 파일을 사용할 수 없습니다." }, "filters": { - "message": "Filters" + "message": "필터" }, "filterVault": { - "message": "Filter vault" + "message": "보관함 필터링" }, "filterApplied": { - "message": "One filter applied" + "message": "필터 1개가 적용되었습니다" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$개의 필터가 적용되었습니다", "placeholders": { "count": { "content": "$1", @@ -4312,16 +4333,16 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "개인 정보" }, "identification": { - "message": "Identification" + "message": "본인 확인" }, "contactInfo": { - "message": "Contact info" + "message": "연락처 정보" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "다운로드 - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4330,23 +4351,23 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "카드 번호는 다음으로 끝납니다", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "로그인 정보" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "인증 키" }, "autofillOptions": { - "message": "Autofill options" + "message": "자동 완성 옵션" }, "websiteUri": { - "message": "Website (URI)" + "message": "웹사이트 (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "웹사이트 (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4356,16 +4377,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "웹사이트 추가됨" }, "addWebsite": { - "message": "Add website" + "message": "웹사이트 추가" }, "deleteWebsite": { - "message": "Delete website" + "message": "웹사이트 삭제" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "기본값 ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4375,7 +4396,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "$WEBSITE$ 일치 인식 보이기", "placeholders": { "website": { "content": "$1", @@ -4384,7 +4405,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "$WEBSITE$ 일치 인식 숨기기", "placeholders": { "website": { "content": "$1", @@ -4393,19 +4414,19 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "페이지 로드 시 자동 완성을 할까요?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "만료된 카드" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "갱신한 경우, 카드 정보를 업데이트합니다" }, "cardDetails": { - "message": "Card details" + "message": "카드 상세정보" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ 상세정보", "placeholders": { "brand": { "content": "$1", @@ -4417,40 +4438,40 @@ "message": "애니메이션 활성화" }, "showAnimations": { - "message": "Show animations" + "message": "애니메이션 표시" }, "addAccount": { - "message": "Add account" + "message": "계정 추가" }, "loading": { - "message": "Loading" + "message": "불러오는 중" }, "data": { - "message": "Data" + "message": "데이터" }, "passkeys": { - "message": "Passkeys", + "message": "패스키", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "비밀번호", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "패스키로 로그인", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "할당" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "이 컬렉션에 액세스할 수 있는 조직 구성원만 해당 항목을 볼 수 있습니다." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "이 컬렉션에 액세스할 수 있는 조직 구성원만 해당 항목들을 볼 수 있습니다." }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "$TOTAL_COUNT$ 항목들을 선택했습니다. 편집 권한이 없기 때문에 항목들의 $READONLY_COUNT$를 업데이트할 수 없습니다.", "placeholders": { "total_count": { "content": "$1", @@ -4462,37 +4483,37 @@ } }, "addField": { - "message": "Add field" + "message": "필드 추가" }, "add": { - "message": "Add" + "message": "추가" }, "fieldType": { - "message": "Field type" + "message": "필드 유형" }, "fieldLabel": { - "message": "Field label" + "message": "필드 레이블" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "보안 질문과 같은 데이터에 텍스트 필드를 사용하세요" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "비밀번호와 같은 중요한 데이터의 경우 숨겨진 필드를 사용하세요." }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "이메일 기억과 같이 양식의 체크박스를 자동으로 채우려면 체크박스들을 사용하세요" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "특정 웹사이트에 대한 자동 채우기 문제가 발생할 때는, 연결 필드를 사용하세요" }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "필드의 html ID, 이름, aria-label 또는 플레이스홀더를 입력하세요" }, "editField": { - "message": "Edit field" + "message": "필드 편집" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "$LABEL$ 편집", "placeholders": { "label": { "content": "$1", @@ -4501,7 +4522,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "$LABEL$ 삭제", "placeholders": { "label": { "content": "$1", @@ -4510,7 +4531,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ 추가됨", "placeholders": { "label": { "content": "$1", @@ -4519,7 +4540,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "$LABEL$을 재정렬합니다. 화살표 키를 사용하여 항목을 위나 아래로 이동할 수 있습니다.", "placeholders": { "label": { "content": "$1", @@ -4528,7 +4549,7 @@ } }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "$LABEL$을 위로 이동했습니다. 위치: $INDEX$ / $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4545,13 +4566,13 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "할당할 컬렉션을 선택하세요" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1개 항목이 선택한 조직으로 영구적으로 전송됩니다. 더 이상 이 항목을 소유하지 않습니다." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 개 항목들이 선택한 조직으로 영구적으로 전송됩니다. 더 이상 이 항목들을 소유하지 않습니다.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4560,7 +4581,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "1개 항목이 $ORG$으로 영구적으로 전송됩니다. 더 이상 이 항목을 소유하지 않습니다.", "placeholders": { "org": { "content": "$1", @@ -4569,7 +4590,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 개 항목들이 $ORG$으로 영구적으로 전송됩니다. 더 이상 이 항목들을 소유하지 않습니다.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4582,13 +4603,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "성공적으로 컬렉션을 할당했습니다" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "아무것도 선택하지 않았습니다." }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "선택한 항목이 $ORGNAME$(으)로 이동됨", "placeholders": { "orgname": { "content": "$1", @@ -4597,7 +4618,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "항목들이 $ORGNAME$로 이동했습니다", "placeholders": { "orgname": { "content": "$1", @@ -4606,7 +4627,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "항목이 $ORGNAME$로 이동했습니다", "placeholders": { "orgname": { "content": "$1", @@ -4615,7 +4636,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "$LABEL$을 아래로 이동했습니다. 위치: $INDEX$ / $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4632,49 +4653,49 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "항목 위치" }, "fileSend": { - "message": "File Send" + "message": "파일 Send" }, "fileSends": { - "message": "File Sends" + "message": "파일 Send" }, "textSend": { - "message": "Text Send" + "message": "텍스트 Send" }, "textSends": { - "message": "Text Sends" + "message": "텍스트 Send" }, "bitwardenNewLook": { - "message": "Bitwarden has a new look!" + "message": "Bitwarden이 새로운 모습으로 돌아왔습니다!" }, "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + "message": "보관함 탭에서 자동 완성하고 검색하는 것이 그 어느 때보다 쉽고 직관적입니다. 둘러보세요!" }, "accountActions": { - "message": "Account actions" + "message": "계정 작업" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "확장 아이콘에 로그인 자동 완성 제안 수 표시" }, "systemDefault": { - "message": "System default" + "message": "시스템 기본 설정" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "기업 정책에 따른 요구사항들이 옵션들에 적용되었습니다." }, "sshPrivateKey": { - "message": "Private key" + "message": "개인 키" }, "sshPublicKey": { - "message": "Public key" + "message": "공개 키" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "지문" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "키 유형" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4689,213 +4710,213 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "재시도" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "최소 사용자 지정 시간 초과는 1분입니다." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "추가 콘텐츠를 사용할 수 있습니다" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "파일을 장치에 저장했습니다. 장치 다운로드로 관리할 수 있습니다." }, "showCharacterCount": { - "message": "Show character count" + "message": "글자 수 표시하기" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "글자 수 숨기기" }, "itemsInTrash": { - "message": "Items in trash" + "message": "휴지통에 있는 항목" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "휴지통에 항목이 없습니다." }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "삭제한 항목은 여기에 표시되며 30일 후 영구적으로 삭제됩니다." }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "30일 이상 휴지통에 보관된 항목은 자동으로 삭제됩니다." }, "restore": { - "message": "Restore" + "message": "복원" }, "deleteForever": { - "message": "Delete forever" + "message": "영구 삭제하기" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "아이템을 수정할 권한이 없습니다." }, "authenticating": { - "message": "Authenticating" + "message": "인증 중" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "생성된 비밀번호를 입력하세요", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "비밀번호가 재생성되었습니다.", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Bitwarden에 로그인을 저장하시겠습니까?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "스페이스", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "물결표(~)", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "백틱(`)", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "느낌표 (!)", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "골뱅이표 (@)", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "해시 기호 (#)", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "달러 기호 ($)", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "퍼센트 기호 (%)", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "캐럿 기호 (^)", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "앰퍼샌드 기호 (&)", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "별표 (*)", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "왼쪽 소괄호 ' ( '", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "오른쪽 소괄호 ' ) '", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "밑줄( _ )", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "붙임표 ( - )", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "더하기 기호 ( + )", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "등호 ( = )", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "왼쪽 중괄호 ' { '", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "오른쪽 중괄호 ' } '", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "왼쪽 대괄호 ' [ '", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "오른쪽 대괄호 ' ] '", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "파이프 기호 ( | )", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "백슬래시 ( \\ )", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "콜론 ( : )", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "세미콜론( ; )", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "쌍 따옴표 ( \" )", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "홑 따옴표 ( ' )", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "보다 작음 ( < )", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "보다 큰 ( > )", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "쉼표( , )", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "마침표 ( . )", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "물음표 ( ? )", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "슬래시 ( / )", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "소문자" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "대문자" }, "generatedPassword": { - "message": "Generated password" + "message": "비밀번호 생성" }, "compactMode": { - "message": "Compact mode" + "message": "컴팩트 모드\n" }, "beta": { - "message": "Beta" + "message": "베타" }, "extensionWidth": { - "message": "Extension width" + "message": "확장 폭" }, "wide": { - "message": "Wide" + "message": "넓게" }, "extraWide": { - "message": "Extra wide" + "message": "매우 넓게" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 8c2c12ec65b..7fd47194b7c 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Tapatybės automatinis užpildymas" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Sukurti slaptažodį (nukopijuotas)" }, @@ -3580,6 +3587,14 @@ "message": "Atrakinti savo paskyrą, atidaromas naujame lange", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Užpildykite prisijungimo duomenis", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index e91d207ff20..3fa9085d005 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Automātiski aizpildīt identitāti" }, + "fillVerificationCode": { + "message": "Aizpildīt apliecinājuma kodu" + }, + "fillVerificationCodeAria": { + "message": "Aizpildīt apliecinājuma kodu", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Izveidot paroli (tiks ievietota starpliktuvē)" }, @@ -3174,7 +3181,7 @@ "message": "Skatīt visas pieteikšanās iespējas" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Skatīt visas pieteikšanās iespējas" }, "notificationSentDevice": { "message": "Uz ierīci ir nosūtīts paziņojums." @@ -3580,6 +3587,14 @@ "message": "Atslēgt savu kontu, tiks atvērts jaunā logā", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Laikā balstīts vienreizējas izmantošanas paroles apliecināšanas kods", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Atlikušais laiks, pirms beigsies pašreizējā TOTP derīgums", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Aizpildīt pieteikšanās datus", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index e031dfcbcbd..80e8cd90052 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "പാസ്‌വേഡ് സൃഷ്ടിക്കുക (പകർത്തുക )" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index dfb25015dc3..be0b2627b8a 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 6aa17c1d7e3..779ff917578 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 0a14e176891..ef36839dd53 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Auto-utfyll identitet" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generer et passord (kopiert)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 6aa17c1d7e3..779ff917578 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 28cbea13382..23fbcd3d265 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Identiteit automatisch invullen" }, + "fillVerificationCode": { + "message": "Verificatiecode invullen" + }, + "fillVerificationCodeAria": { + "message": "Verificatiecode invullen", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Wachtwoord genereren (op klembord)" }, @@ -3580,6 +3587,14 @@ "message": "Je account ontgrendelen, opent in een nieuw venster", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-gebaseerde eenmalige wachtwoord verificatiecode", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Resterende tijd voordat de huidige TOTP vervalt", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Inloggegevens invullen voor", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 6aa17c1d7e3..779ff917578 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 6aa17c1d7e3..779ff917578 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index a49b5c52e3b..0db4fc4dd8b 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -177,7 +177,7 @@ "message": "Kopiuj notatki" }, "fill": { - "message": "Fill", + "message": "Wypełnij", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autouzupełnianie tożsamości" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Wygeneruj hasło (do schowka)" }, @@ -999,7 +1006,7 @@ "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, "showIdentitiesInVaultView": { - "message": "Pokaż tożsamośći jako sugestie autouzupełniania w widoku sejfu" + "message": "Pokaż tożsamości jako sugestie autouzupełniania w widoku sejfu" }, "showIdentitiesCurrentTab": { "message": "Pokaż tożsamości na stronie głównej" @@ -1771,7 +1778,7 @@ "message": "Tożsamość" }, "typeSshKey": { - "message": "SSH key" + "message": "Klucz SSH" }, "newItemHeader": { "message": "Nowy $TYPE$", @@ -1804,13 +1811,13 @@ "message": "Historia hasła" }, "generatorHistory": { - "message": "Generator history" + "message": "Historia generatora" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Wyczyść historię generatora" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Jeśli zatwierdzisz, wszystkie wygenerowane hasła zostaną usunięte z historii generatora. Czy chcesz kontynuować mimo to?" }, "back": { "message": "Powrót" @@ -1849,7 +1856,7 @@ "message": "Bezpieczne notatki" }, "sshKeys": { - "message": "SSH Keys" + "message": "Klucze SSH" }, "clear": { "message": "Wyczyść", @@ -1932,10 +1939,10 @@ "message": "Wyczyść historię" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Brak zawartości do pokazania" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Nic nie zostało wygenerowane przez ciebie w ostatnim czasie" }, "remove": { "message": "Usuń" @@ -2890,7 +2897,7 @@ "message": "Generate email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Wartość musi być pomiędzy $MIN$ a $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2904,7 +2911,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Użyj $RECOMMENDED$ znaków lub więcej, aby wygenerować silne hasło.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2914,7 +2921,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Użyj $RECOMMENDED$ słów lub więcej, aby wygenerować silne hasło.", "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": { @@ -3580,6 +3587,14 @@ "message": "Odblokuj swoje konto, otwiera się w nowym oknie", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Wypełnij dane logowania dla", "description": "Screen reader text for when overlay item is in focused" @@ -4671,28 +4686,28 @@ "message": "Do tego ustalenia zastosowano wymogi polityki przedsiębiorstw" }, "sshPrivateKey": { - "message": "Private key" + "message": "Klucz prywatny" }, "sshPublicKey": { - "message": "Public key" + "message": "Klucz publiczny" }, "sshFingerprint": { "message": "Fingerprint" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Typ klucza" }, "sshKeyAlgorithmED25519": { "message": "ED25519" }, "sshKeyAlgorithmRSA2048": { - "message": "RSA 2048-Bit" + "message": "RSA 2048-Bitowy" }, "sshKeyAlgorithmRSA3072": { - "message": "RSA 3072-Bit" + "message": "RSA 3072-Bitowy" }, "sshKeyAlgorithmRSA4096": { - "message": "RSA 4096-Bit" + "message": "RSA 4096-Bitowy" }, "retry": { "message": "Powtórz" @@ -4737,7 +4752,7 @@ "message": "Uwierzytelnianie" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "Uzupełnij wygenerowanym hasłem", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { @@ -4890,18 +4905,18 @@ "message": "Generated password" }, "compactMode": { - "message": "Compact mode" + "message": "Tryb kompaktowy" }, "beta": { "message": "Beta" }, "extensionWidth": { - "message": "Extension width" + "message": "Szerokość rozszerzenia" }, "wide": { - "message": "Wide" + "message": "Szerokie" }, "extraWide": { - "message": "Extra wide" + "message": "Bardzo szerokie" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 0a34f5b9dd7..b346b8927e3 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Preenchimento automático identidade" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Gerar Senha (copiada)" }, @@ -3580,6 +3587,14 @@ "message": "Desbloqueie sua conta, abra em uma nova janela", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Preencha as credenciais para", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 6c85d72e6bf..9b657e66b65 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Preencher automaticamente identidade" }, + "fillVerificationCode": { + "message": "Preencher código de verificação" + }, + "fillVerificationCodeAria": { + "message": "Preencher código de verificação", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Gerar palavra-passe (copiada)" }, @@ -3174,7 +3181,7 @@ "message": "Ver todas as opções de início de sessão" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Ver todas as opções de início de sessão" }, "notificationSentDevice": { "message": "Foi enviada uma notificação para o seu dispositivo." @@ -3580,6 +3587,14 @@ "message": "Desbloqueie a sua conta, abre numa nova janela", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Código de verificação de palavra-passe única com base no tempo", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Tempo restante antes da TOTP atual expirar", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Preencher as credenciais para", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 680d22feeb6..a3e3ca308ed 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autocompletare identitate" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generare parolă (s-a copiat)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 526d77e009d..145cd5d2d7f 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Автозаполнение личности" }, + "fillVerificationCode": { + "message": "Заполнить код подтверждения" + }, + "fillVerificationCodeAria": { + "message": "Заполнить код подтверждения", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Сгенерировать пароль (с копированием)" }, @@ -3174,7 +3181,7 @@ "message": "Посмотреть все варианты авторизации" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Посмотреть все варианты авторизации" }, "notificationSentDevice": { "message": "На ваше устройство отправлено уведомление." @@ -3580,6 +3587,14 @@ "message": "Разблокируйте ваш аккаунт, откроется в новом окне", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Код подтверждения, основанный на времени", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Время, оставшееся до истечения срока действия текущего TOTP", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Заполнить учетные данные", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 9ada4ff9281..81ece16334b 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "මුරපදය ජනනය (පිටපත්)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index d1374c51cfe..4bb0a99cca0 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Automatické vyplnenie identity" }, + "fillVerificationCode": { + "message": "Vyplniť overovací kód" + }, + "fillVerificationCodeAria": { + "message": "Vyplniť overovací kód", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Vygenerovať heslo (skopírované)" }, @@ -3174,7 +3181,7 @@ "message": "Zobraziť všetky možnosti prihlásenia" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Zobraziť všetky možnosti prihlásenia" }, "notificationSentDevice": { "message": "Do vášho zariadenia bolo odoslané upozornenie." @@ -3580,6 +3587,14 @@ "message": "Odomknúť konto v novom okne", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Overovací kód TOTP", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Čas zostávajúci do vypršania aktuálneho TOTP", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Vyplňte prihlasovacie údaje pre", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index d155e1b1a76..597155b775e 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Samodejno izpolni identiteto" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generiraj geslo (kopirano)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index f17bc4c97eb..df83b41a625 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Ауто-пуњење идентитета" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Генериши Лозинку (копирано)" }, @@ -3580,6 +3587,14 @@ "message": "Откључајте свој налог, отвара се у новом прозору", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Попунити акредитиве за", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index dc8c1bed901..efdf8c10018 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofyll identitet" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Skapa lösenord (kopierad)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fyll i uppgifter för", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 6aa17c1d7e3..779ff917578 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 517ab17a489..b660dc785ba 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Autofill identity" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Generate Password (copied)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 496ce4eac76..8c8ffb0ffa7 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Kimliği otomatik doldur" }, + "fillVerificationCode": { + "message": "Doğrulama kodunu doldur" + }, + "fillVerificationCodeAria": { + "message": "Doğrulama kodunu doldur", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Parola oluştur (ve kopyala)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Zamana dayalı tek seferlik parola doğrulama kodu", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Geçerli TOTP için kalan süre", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Bilgileri doldur", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index d9a1c11bed5..dc569fe0818 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Автозаповнення посвідчень" }, + "fillVerificationCode": { + "message": "Заповнити код підтвердження" + }, + "fillVerificationCodeAria": { + "message": "Заповнити код підтвердження", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Генерувати пароль (з копіюванням)" }, @@ -3174,7 +3181,7 @@ "message": "Переглянути всі варіанти входу" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Переглянути всі варіанти входу" }, "notificationSentDevice": { "message": "Сповіщення було надіслано на ваш пристрій." @@ -3580,6 +3587,14 @@ "message": "Розблокування облікового запису – відкриється нове вікно", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Код підтвердження одноразового пароля", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Час, що залишився до завершення чинного TOTP", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Заповнити облікові дані для", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 317a2599be1..591cb013968 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "Tự động điền danh tính" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "Tạo mật khẩu (đã sao chép)" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "Điền thông tin đăng nhập cho", "description": "Screen reader text for when overlay item is in focused" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fb7f7dd7bdc..bdc4902b27d 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "自动填充身份" }, + "fillVerificationCode": { + "message": "填写验证码" + }, + "fillVerificationCodeAria": { + "message": "填写验证码", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "生成密码(并复制)" }, @@ -345,7 +352,7 @@ "message": "免费 Bitwarden 家庭" }, "freeBitwardenFamiliesPageDesc": { - "message": "您有资格获得免费的 Bitwarden 家庭。立即在网页应用中兑换此优惠。" + "message": "您有资格获得免费的 Bitwarden 家庭。立即在网页 App 中兑换此优惠。" }, "version": { "message": "版本" @@ -3174,7 +3181,7 @@ "message": "查看所有登录选项" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "查看所有登录选项" }, "notificationSentDevice": { "message": "通知已发送到您的设备。" @@ -3580,6 +3587,14 @@ "message": "解锁您的账户(在新窗口中打开)", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "基于时间的一次性密码验证码", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "TOTP 到期前剩余时间", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "为其填写凭据", "description": "Screen reader text for when overlay item is in focused" @@ -3711,7 +3726,7 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "与 Duo 服务连接时出错。请使用不同的两步登录方式或联系 Duo 寻求帮助。" + "message": "与 Duo 服务连接时出错。请使用不同的两步登录方式或联系 Duo 获取协助。" }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "启动 DUO 并按照步骤完成登录。" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 9d51dfedb53..cd0c1888034 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -192,6 +192,13 @@ "autoFillIdentity": { "message": "自動填入身分資訊" }, + "fillVerificationCode": { + "message": "Fill verification code" + }, + "fillVerificationCodeAria": { + "message": "Fill Verification Code", + "description": "Aria label for the heading displayed the inline menu for totp code autofill" + }, "generatePasswordCopied": { "message": "產生及複製密碼" }, @@ -3580,6 +3587,14 @@ "message": "Unlock your account, opens in a new window", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, + "totpCodeAria": { + "message": "Time-based One-Time Password Verification Code", + "description": "Aria label for the totp code displayed in the inline menu for autofill" + }, + "totpSecondsSpanAria": { + "message": "Time remaining before current TOTP expires", + "description": "Aria label for the totp seconds displayed in the inline menu for autofill" + }, "fillCredentialsFor": { "message": "填入登入資訊給", "description": "Screen reader text for when overlay item is in focused" From 6cea32839b07c2da2ff50985ce91ca474df97ac5 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:01:24 +0100 Subject: [PATCH 03/57] Autosync the updated translations (#12266) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/desktop/src/locales/af/messages.json | 9 ++- apps/desktop/src/locales/ar/messages.json | 9 ++- apps/desktop/src/locales/az/messages.json | 27 +++---- apps/desktop/src/locales/be/messages.json | 9 ++- apps/desktop/src/locales/bg/messages.json | 11 +-- apps/desktop/src/locales/bn/messages.json | 9 ++- apps/desktop/src/locales/bs/messages.json | 9 ++- apps/desktop/src/locales/ca/messages.json | 9 ++- apps/desktop/src/locales/cs/messages.json | 9 ++- apps/desktop/src/locales/cy/messages.json | 9 ++- apps/desktop/src/locales/da/messages.json | 9 ++- apps/desktop/src/locales/de/messages.json | 9 ++- apps/desktop/src/locales/el/messages.json | 9 ++- apps/desktop/src/locales/en_GB/messages.json | 9 ++- apps/desktop/src/locales/en_IN/messages.json | 9 ++- apps/desktop/src/locales/eo/messages.json | 9 ++- apps/desktop/src/locales/es/messages.json | 37 +++++----- apps/desktop/src/locales/et/messages.json | 9 ++- apps/desktop/src/locales/eu/messages.json | 9 ++- apps/desktop/src/locales/fa/messages.json | 9 ++- apps/desktop/src/locales/fi/messages.json | 55 +++++++------- apps/desktop/src/locales/fil/messages.json | 9 ++- apps/desktop/src/locales/fr/messages.json | 57 ++++++++------- apps/desktop/src/locales/gl/messages.json | 9 ++- apps/desktop/src/locales/he/messages.json | 9 ++- apps/desktop/src/locales/hi/messages.json | 9 ++- apps/desktop/src/locales/hr/messages.json | 9 ++- apps/desktop/src/locales/hu/messages.json | 11 +-- apps/desktop/src/locales/id/messages.json | 9 ++- apps/desktop/src/locales/it/messages.json | 9 ++- apps/desktop/src/locales/ja/messages.json | 9 ++- apps/desktop/src/locales/ka/messages.json | 9 ++- apps/desktop/src/locales/km/messages.json | 9 ++- apps/desktop/src/locales/kn/messages.json | 9 ++- apps/desktop/src/locales/ko/messages.json | 9 ++- apps/desktop/src/locales/lt/messages.json | 9 ++- apps/desktop/src/locales/lv/messages.json | 15 ++-- apps/desktop/src/locales/me/messages.json | 9 ++- apps/desktop/src/locales/ml/messages.json | 9 ++- apps/desktop/src/locales/mr/messages.json | 9 ++- apps/desktop/src/locales/my/messages.json | 9 ++- apps/desktop/src/locales/nb/messages.json | 9 ++- apps/desktop/src/locales/ne/messages.json | 9 ++- apps/desktop/src/locales/nl/messages.json | 11 +-- apps/desktop/src/locales/nn/messages.json | 9 ++- apps/desktop/src/locales/or/messages.json | 9 ++- apps/desktop/src/locales/pl/messages.json | 9 ++- apps/desktop/src/locales/pt_BR/messages.json | 11 +-- apps/desktop/src/locales/pt_PT/messages.json | 9 ++- apps/desktop/src/locales/ro/messages.json | 9 ++- apps/desktop/src/locales/ru/messages.json | 9 ++- apps/desktop/src/locales/si/messages.json | 9 ++- apps/desktop/src/locales/sk/messages.json | 9 ++- apps/desktop/src/locales/sl/messages.json | 9 ++- apps/desktop/src/locales/sr/messages.json | 75 ++++++++++---------- apps/desktop/src/locales/sv/messages.json | 9 ++- apps/desktop/src/locales/te/messages.json | 9 ++- apps/desktop/src/locales/th/messages.json | 9 ++- apps/desktop/src/locales/tr/messages.json | 33 +++++---- apps/desktop/src/locales/uk/messages.json | 57 ++++++++------- apps/desktop/src/locales/vi/messages.json | 9 ++- apps/desktop/src/locales/zh_CN/messages.json | 15 ++-- apps/desktop/src/locales/zh_TW/messages.json | 55 +++++++------- 63 files changed, 550 insertions(+), 361 deletions(-) diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 2bf32319e28..072b13787cb 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Bedienerbronadres" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bevestig vir Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Ontgrendel met Touch ID" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 56083c1f149..de7d91341d4 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "رابط الخادم" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "رابط خادم الاستضافة الذاتية", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "تحقق من Bitwarden." }, - "polkitConsentMessage": { - "message": "مصادقة لفتح Bitwarden." - }, "unlockWithTouchId": { "message": "فتح بواسطة معرف اللمس" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index e49c4be0418..91705bf7a84 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -217,10 +217,10 @@ "message": "Parolu təsdiqlə" }, "enterSshKeyPasswordDesc": { - "message": "SSH açarının parolunu daxil edin." + "message": "SSH açarı üçün parolu daxil edin." }, "enterSshKeyPassword": { - "message": "Parolu daxil et" + "message": "Parolu daxil edin" }, "sshAgentUnlockRequired": { "message": "SSH açar tələbini təsdiqləmək üçün seyfinizin kilidini açın." @@ -645,7 +645,7 @@ "message": "Cihazla giriş et" }, "useSingleSignOn": { - "message": "Tək daxil olma üsulunu istifadə et" + "message": "Vahid daxil olma üsulunu istifadə et" }, "submit": { "message": "Göndər" @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL-si" }, + "authenticationTimeout": { + "message": "Kimlik doğrulama vaxtı bitdi" + }, + "authenticationSessionTimedOut": { + "message": "Kimlik doğrulama seansının vaxtı bitdi. Lütfən giriş prosesini yenidən başladın." + }, "selfHostBaseUrl": { "message": "Self-host server URL-si", "description": "Label for field requesting a self-hosted integration service URL" @@ -1393,7 +1399,7 @@ "message": "Yaradıcı tarixçəsini təmizlə" }, "cleargGeneratorHistoryDescription": { - "message": "Davam etsəniz, yaradıcı tarixçəsindəki bütün girişlər həmişəlik silinəcək. Davam etmək istədiyinizə əminsiniz?" + "message": "Davam etsəniz, bütün girişlər yaradıcı tarixçəsindən həmişəlik silinəcək. Davam etmək istəyirsiniz?" }, "clear": { "message": "Təmizlə", @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bitwarden üçün doğrula." }, - "polkitConsentMessage": { - "message": "Bitwarden kilidini açmaq üçün kimliyi doğrula." - }, "unlockWithTouchId": { "message": "Touch ID kilidini aç" }, @@ -1870,7 +1873,7 @@ "message": "Vaxt bitmə əməliyyat təsdiqi" }, "enterpriseSingleSignOn": { - "message": "Müəssisə üçün tək daxil olma" + "message": "Müəssisə üçün vahid daxil olma" }, "setMasterPassword": { "message": "Ana parolu ayarla" @@ -2726,7 +2729,7 @@ "message": "Cihazınıza bir bildiriş göndərildi" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Hesabınızın kilidinin açıq olduğuna və barmaq izi ifadəsinin digər cihazda uyuşduğuna əmin olun" + "message": "Lütfən hesabınızın kilidinin açıq olduğuna və barmaq izi ifadəsinin digər cihazla uyuşduğuna əmin olun" }, "needAnotherOptionV1": { "message": "Başqa bir seçimə ehtiyacınız var?" @@ -2738,7 +2741,7 @@ "message": "Barmaq izi ifadəsi" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "Tələbiniz təsdiqləndikdə bildiriş alacaqsınız" + "message": "Tələbiniz təsdiqləndikdə məlumatlandırılacaqsınız" }, "needAnotherOption": { "message": "Cihazla giriş etmə, Bitwarden tətbiqinin ayarlarında qurulmalıdır. Başqa bir seçimə ehtiyacınız var?" @@ -2899,13 +2902,13 @@ "message": "Tövsiyə edilən Ayarlar Güncəlləməsi" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Gələcək girişləri problemsiz etmək üçün bu cihazı xatırla" + "message": "Gələcəkdəki girişləri problemsiz etmək üçün bu cihazı xatırla" }, "deviceApprovalRequired": { "message": "Cihaz təsdiqi tələb olunur. Aşağıdan bir təsdiq variantı seçin:" }, "deviceApprovalRequiredV2": { - "message": "Cihaz təsdiqi tələb olunur" + "message": "Cihazın təsdiq olunması tələb olunur" }, "selectAnApprovalOptionBelow": { "message": "Aşağıdan bir təsdiq seçimi edin" diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 7a7aa20d7c7..a37dc6ac5bf 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL-адрас сервера" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Праверыць на Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Разблакіраваць з Touch ID" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 264233bbe1c..0b8e237cfd5 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -220,7 +220,7 @@ "message": "Въведете паролата за SSH-ключа." }, "enterSshKeyPassword": { - "message": "Въведете парола" + "message": "Въведете паролата" }, "sshAgentUnlockRequired": { "message": "Отключете трезора си, за да одобрите заявката за SSH ключ." @@ -919,6 +919,12 @@ "baseUrl": { "message": "Адрес на сървъра" }, + "authenticationTimeout": { + "message": "Време на давност за удостоверяването" + }, + "authenticationSessionTimedOut": { + "message": "Сесията за удостоверяване е изтекла. Моля, започнете отначало процеса по вписване." + }, "selfHostBaseUrl": { "message": "Адрес на собствения сървър", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Потвърждаване за Битуорден." }, - "polkitConsentMessage": { - "message": "Идентифицирайте се, за да отключите Битуорден." - }, "unlockWithTouchId": { "message": "Отключване с Touch ID" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 466dcde91d6..50be17bf6b4 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "সার্ভার URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index c1dd204d814..2829a2465c4 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL servera" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Potvrdi za Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Otključaj koristeći Touch ID" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 4ccfdc2f5ac..a0b7703748e 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL del servidor" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verifica per Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Desbloqueja amb Touch ID" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 0e45ca58a41..0c932907bd8 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL serveru" }, + "authenticationTimeout": { + "message": "Časový limit ověření" + }, + "authenticationSessionTimedOut": { + "message": "Vypršel časový limit relace ověřování. Restartujte proces přihlášení." + }, "selfHostBaseUrl": { "message": "Adresa URL serveru vlastního hostování", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Ověřte se pro Bitwarden." }, - "polkitConsentMessage": { - "message": "Ověřte se pro odemknutí Bitwardenu." - }, "unlockWithTouchId": { "message": "Odemknout pomocí Touch ID" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index a77e3b61ae0..d423151ad40 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index d00ca886f8a..13795673198 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server-URL" }, + "authenticationTimeout": { + "message": "Godkendelsestimeout" + }, + "authenticationSessionTimedOut": { + "message": "Godkendelsessessionen fik timeout. Genstart loginprocessen." + }, "selfHostBaseUrl": { "message": "URL til selv-hostet server", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bekræft for Bitwarden." }, - "polkitConsentMessage": { - "message": "Godkend for at oplåse Bitwarden." - }, "unlockWithTouchId": { "message": "Oplås med Touch ID" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 7b7707d33fd..ae8a024fe38 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server-URL" }, + "authenticationTimeout": { + "message": "Authentifizierungs-Timeout" + }, + "authenticationSessionTimedOut": { + "message": "Die Authentifizierungssitzung ist abgelaufen. Bitte starte den Anmeldeprozess neu." + }, "selfHostBaseUrl": { "message": "Selbst gehostete Server-URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Für Bitwarden verifizieren." }, - "polkitConsentMessage": { - "message": "Authentifizieren, um Bitwarden zu entsperren." - }, "unlockWithTouchId": { "message": "Mit Touch ID entsperren" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index df5f968ef59..ff4a4a0f893 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL Διακομιστή" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "URL διακομιστή αυτο-φιλοξενίας", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Επαληθεύστε για το Bitwarden." }, - "polkitConsentMessage": { - "message": "Αυθεντικοποίηση για ξεκλείδωμα του Bitwarden." - }, "unlockWithTouchId": { "message": "Ξεκλείδωμα με Touch ID" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 316ea4bf085..c13ceb9b3d7 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 1804169218c..f7b6552a90a 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 4499b27b845..c30f33aa612 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Malŝlosi per Touch ID" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 3db7acb80fa..365c3835137 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -64,7 +64,7 @@ } }, "welcomeBack": { - "message": "Welcome back" + "message": "Bienvenido de nuevo" }, "moveToOrgDesc": { "message": "Elige una organización a la que deseas mover este objeto. Moviendo a una organización transfiere la propiedad del objeto a esa organización. Ya no serás el dueño directo de este objeto una vez que haya sido movido." @@ -181,16 +181,16 @@ "message": "Dirección" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clave privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clave pública" }, "sshFingerprint": { "message": "Fingerprint" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipo de clave" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -208,19 +208,19 @@ "message": "A new SSH key was generated" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "La contraseña introducida es incorrecta." }, "importSshKey": { "message": "Import" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "Confirmar contraseña" }, "enterSshKeyPasswordDesc": { "message": "Enter the password for the SSH key." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "Introducir la contraseña" }, "sshAgentUnlockRequired": { "message": "Please unlock your vault to approve the SSH key request." @@ -229,7 +229,7 @@ "message": "SSH key request timed out." }, "enableSshAgent": { - "message": "Enable SSH agent" + "message": "Habilitar agente SSH" }, "enableSshAgentDesc": { "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." @@ -498,7 +498,7 @@ "message": "Carácteres especiales (!@#$%^&*)" }, "include": { - "message": "Include", + "message": "Incluir", "description": "Card header for password generator include block" }, "uppercaseDescription": { @@ -518,7 +518,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Incluir números", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -624,7 +624,7 @@ "message": "Crear cuenta" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "¿Nuevo en Bitwarden?" }, "setAStrongPassword": { "message": "Establece una contraseña fuerte" @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL del servidor" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1070,13 +1076,13 @@ "message": "Your account is locked" }, "or": { - "message": "or" + "message": "o" }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "Desbloquear con biométricos" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Desbloquear con contraseña maestra" }, "unlock": { "message": "Desbloquear" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verificar para Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Desbloquear con Touch ID" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index bf3f9861f99..15161f2cd7b 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Serveri URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Kinnita Bitwardenisse sisselogimine." }, - "polkitConsentMessage": { - "message": "Autentiteeri ennast Bitwardeni avamiseks." - }, "unlockWithTouchId": { "message": "Lukusta lahti Touch ID-ga" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 0bc53106ca5..6283845a905 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Zerbitzariaren URL-a" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Egiaztatu Bitwarden-entzako." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Desblokeatu Touch ID-arekin" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 9ba711ef7fc..c90e92f1389 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "نشانی اینترنتی سرور" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "تأیید برای Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "باز کردن با Touch ID" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index 1fb69e5a3ff..7b661943210 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -208,19 +208,19 @@ "message": "Uusi SSH-avain luotiin" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "Syöttämäsi salasana on virheellinen." }, "importSshKey": { - "message": "Import" + "message": "Tuo" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "Vahvista salasana" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "Syötä SSH-avaimen salasana." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "Syötä salasana" }, "sshAgentUnlockRequired": { "message": "Ole hyvä ja avaa holvisi hyväksyäksesi SSH-avainpyynnön." @@ -919,6 +919,12 @@ "baseUrl": { "message": "Palvelimen URL" }, + "authenticationTimeout": { + "message": "Todennuksen aikakatkaisu" + }, + "authenticationSessionTimedOut": { + "message": "Todennusistunto aikakatkaistiin. Ole hyvä ja aloita kirjautumisprosessi uudelleen." + }, "selfHostBaseUrl": { "message": "Itse ylläpidetyn palvelimen URL-osoite", "description": "Label for field requesting a self-hosted integration service URL" @@ -1387,13 +1393,13 @@ "message": "Salasanahistoria" }, "generatorHistory": { - "message": "Generator history" + "message": "Generaattorihistoria" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Tyhjennä generaattorihistoria" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Jos jatkat, kaikki generaattorihistorian kohteet poistetaan. Haluatko varmasti jatkaa?" }, "clear": { "message": "Tyhjennä", @@ -1403,13 +1409,13 @@ "message": "Ei näytettäviä salasanoja." }, "clearHistory": { - "message": "Clear history" + "message": "Tyhjennä historia" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Ei näytettävää" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Et ole luonut mitään hiljattain" }, "undo": { "message": "Kumoa" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Vahvista Bitwarden." }, - "polkitConsentMessage": { - "message": "Avaa Bitwardenin lukitus tunnistautumalla." - }, "unlockWithTouchId": { "message": "Avaa Touch ID:llä" }, @@ -2478,7 +2481,7 @@ "message": "Luo sähköpostiosoite" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Arvon tulee olla väliltä $MIN$—$MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2492,7 +2495,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Käytä $RECOMMENDED$ tai useampaa merkkiä vahvan salasanan luomiseksi.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2502,7 +2505,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Käytä $RECOMMENDED$ tai useampaa sanaa vahvan salalauseen luomiseksi.", "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": { @@ -2723,13 +2726,13 @@ "message": "Laitteellesi on lähetetty ilmoitus." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Laitteeseesi lähetettiin ilmoitus" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Tarvitsetko toisen vaihtoehdon?" }, "fingerprintMatchInfo": { "message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen." @@ -2738,13 +2741,13 @@ "message": "Tunnistelauseke" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Ilmoitamme sinulle, kun pyyntösi on hyväksytty" }, "needAnotherOption": { "message": "Laitteella kirjautuminen on määritettävä Bitwarden-sovelluksen asetuksista. Tarvitsetko eri vaihtoehdon?" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Näytä kaikki kirjautumisvaihtoehdot" }, "viewAllLoginOptions": { "message": "Näytä kaikki kirjautumisvaihtoehdot" @@ -2899,16 +2902,16 @@ "message": "Suositeltava asetusmuutos" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Muista tämä laite tehdäksesi tulevista kirjautumisista helpompaa" }, "deviceApprovalRequired": { "message": "Laitehyväksyntä vaaditaan. Valitse hyväksyntätapa alta:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Laitteen hyväksyntä vaaditaan" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Valitse hyväksyntävaihtoehto alta" }, "rememberThisDevice": { "message": "Muista tämä laite" @@ -2963,7 +2966,7 @@ "message": "Käyttäjän sähköpostiosoite puuttuu" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Aktiivista käyttäjän sähköpostiosoitetta ei löytynyt. Kirjaudutaan ulos." }, "deviceTrusted": { "message": "Laitteeseen luotettu" @@ -3363,7 +3366,7 @@ "message": "Valtuuta" }, "deny": { - "message": "Deny" + "message": "Estä" }, "sshkeyApprovalTitle": { "message": "Vahvista SSH-avainkäyttö" diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 281f58182fa..566c3de07f7 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL ng Server" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify para sa Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "I-unlock gamit ang Touch ID" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index df5527fd9a1..b7e3c6f8cfa 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -208,19 +208,19 @@ "message": "Une nouvelle clé SSH a été générée" }, "sshKeyWrongPassword": { - "message": "Le mot de passe saisi est incorrect." + "message": "The password you entered is incorrect." }, "importSshKey": { - "message": "Importer" + "message": "Import" }, "confirmSshKeyPassword": { - "message": "Confirmez le mot de passe" + "message": "Confirm password" }, "enterSshKeyPasswordDesc": { - "message": "Entrez le mot de passe de la clé SSH." + "message": "Enter the password for the SSH key." }, "enterSshKeyPassword": { - "message": "Entrez le mot de passe" + "message": "Enter password" }, "sshAgentUnlockRequired": { "message": "Veuillez déverrouiller votre coffre pour approuver la demande de clé SSH." @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL du serveur" }, + "authenticationTimeout": { + "message": "Délai d'authentification dépassé" + }, + "authenticationSessionTimedOut": { + "message": "La session d'authentification a expiré. Veuillez redémarrer le processus de connexion." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1387,13 +1393,13 @@ "message": "Historique du mot de passe" }, "generatorHistory": { - "message": "Historique du générateur" + "message": "Generator history" }, "clearGeneratorHistoryTitle": { - "message": "Effacer l'historique du générateur" + "message": "Clear generator history" }, "cleargGeneratorHistoryDescription": { - "message": "Si vous continuez, toutes les entrées seront définitivement supprimées de l'historique du générateur. Êtes-vous sûr de vouloir continuer ?" + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" }, "clear": { "message": "Effacer", @@ -1403,13 +1409,13 @@ "message": "Aucun mot de passe à afficher." }, "clearHistory": { - "message": "Effacer l'historique" + "message": "Clear history" }, "nothingToShow": { - "message": "Rien à afficher" + "message": "Nothing to show" }, "nothingGeneratedRecently": { - "message": "Vous n'avez rien généré récemment" + "message": "You haven't generated anything recently" }, "undo": { "message": "Annuler" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Vérifier pour Bitwarden." }, - "polkitConsentMessage": { - "message": "S'authentifier pour déverrouiller Bitwarden." - }, "unlockWithTouchId": { "message": "Déverrouiller avec Touch ID" }, @@ -2478,7 +2481,7 @@ "message": "Generate email" }, "spinboxBoundariesHint": { - "message": "La valeur doit être comprise entre $MIN$ et $MAX$.", + "message": "Value must be between $MIN$ and $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2492,7 +2495,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Utilisez $RECOMMENDED$ caractères ou plus pour générer un mot de passe fort.", + "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2502,7 +2505,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Utilisez $RECOMMENDED$ mots ou plus pour générer une phrase de passe forte.", + "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", "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": { @@ -2723,13 +2726,13 @@ "message": "Une notification a été envoyée à votre appareil." }, "aNotificationWasSentToYourDevice": { - "message": "Une notification a été envoyée à votre appareil" + "message": "A notification was sent to your device" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Assurez-vous que votre compte est déverrouillé et que la phrase d'empreinte digitale correspond à celle de l'autre appareil" + "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" }, "needAnotherOptionV1": { - "message": "Besoin d'une autre option ?" + "message": "Need another option?" }, "fingerprintMatchInfo": { "message": "Veuillez vous assurer que votre coffre est déverrouillé et que la phrase d'empreinte correspond à celle de l'autre appareil." @@ -2738,13 +2741,13 @@ "message": "Phrase d'empreinte" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "Vous serez notifié une fois que la demande sera approuvée" + "message": "You will be notified once the request is approved" }, "needAnotherOption": { "message": "La connexion avec l'appareil doit être configurée dans les paramètres de l'application Bitwarden. Besoin d'une autre option ?" }, "viewAllLogInOptions": { - "message": "Afficher toutes les options de connexion" + "message": "View all log in options" }, "viewAllLoginOptions": { "message": "Afficher toutes les options de connexion" @@ -2866,7 +2869,7 @@ "message": "Vérifier les brèches de données connues pour ce mot de passe" }, "loggedInExclamation": { - "message": "Connecté !" + "message": "Logged in!" }, "important": { "message": "Important :" @@ -2899,16 +2902,16 @@ "message": "Une mise à jour des paramètres est recommandée" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Mémorisez cet appareil pour faciliter les futures connexions" + "message": "Remember this device to make future logins seamless" }, "deviceApprovalRequired": { "message": "L'approbation de l'appareil est requise. Sélectionnez une option d'approbation ci-dessous:" }, "deviceApprovalRequiredV2": { - "message": "Autorisation de l'appareil requise" + "message": "Device approval required" }, "selectAnApprovalOptionBelow": { - "message": "Sélectionnez une option d'approbation ci-dessous" + "message": "Select an approval option below" }, "rememberThisDevice": { "message": "Se souvenir de cet appareil" @@ -2963,7 +2966,7 @@ "message": "E-mail de l'utilisateur manquant" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Courriel utilisateur actif introuvable. Déconnexion en cours." + "message": "Active user email not found. Logging you out." }, "deviceTrusted": { "message": "Appareil de confiance" diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index e9bebb8bfc0..f8f81a5ac2c 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index be64236849c..8e8162732da 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "כתובת שרת" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "אימות עבור Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "שחרור נעילה עם Touch ID" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 249bc4e2852..1b489fc49da 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 6589e655670..61e37859dd8 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL poslužitelja" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Otključaj trezor." }, - "polkitConsentMessage": { - "message": "Otključaj Bitwarden autentifikacijom." - }, "unlockWithTouchId": { "message": "Otključaj koristeći Touch ID" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 97c3e1eee84..ffc6a439ff3 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -217,7 +217,7 @@ "message": "Jelszó megerősítése" }, "enterSshKeyPasswordDesc": { - "message": "Írjuk be az SSH kulcshoz tartozó jelszót." + "message": "Adjuk meg az SSH kulcs jelszót." }, "enterSshKeyPassword": { "message": "Jelszó megadása" @@ -919,6 +919,12 @@ "baseUrl": { "message": "Szerver webcím" }, + "authenticationTimeout": { + "message": "Hitelesítési időkifutás" + }, + "authenticationSessionTimedOut": { + "message": "A hitelesítési munkamenet időkifutással leállt. Indítsuk újra a bejelentkezési folyamatot." + }, "selfHostBaseUrl": { "message": "Saját üzemeltetésű szerver webcím", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bitwarden ellenőrzés." }, - "polkitConsentMessage": { - "message": "Hitelesítés a Bitwarden feloldásához." - }, "unlockWithTouchId": { "message": "Feloldás Touch ID segítségével" }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index b74e7b5f378..a47f1acb9eb 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL Server" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verifikasi untuk Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Buka kunci dengan Touch ID" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 7704f760c4f..ce4b41a1762 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL del server" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "URL server autogestito", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verifica per Bitwarden." }, - "polkitConsentMessage": { - "message": "Autenticazione per sbloccare Bitwarden." - }, "unlockWithTouchId": { "message": "Sblocca con Touch ID" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index b7ed266780b..4ff2e16300f 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "サーバー URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bitwarden の認証を行います。" }, - "polkitConsentMessage": { - "message": "認証して Bitwarden のロックを解除します。" - }, "unlockWithTouchId": { "message": "Touch ID でロック解除" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index edeb817ac5d..7d748417b17 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "სერვერის URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index e9bebb8bfc0..f8f81a5ac2c 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index ad36e6590f7..094c4ced0bb 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "ಸರ್ವರ್ URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್‌ಗಾಗಿ ಪರಿಶೀಲಿಸಿ." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "ಟಚ್ ಐಡಿ ಯೊಂದಿಗೆ ಅನ್ಲಾಕ್ ಮಾಡಿ" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index b071f1734b3..9a63577dca0 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "서버 URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bitwarden에서 인증을 요청합니다." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Touch ID를 사용하여 잠금 해제" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index c346dcbef62..c6ab4ba6a7b 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Serverio nuoroda" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Patvirtinti Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Atrakinti naudojant Touch ID" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index d8aca775022..ba159a52d71 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Servera URL" }, + "authenticationTimeout": { + "message": "Autentificēšanās noildze" + }, + "authenticationSessionTimedOut": { + "message": "Iestājās autentificēšanās sesijas noildze. Lūgums sākt pieteikšanos no jauna." + }, "selfHostBaseUrl": { "message": "Pašmitināta servera URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1409,7 +1415,7 @@ "message": "Nav nekā, ko parādīt" }, "nothingGeneratedRecently": { - "message": "Pēdējā laikā nav nekas izveidots" + "message": "Pēdējā laikā nekas nav izveidots" }, "undo": { "message": "Atsaukt" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Apstiprināt Bitwarden." }, - "polkitConsentMessage": { - "message": "Autentificēt, lai atslēgtu Bitwarden." - }, "unlockWithTouchId": { "message": "Atslēgt ar Touch ID" }, @@ -2492,7 +2495,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Jāizmanto $RECOMMENDED$ vai vairāk rakstzīmju, la izveidotu spēcīgu paroli.", + "message": " Jāizmanto $RECOMMENDED$ vai vairāk rakstzīmju, lai izveidotu spēcīgu paroli.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2502,7 +2505,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Jāizmanto $RECOMMENDED$ vai vairāk vārdu, lai aizveidotu spēcīgu paroles vārdkopu.", + "message": " Jāizmanto $RECOMMENDED$ vai vairāk vārdu, lai izveidotu spēcīgu paroles vārdkopu.", "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": { diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index f6a7e899aef..bf7110b77a1 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verifikuj za Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Otključaj sa Touch ID" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 35df8e74614..f14d21f2c79 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "സെർവർ യു ർ ൽ" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bitwarden വേണ്ടി പരിശോധിച്ചുറപ്പിക്കുക." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Touch ID ഉപയോഗിച്ച് അൺലോക്കുചെയ്യുക" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index e9bebb8bfc0..f8f81a5ac2c 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index f0d9edb57de..dccd56fdb29 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 12a1608c70a..08a3ad9528f 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Tjener-nettadresse" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bekreft for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Lås opp med Touch ID" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 4d43f6ff11b..16a52cb1723 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 81aad63b2ed..9be7e7f2c58 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -217,7 +217,7 @@ "message": "Wachtwoord bevestigen" }, "enterSshKeyPasswordDesc": { - "message": "Voer het wachtwoord voor de SSH-sleutel in." + "message": "Voer het wachtwoord voor de SSH sleutel in." }, "enterSshKeyPassword": { "message": "Wachtwoord invoeren" @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server-URL" }, + "authenticationTimeout": { + "message": "Authenticatie-timeout" + }, + "authenticationSessionTimedOut": { + "message": "De inlogsessie is verlopen. Start het inlogproces opnieuw op." + }, "selfHostBaseUrl": { "message": "URL zelfgehoste server", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verifiëren voor Bitwarden." }, - "polkitConsentMessage": { - "message": "Verifieer om Bitwarden te ontgrendelen." - }, "unlockWithTouchId": { "message": "Ontgrendelen met Touch ID" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 210c32b6245..1b94deb76ab 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Nettadresse for tenar" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index eff19372cbb..ed279134638 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index e69fcbd73a4..9f160f6329e 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Adres URL serwera" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Zweryfikuj dla Bitwarden." }, - "polkitConsentMessage": { - "message": "Uwierzytelnij, aby odblokować Bitwarden." - }, "unlockWithTouchId": { "message": "Odblokuj za pomocą Touch ID" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index b9ec08a2de7..1a71014c6c8 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL do Servidor" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "URL do servidor auto-host", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verifique para o Bitwarden." }, - "polkitConsentMessage": { - "message": "Autentice para desbloquear o Bitwarden." - }, "unlockWithTouchId": { "message": "Desbloquear com o Touch ID" }, @@ -2492,7 +2495,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": "", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 9e1ccde4194..ffd1f8de2ce 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL do servidor" }, + "authenticationTimeout": { + "message": "Tempo limite de autenticação" + }, + "authenticationSessionTimedOut": { + "message": "A sessão de autenticação expirou. Por favor, reinicie o processo de início de sessão." + }, "selfHostBaseUrl": { "message": "URL do servidor auto-hospedado", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verificar para o Bitwarden." }, - "polkitConsentMessage": { - "message": "Autenticar para desbloquear o Bitwarden." - }, "unlockWithTouchId": { "message": "Desbloquear com Touch ID" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index c1e8f81824d..65bc39c4cda 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL server" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verificați pentru Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Deblocare cu Touch ID" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 51f6353ddd9..83a6c49f689 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL сервера" }, + "authenticationTimeout": { + "message": "Тайм-аут аутентификации" + }, + "authenticationSessionTimedOut": { + "message": "Сеанс аутентификации завершился по времени. Пожалуйста, перезапустите процесс авторизации." + }, "selfHostBaseUrl": { "message": "URL собственного сервера", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Верификация для Bitwarden." }, - "polkitConsentMessage": { - "message": "Для разблокировки Bitwarden пройдите аутентификацию." - }, "unlockWithTouchId": { "message": "Разблокировать с Touch ID" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 9d5047f92bb..b47ec4e684d 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index c7ac8a5e2cb..4abf39f0eaa 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL servera" }, + "authenticationTimeout": { + "message": "Časový limit overenia" + }, + "authenticationSessionTimedOut": { + "message": "Relácia overovania skončila. Znovu spustite proces prihlásenia." + }, "selfHostBaseUrl": { "message": "Adresa URL vlastného hostingu", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Overiť sa pre Bitwarden." }, - "polkitConsentMessage": { - "message": "Overením odomknete Bitwarden." - }, "unlockWithTouchId": { "message": "Odomknúť s Touch ID" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 9603136c75e..d146f0d72f3 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL naslov strežnika" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Preverite za Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Odkleni z biometriko" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index eca561f2fd7..d99db99d0f8 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -27,7 +27,7 @@ "message": "Сигурносна белешка" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH кључ" }, "folders": { "message": "Фасцикле" @@ -181,31 +181,31 @@ "message": "Адреса" }, "sshPrivateKey": { - "message": "Private key" + "message": "Приватни кључ" }, "sshPublicKey": { - "message": "Public key" + "message": "Јавни кључ" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Отисак" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Врста кључа" }, "sshKeyAlgorithmED25519": { "message": "ED25519" }, "sshKeyAlgorithmRSA2048": { - "message": "RSA 2048-Bit" + "message": "RSA 2048-бита" }, "sshKeyAlgorithmRSA3072": { - "message": "RSA 3072-Bit" + "message": "RSA 3072-бита" }, "sshKeyAlgorithmRSA4096": { - "message": "RSA 4096-Bit" + "message": "RSA 4096-бита" }, "sshKeyGenerated": { - "message": "A new SSH key was generated" + "message": "Генерисан је нови SSH кључ" }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." @@ -223,19 +223,19 @@ "message": "Enter password" }, "sshAgentUnlockRequired": { - "message": "Please unlock your vault to approve the SSH key request." + "message": "Откључајте свој сеф да бисте одобрили захтев за SSH кључ." }, "sshAgentUnlockTimeout": { - "message": "SSH key request timed out." + "message": "Захтев за SSH кључем је истекао." }, "enableSshAgent": { - "message": "Enable SSH agent" + "message": "Упали SSH агент" }, "enableSshAgentDesc": { - "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." + "message": "Омогућите SSH агенту да потписује SSH захтеве директно из вашег Bitwarden сефа." }, "enableSshAgentHelp": { - "message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." + "message": "SSH агент је услуга намењена програмерима која вам омогућава да потпишете SSH захтеве директно из вашег Bitwarden сефа." }, "premiumRequired": { "message": "Потребан Премијум" @@ -461,10 +461,10 @@ "message": "Копирај лозинку" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "Регенеришите SSH кључ" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "Копирајте SSH приватни кључ" }, "copyPassphrase": { "message": "Копирај приступну фразу", @@ -624,7 +624,7 @@ "message": "Креирај налог" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Нови сте у Bitwarden-у?" }, "setAStrongPassword": { "message": "Поставите јаку лозинку" @@ -636,16 +636,16 @@ "message": "Пријавите се" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Пријавите се на Bitwarden" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Пријавите се са приступним кључем" }, "loginWithDevice": { - "message": "Log in with device" + "message": "Пријавите се са уређајем" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Употребити једнократну пријаву" }, "submit": { "message": "Пошаљи" @@ -919,6 +919,12 @@ "baseUrl": { "message": "УРЛ Сервера" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "УРЛ сервера који се самостално хостује", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Потврди за Bitwarden." }, - "polkitConsentMessage": { - "message": "Аутентификујте се да бисте откључали Bitwarden." - }, "unlockWithTouchId": { "message": "Откључај са Touch ID" }, @@ -1768,10 +1771,10 @@ "message": "Брисање налога је трајно. Не може се поништити." }, "cannotDeleteAccount": { - "message": "Cannot delete account" + "message": "Није могуће избрисати налог" }, "cannotDeleteAccountDesc": { - "message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." + "message": "Ова радња се не може довршити јер је ваш налог у власништву организације. Обратите се администратору своје организације за додатне детаље." }, "accountDeleted": { "message": "Налог обрисан" @@ -3360,34 +3363,34 @@ "message": "Нису пронађени портови за SSO пријаву." }, "authorize": { - "message": "Authorize" + "message": "Ауторизуј" }, "deny": { - "message": "Deny" + "message": "Одбиј" }, "sshkeyApprovalTitle": { - "message": "Confirm SSH key usage" + "message": "Потврдите употребу SSH кључа" }, "sshkeyApprovalMessageInfix": { - "message": "is requesting access to" + "message": "тражи приступ" }, "unknownApplication": { - "message": "An application" + "message": "Апликација" }, "sshKeyPasswordUnsupported": { - "message": "Importing password protected SSH keys is not yet supported" + "message": "Увоз лозинке заштићене SSH кључом још увек није подржано" }, "invalidSshKey": { - "message": "The SSH key is invalid" + "message": "SSH кључ је неважећи" }, "sshKeyTypeUnsupported": { - "message": "The SSH key type is not supported" + "message": "Тип SSH кључа није подржан" }, "importSshKeyFromClipboard": { - "message": "Import key from clipboard" + "message": "Увезите кључ из оставе" }, "sshKeyPasted": { - "message": "SSH key imported successfully" + "message": "SSH кључ је успешно увезен" }, "fileSavedToDevice": { "message": "Датотека је сачувана на уређају. Управљајте преузимањима са свог уређаја." diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 624a6c666b3..a227f5d27c1 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server-URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bekräfta för Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Lås upp med Touch ID" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index e9bebb8bfc0..f8f81a5ac2c 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Server URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 8dd9e8c4f40..21e09c96ae5 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL ของเซิร์ฟเวอร์" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Verify for Bitwarden." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Unlock with Touch ID" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index bfda568235c..15e38ad3dc3 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Sunucu URL'si" }, + "authenticationTimeout": { + "message": "Kimlik doğrulama zaman aşımı" + }, + "authenticationSessionTimedOut": { + "message": "Kimlik doğrulama oturumu zaman aşımına uğradı. Lütfen giriş sürecini yeniden başlatın." + }, "selfHostBaseUrl": { "message": "Kendi kendine barındırılan sunucu URL'si", "description": "Label for field requesting a self-hosted integration service URL" @@ -1387,13 +1393,13 @@ "message": "Parola geçmişi" }, "generatorHistory": { - "message": "Generator history" + "message": "Üreteç geçmişi" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Üreteç geçmişini temizle" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Devam ederseniz üreteç geçmişindeki tüm kayıtlar kalıcı olarak silinecektir. Devam etmek istediğinizden emin misiniz?" }, "clear": { "message": "Temizle", @@ -1403,13 +1409,13 @@ "message": "Listelenecek şifre yok." }, "clearHistory": { - "message": "Clear history" + "message": "Geçmişi temizle" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Gösterilecek bir şey yok" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Yakın zamanda herhangi bir şey üretmediniz" }, "undo": { "message": "Geri al" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Bitwarden için doğrulayın." }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "Kilidi Touch ID ile aç" }, @@ -2478,7 +2481,7 @@ "message": "E-posta oluştur" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Değer $MIN$ ile $MAX$ arasında olmalıdır.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2866,7 +2869,7 @@ "message": "Bilinen veri ihlallerinde bu parolayı kontrol et" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Giriş yapıldı!" }, "important": { "message": "Önemli:" @@ -2899,16 +2902,16 @@ "message": "Önerilen Ayarlar Güncellemesi" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Sonraki girişleri kolaylaştırmak için bu cihazı hatırla" }, "deviceApprovalRequired": { "message": "Cihaz onayı gerekiyor. Lütfen onay yönteminizi seçin:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Cihazı onaylamanız gerekiyor" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Aşağıdan bir onay yöntemi seçin" }, "rememberThisDevice": { "message": "Bu cihazı hatırla" @@ -2963,7 +2966,7 @@ "message": "Kullanıcının e-postası eksik" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Aktif kullanıcı e-postası bulunamadı. Çıkış yapılıyor." }, "deviceTrusted": { "message": "Cihaza güvenildi" diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 733994d03b4..5f9b3eeac65 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -208,19 +208,19 @@ "message": "Згенеровано новий ключ SSH" }, "sshKeyWrongPassword": { - "message": "The password you entered is incorrect." + "message": "Ви ввели неправильний пароль." }, "importSshKey": { - "message": "Import" + "message": "Імпорт" }, "confirmSshKeyPassword": { - "message": "Confirm password" + "message": "Підтвердити пароль" }, "enterSshKeyPasswordDesc": { - "message": "Enter the password for the SSH key." + "message": "Введіть пароль для ключа SSH." }, "enterSshKeyPassword": { - "message": "Enter password" + "message": "Введіть пароль" }, "sshAgentUnlockRequired": { "message": "Розблокуйте своє сховище, щоб затвердити запит ключа SSH." @@ -919,6 +919,12 @@ "baseUrl": { "message": "URL-адреса сервера" }, + "authenticationTimeout": { + "message": "Час очікування автентифікації" + }, + "authenticationSessionTimedOut": { + "message": "Час очікування сеансу автентифікації завершився. Перезапустіть процес входу в систему." + }, "selfHostBaseUrl": { "message": "URL-адреса власного сервера", "description": "Label for field requesting a self-hosted integration service URL" @@ -1387,13 +1393,13 @@ "message": "Історія паролів" }, "generatorHistory": { - "message": "Generator history" + "message": "Історія генератора" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Очистити історію генератора" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Якщо ви продовжите, усі записи будуть остаточно видалені з історії генератора. Справді продовжити?" }, "clear": { "message": "Стерти", @@ -1403,13 +1409,13 @@ "message": "Немає паролів." }, "clearHistory": { - "message": "Clear history" + "message": "Очистити історію" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Немає даних для показу" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Ви нічого не генерували останнім часом" }, "undo": { "message": "Повернути" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Перевірити на Bitwarden." }, - "polkitConsentMessage": { - "message": "Автентифікуйтесь для розблокування Bitwarden." - }, "unlockWithTouchId": { "message": "Розблокувати з Touch ID" }, @@ -2478,7 +2481,7 @@ "message": "Генерувати е-пошту" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Значення має бути між $MIN$ та $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2492,7 +2495,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Використовуйте $RECOMMENDED$ або більше символів, щоб згенерувати надійний пароль.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2502,7 +2505,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Використовуйте $RECOMMENDED$ або більше слів, щоб згенерувати надійну парольну фразу.", "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": { @@ -2723,13 +2726,13 @@ "message": "Сповіщення було надіслано на ваш пристрій." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Сповіщення надіслано на ваш пристрій" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Переконайтеся, що ваш обліковий запис розблоковано і фраза відбитка на іншому пристрої збігається" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Потрібен інший варіант?" }, "fingerprintMatchInfo": { "message": "Переконайтеся, що ваше сховище розблоковане, а фраза відбитка збігається з іншим пристроєм." @@ -2738,13 +2741,13 @@ "message": "Фраза відбитка" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Після схвалення запиту ви отримаєте сповіщення" }, "needAnotherOption": { "message": "Потрібно увімкнути схвалення запитів на вхід у налаштуваннях програми Bitwarden. Потрібен інший варіант?" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Переглянути всі варіанти входу" }, "viewAllLoginOptions": { "message": "Переглянути всі варіанти входу" @@ -2866,7 +2869,7 @@ "message": "Перевірити відомі витоки даних для цього пароля" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Ви увійшли!" }, "important": { "message": "Важливо:" @@ -2899,16 +2902,16 @@ "message": "Оновлення рекомендованих налаштувань" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Запам'ятайте цей пристрій, щоб спростити майбутні входи в систему" }, "deviceApprovalRequired": { "message": "Необхідне підтвердження пристрою. Виберіть варіант підтвердження нижче:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Потрібне підтвердження пристрою" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Виберіть варіант підтвердження нижче" }, "rememberThisDevice": { "message": "Запам'ятати цей пристрій" @@ -2963,7 +2966,7 @@ "message": "Немає адреси електронної пошти" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Адресу е-пошти активного користувача не знайдено. Виконується вихід із системи." }, "deviceTrusted": { "message": "Довірений пристрій" diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 2da1b5e4099..a2c2aa7ed0f 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -919,6 +919,12 @@ "baseUrl": { "message": "Địa chỉ máy chủ" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "Xác minh cho Bitwarden." }, - "polkitConsentMessage": { - "message": "Xác thực để mở khóa Bitwarden." - }, "unlockWithTouchId": { "message": "Mở khóa với Touch ID" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 12ee33649a2..5a6d68cd90d 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -624,7 +624,7 @@ "message": "创建账户" }, "newToBitwarden": { - "message": "您是 Bitwarden 新手吗?" + "message": "Bitwarden 新手吗?" }, "setAStrongPassword": { "message": "设置强密码" @@ -919,6 +919,12 @@ "baseUrl": { "message": "服务器 URL" }, + "authenticationTimeout": { + "message": "身份验证超时" + }, + "authenticationSessionTimedOut": { + "message": "身份验证会话超时。请重新启动登录过程。" + }, "selfHostBaseUrl": { "message": "自托管服务器 URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "验证 Bitwarden。" }, - "polkitConsentMessage": { - "message": "验证以解锁 Bitwarden。" - }, "unlockWithTouchId": { "message": "使用触控 ID 解锁" }, @@ -2905,7 +2908,7 @@ "message": "需要设备批准。请在下面选择一个批准选项:" }, "deviceApprovalRequiredV2": { - "message": "需要设备批准" + "message": "已请求设备批准" }, "selectAnApprovalOptionBelow": { "message": "在下方选择一个批准选项" @@ -3127,7 +3130,7 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "与 Duo 服务连接时出错。请使用不同的两步登录方式或联系 Duo 寻求帮助。" + "message": "与 Duo 服务连接时出错。请使用不同的两步登录方式或联系 Duo 获取协助。" }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "启动 Duo 然后按照步骤完成登录。" diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 8052da08870..23166f4aea8 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -205,7 +205,7 @@ "message": "RSA 4096-Bit" }, "sshKeyGenerated": { - "message": "A new SSH key was generated" + "message": "一組SSH金鑰已在之前生成了" }, "sshKeyWrongPassword": { "message": "The password you entered is incorrect." @@ -223,19 +223,19 @@ "message": "Enter password" }, "sshAgentUnlockRequired": { - "message": "Please unlock your vault to approve the SSH key request." + "message": "請解鎖密碼庫以核准SSh金鑰的請求" }, "sshAgentUnlockTimeout": { - "message": "SSH key request timed out." + "message": "SSH金鑰請求超時" }, "enableSshAgent": { - "message": "Enable SSH agent" + "message": "啟用SSH代理" }, "enableSshAgentDesc": { - "message": "Enable the SSH agent to sign SSH requests right from your Bitwarden vault." + "message": "啟用SSBitwardenH代理以從 Bitwarden 密碼庫簽發SSH請求" }, "enableSshAgentHelp": { - "message": "The SSH agent is a service targeted at developers that allows you to sign SSH requests directly from your Bitwarden vault." + "message": "SSH代理是一個針對開發者的服務,它能夠直接從 Bitwarden 密碼庫簽發SSH請求。" }, "premiumRequired": { "message": "需要進階會員資格" @@ -323,7 +323,7 @@ "message": "產生密碼" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "產生密碼片語" }, "type": { "message": "類型" @@ -461,13 +461,13 @@ "message": "複製密碼" }, "regenerateSshKey": { - "message": "Regenerate SSH key" + "message": "重新產生密碼片語" }, "copySshPrivateKey": { - "message": "Copy SSH private key" + "message": "複製SSH私鑰" }, "copyPassphrase": { - "message": "Copy passphrase", + "message": "複製密碼片語", "description": "Copy passphrase to clipboard" }, "copyUri": { @@ -498,11 +498,11 @@ "message": "特殊字元 (!@#$%^&*)" }, "include": { - "message": "Include", + "message": "包含", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "包含大寫字元", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -518,7 +518,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "包含數字", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -526,7 +526,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "包含特殊字元", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -561,11 +561,11 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "避免易混淆的字元", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "企業原則之要求已在你的產生器選項中生效", "description": "Indicates that a policy limits the credential generator screen." }, "searchCollection": { @@ -624,22 +624,22 @@ "message": "建立帳戶" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "第一次使用 Bitwarden?" }, "setAStrongPassword": { "message": "設定一個強密碼" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "設定密碼以完成帳號創建" }, "logIn": { "message": "登入" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "登入 Bitwarden" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "以通行密鑰 (passkey) 登入" }, "loginWithDevice": { "message": "Log in with device" @@ -694,7 +694,7 @@ "message": "加入組織" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "加入 $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -767,7 +767,7 @@ "message": "Your new account has been created!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "你已經登入!" }, "masterPassSent": { "message": "已寄出包含您主密碼提示的電子郵件。" @@ -800,7 +800,7 @@ "message": "必須填入驗證碼。" }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "驗證已被取消或時間過長。請再試一次。" }, "invalidVerificationCode": { "message": "無效的驗證碼" @@ -919,6 +919,12 @@ "baseUrl": { "message": "伺服器 URL" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "selfHostBaseUrl": { "message": "Self-host server URL", "description": "Label for field requesting a self-hosted integration service URL" @@ -1728,9 +1734,6 @@ "windowsHelloConsentMessage": { "message": "驗證 Bitwarden。" }, - "polkitConsentMessage": { - "message": "Authenticate to unlock Bitwarden." - }, "unlockWithTouchId": { "message": "使用 Touch ID 解鎖" }, From 32c2cba2e7dca07ff41ff938af294ace19d63768 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Mon, 16 Dec 2024 10:07:46 -0500 Subject: [PATCH 04/57] create pipe for replacing string in template (#12381) --- .../src/app/billing/providers/clients/index.ts | 1 + .../src/app/billing/providers/clients/replace.pipe.ts | 11 +++++++++++ .../clients/vnext-manage-clients.component.html | 2 +- .../clients/vnext-manage-clients.component.ts | 4 ++-- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/billing/providers/clients/replace.pipe.ts diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts index f8b344372ef..05887fc198e 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -4,3 +4,4 @@ export * from "./manage-client-name-dialog.component"; export * from "./manage-client-subscription-dialog.component"; export * from "./no-clients.component"; export * from "./vnext-manage-clients.component"; +export * from "./replace.pipe"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/replace.pipe.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/replace.pipe.ts new file mode 100644 index 00000000000..4a06e85f533 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/replace.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +@Pipe({ + name: "replace", + standalone: true, +}) +export class ReplacePipe implements PipeTransform { + transform(value: string, pattern: string, replacement: string): string { + return value.replace(pattern, replacement); + } +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html index 99de9352f62..73a62c513e9 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html @@ -46,7 +46,7 @@ {{ row.remainingSeats }} - {{ row.plan }} + {{ row.plan | replace: " (Monthly)" : "" }} diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 161a1fe1692..93beef42bdb 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -137,6 +137,9 @@ export class LoginComponent implements OnInit, OnDestroy { } async ngOnInit(): Promise { + // Add popstate listener to listen for browser back button clicks + window.addEventListener("popstate", this.handlePopState); + // TODO: remove this when the UnauthenticatedExtensionUIRefresh feature flag is removed. this.listenForUnauthUiRefreshFlagChanges(); @@ -148,6 +151,9 @@ export class LoginComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { + // Remove popstate listener + window.removeEventListener("popstate", this.handlePopState); + if (this.clientType === ClientType.Desktop) { // TODO: refactor to not use deprecated broadcaster service. this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); @@ -562,4 +568,28 @@ export class LoginComponent implements OnInit, OnDestroy { this.clientType !== ClientType.Browser ); } + + /** + * Handle the back button click to transition back to the email entry state. + */ + protected async backButtonClicked() { + // Replace the history so the "forward" button doesn't show (which wouldn't do anything) + history.pushState(null, "", window.location.pathname); + await this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY); + } + + /** + * Handle the popstate event to transition back to the email entry state when the back button is clicked. + * @param event - The popstate event. + */ + private handlePopState = (event: PopStateEvent) => { + if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) { + // Prevent default navigation + event.preventDefault(); + // Replace the history so the "forward" button doesn't show (which wouldn't do anything) + history.pushState(null, "", window.location.pathname); + // Transition back to email entry state + void this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY); + } + }; } From 6132df395caef88729c79b52ad941debac33f970 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 16 Dec 2024 08:33:01 -0800 Subject: [PATCH 08/57] Add strict to missed components tsconfig (#12429) --- libs/components/src/button/button.component.spec.ts | 2 ++ .../bit-validators/forbidden-characters.validator.spec.ts | 2 ++ .../src/form-field/bit-validators/trim.validator.spec.ts | 2 ++ .../src/radio-button/radio-button.component.spec.ts | 2 ++ .../src/toggle-group/toggle-group.component.spec.ts | 2 ++ libs/components/src/toggle-group/toggle.component.spec.ts | 2 ++ libs/components/tsconfig.json | 7 ++++++- 7 files changed, 18 insertions(+), 1 deletion(-) diff --git a/libs/components/src/button/button.component.spec.ts b/libs/components/src/button/button.component.spec.ts index f3c3aa3175c..d63f611a5f8 100644 --- a/libs/components/src/button/button.component.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Component, DebugElement } from "@angular/core"; import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; diff --git a/libs/components/src/form-field/bit-validators/forbidden-characters.validator.spec.ts b/libs/components/src/form-field/bit-validators/forbidden-characters.validator.spec.ts index 332294b26ec..ecd9aa550a0 100644 --- a/libs/components/src/form-field/bit-validators/forbidden-characters.validator.spec.ts +++ b/libs/components/src/form-field/bit-validators/forbidden-characters.validator.spec.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { FormControl } from "@angular/forms"; import { forbiddenCharacters } from "./forbidden-characters.validator"; diff --git a/libs/components/src/form-field/bit-validators/trim.validator.spec.ts b/libs/components/src/form-field/bit-validators/trim.validator.spec.ts index 471f5396786..38dd36a7706 100644 --- a/libs/components/src/form-field/bit-validators/trim.validator.spec.ts +++ b/libs/components/src/form-field/bit-validators/trim.validator.spec.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { FormControl } from "@angular/forms"; import { trimValidator as validate } from "./trim.validator"; diff --git a/libs/components/src/radio-button/radio-button.component.spec.ts b/libs/components/src/radio-button/radio-button.component.spec.ts index c7344f1bd38..f8cdae00664 100644 --- a/libs/components/src/radio-button/radio-button.component.spec.ts +++ b/libs/components/src/radio-button/radio-button.component.spec.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Component } from "@angular/core"; import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; diff --git a/libs/components/src/toggle-group/toggle-group.component.spec.ts b/libs/components/src/toggle-group/toggle-group.component.spec.ts index 0fe863fcb9f..e418a7b410c 100644 --- a/libs/components/src/toggle-group/toggle-group.component.spec.ts +++ b/libs/components/src/toggle-group/toggle-group.component.spec.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Component } from "@angular/core"; import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; diff --git a/libs/components/src/toggle-group/toggle.component.spec.ts b/libs/components/src/toggle-group/toggle.component.spec.ts index 73809a97f76..fe91f94071d 100644 --- a/libs/components/src/toggle-group/toggle.component.spec.ts +++ b/libs/components/src/toggle-group/toggle.component.spec.ts @@ -1,3 +1,5 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore import { Component } from "@angular/core"; import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; diff --git a/libs/components/tsconfig.json b/libs/components/tsconfig.json index 3c947bf582d..dabcecf78e9 100644 --- a/libs/components/tsconfig.json +++ b/libs/components/tsconfig.json @@ -22,7 +22,12 @@ "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/angular/*": ["../angular/src/*"], "@bitwarden/platform": ["../platform/src"] - } + }, + "plugins": [ + { + "name": "typescript-strict-plugin" + } + ] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, From d317051d458b3ca71975904ebdd325a218f191f9 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:32:06 +0100 Subject: [PATCH 09/57] [PM-15920] Remove v1 export page (#12349) * Remove v1 export page and extension refresh conditional routing * Remove unused RouterLink import --------- Co-authored-by: Daniel James Smith --- apps/browser/src/popup/app-routing.module.ts | 6 +-- .../export/export-browser-v2.component.ts | 3 +- .../export/export-browser.component.html | 26 ------------ .../export/export-browser.component.ts | 40 ------------------- 4 files changed, 4 insertions(+), 71 deletions(-) delete mode 100644 apps/browser/src/tools/popup/settings/export/export-browser.component.html delete mode 100644 apps/browser/src/tools/popup/settings/export/export-browser.component.ts diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 38071a9e5c2..035bdb11431 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -95,7 +95,6 @@ import { AboutPageComponent } from "../tools/popup/settings/about-page/about-pag import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about-page/more-from-bitwarden-page-v2.component"; import { MoreFromBitwardenPageComponent } from "../tools/popup/settings/about-page/more-from-bitwarden-page.component"; import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-browser-v2.component"; -import { ExportBrowserComponent } from "../tools/popup/settings/export/export-browser.component"; import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component"; import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; @@ -355,11 +354,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }), - ...extensionRefreshSwap(ExportBrowserComponent, ExportBrowserV2Component, { + { path: "export", + component: ExportBrowserV2Component, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, - }), + }, ...extensionRefreshSwap(AutofillV1Component, AutofillComponent, { path: "autofill", canActivate: [authGuard], diff --git a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts index cbb66cbcf5a..86131176a6e 100644 --- a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; +import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; @@ -16,7 +16,6 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page standalone: true, imports: [ CommonModule, - RouterLink, JslibModule, DialogModule, AsyncActionsModule, diff --git a/apps/browser/src/tools/popup/settings/export/export-browser.component.html b/apps/browser/src/tools/popup/settings/export/export-browser.component.html deleted file mode 100644 index bccde32a68d..00000000000 --- a/apps/browser/src/tools/popup/settings/export/export-browser.component.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
- -
-

- {{ "exportVault" | i18n }} -

-
- -
-
-
-
- -
-
diff --git a/apps/browser/src/tools/popup/settings/export/export-browser.component.ts b/apps/browser/src/tools/popup/settings/export/export-browser.component.ts deleted file mode 100644 index 3125e0a2934..00000000000 --- a/apps/browser/src/tools/popup/settings/export/export-browser.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; -import { ExportComponent } from "@bitwarden/vault-export-ui"; - -@Component({ - templateUrl: "export-browser.component.html", - standalone: true, - imports: [ - CommonModule, - RouterLink, - JslibModule, - DialogModule, - AsyncActionsModule, - ButtonModule, - ExportComponent, - ], -}) -export class ExportBrowserComponent { - /** - * Used to control the disabled state of the Submit button - * Gets set indirectly by the disabled state being emitted from the sub-form when thier form gets disabled or the submit button is clicked - */ - protected disabled = false; - - /** - * Used to control the disabled state of the Submit button - * Gets set indirectly by the loading state being emitted from the sub-form when their form is loading or finished loading - */ - protected loading = false; - - constructor(private router: Router) {} - - protected async onSuccessfulExport(organizationId: string): Promise { - await this.router.navigate(["/vault-settings"]); - } -} From c628f541d18d0c99c8e9c123b135cab1a8c9631b Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Mon, 16 Dec 2024 12:35:00 -0500 Subject: [PATCH 10/57] Sign main branch Unified container builds with cosign and perform security scanning (#12403) --- .github/workflows/build-web.yml | 35 ++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 6e5e11c3361..c686b46d51a 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -174,6 +174,9 @@ jobs: build-containers: name: Build Docker images runs-on: ubuntu-22.04 + permissions: + security-events: write + id-token: write needs: - setup - build-artifacts @@ -270,6 +273,7 @@ jobs: run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Build Docker image + id: build-docker uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: apps/web @@ -279,11 +283,40 @@ jobs: tags: ${{ steps.image-name.outputs.name }} secrets: | "GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}" + + - name: Install Cosign + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + + - name: Sign image with Cosign + if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' + env: + DIGEST: ${{ steps.build-docker.outputs.digest }} + TAGS: ${{ steps.image-name.outputs.name }} + run: | + IFS="," read -a tags <<< "${TAGS}" + images="" + for tag in "${tags[@]}"; do + images+="${tag}@${DIGEST} " + done + cosign sign --yes ${images} + + - name: Scan Docker image + id: container-scan + uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0 + with: + image: ${{ steps.image-name.outputs.name }} + fail-build: false + output-format: sarif + + - name: Upload Grype results to GitHub + uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + with: + sarif_file: ${{ steps.container-scan.outputs.sarif }} - name: Log out of Docker run: docker logout - crowdin-push: name: Crowdin Push if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' From 971c157f564793aafbd62884e39d4f9f0dafbeac Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:00:17 -0600 Subject: [PATCH 11/57] [PM-12700] Add private key regeneration process (#11829) * add user asymmetric key api service * Add user asymmetric key regen service * add feature flag * Add LoginSuccessHandlerService * add loginSuccessHandlerService to BaseLoginViaWebAuthnComponent * Only run loginSuccessHandlerService if webAuthn is used for vault decryption. * Updates for TS strict * bump SDK version * swap to combineLatest * Update abstractions --- .../base-login-via-webauthn.component.ts | 21 +- .../src/services/jslib-services.module.ts | 29 ++ libs/auth/src/angular/lock/lock.component.ts | 9 +- .../login-via-auth-request.component.ts | 12 +- .../auth/src/angular/login/login.component.ts | 6 +- libs/auth/src/common/abstractions/index.ts | 1 + .../login-success-handler.service.ts | 10 + libs/auth/src/common/services/index.ts | 1 + .../default-login-success-handler.service.ts | 16 + libs/common/src/enums/feature-flag.enum.ts | 2 + libs/key-management/src/index.ts | 2 + ...asymmetric-key-regeneration-api.service.ts | 8 + ...ser-asymmetric-key-regeneration.service.ts | 10 + .../user-asymmetric-key-regeneration/index.ts | 5 + .../requests/key-regeneration.request.ts | 11 + ...asymmetric-key-regeneration-api.service.ts | 29 ++ ...symmetric-key-regeneration.service.spec.ts | 306 ++++++++++++++++++ ...ser-asymmetric-key-regeneration.service.ts | 158 +++++++++ package-lock.json | 8 +- package.json | 2 +- 20 files changed, 628 insertions(+), 18 deletions(-) create mode 100644 libs/auth/src/common/abstractions/login-success-handler.service.ts create mode 100644 libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration-api.service.ts create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration.service.ts create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/index.ts create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/models/requests/key-regeneration.request.ts create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration-api.service.ts create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts create mode 100644 libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts diff --git a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts b/libs/angular/src/auth/components/base-login-via-webauthn.component.ts index 82d93ff0b8b..1ad4829767a 100644 --- a/libs/angular/src/auth/components/base-login-via-webauthn.component.ts +++ b/libs/angular/src/auth/components/base-login-via-webauthn.component.ts @@ -2,7 +2,9 @@ // @ts-strict-ignore import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { LoginSuccessHandlerService } from "@bitwarden/auth/common"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view"; @@ -10,6 +12,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { KeyService } from "@bitwarden/key-management"; export type State = "assert" | "assertFailed"; @@ -26,6 +29,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { private logService: LogService, private validationService: ValidationService, private i18nService: I18nService, + private loginSuccessHandlerService: LoginSuccessHandlerService, + private keyService: KeyService, ) {} ngOnInit(): void { @@ -59,11 +64,21 @@ export class BaseLoginViaWebAuthnComponent implements OnInit { this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"), ); this.currentState = "assertFailed"; - } else if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { + return; + } + + // Only run loginSuccessHandlerService if webAuthn is used for vault decryption. + const userKey = await firstValueFrom(this.keyService.userKey$(authResult.userId)); + if (userKey) { + await this.loginSuccessHandlerService.run(authResult.userId); + } + + if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { await this.router.navigate([this.forcePasswordResetRoute]); - } else { - await this.router.navigate([this.successRoute]); + return; } + + await this.router.navigate([this.successRoute]); } catch (error) { if (error instanceof ErrorResponse) { this.validationService.showError(this.i18nService.t("invalidPasskeyPleaseTryAgain")); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 92042a4162f..0e50cec1b64 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -37,6 +37,8 @@ import { RegisterRouteService, AuthRequestApiService, DefaultAuthRequestApiService, + DefaultLoginSuccessHandlerService, + LoginSuccessHandlerService, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -281,6 +283,10 @@ import { DefaultBiometricStateService, KdfConfigService, DefaultKdfConfigService, + UserAsymmetricKeysRegenerationService, + DefaultUserAsymmetricKeysRegenerationService, + UserAsymmetricKeysRegenerationApiService, + DefaultUserAsymmetricKeysRegenerationApiService, } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; import { @@ -1395,6 +1401,29 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginDecryptionOptionsService, deps: [MessagingServiceAbstraction], }), + safeProvider({ + provide: UserAsymmetricKeysRegenerationApiService, + useClass: DefaultUserAsymmetricKeysRegenerationApiService, + deps: [ApiServiceAbstraction], + }), + safeProvider({ + provide: UserAsymmetricKeysRegenerationService, + useClass: DefaultUserAsymmetricKeysRegenerationService, + deps: [ + KeyServiceAbstraction, + CipherServiceAbstraction, + UserAsymmetricKeysRegenerationApiService, + LogService, + SdkService, + ApiServiceAbstraction, + ConfigService, + ], + }), + safeProvider({ + provide: LoginSuccessHandlerService, + useClass: DefaultLoginSuccessHandlerService, + deps: [SyncService, UserAsymmetricKeysRegenerationService], + }), ]; @NgModule({ diff --git a/libs/auth/src/angular/lock/lock.component.ts b/libs/auth/src/angular/lock/lock.component.ts index 14a5553577f..bcbc2bd5751 100644 --- a/libs/auth/src/angular/lock/lock.component.ts +++ b/libs/auth/src/angular/lock/lock.component.ts @@ -37,7 +37,11 @@ import { IconButtonModule, ToastService, } from "@bitwarden/components"; -import { KeyService, BiometricStateService } from "@bitwarden/key-management"; +import { + KeyService, + BiometricStateService, + UserAsymmetricKeysRegenerationService, +} from "@bitwarden/key-management"; import { PinServiceAbstraction } from "../../common/abstractions"; import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; @@ -139,6 +143,7 @@ export class LockV2Component implements OnInit, OnDestroy { private passwordStrengthService: PasswordStrengthServiceAbstraction, private formBuilder: FormBuilder, private toastService: ToastService, + private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService, private lockComponentService: LockComponentService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, @@ -532,6 +537,8 @@ export class LockV2Component implements OnInit, OnDestroy { // Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service. await this.syncService.fullSync(false); + await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(this.activeAccount.id); + if (this.clientType === "browser") { const previousUrl = this.lockComponentService.getPreviousUrl(); /** diff --git a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts index 99e52d30914..b9a5ee4fe73 100644 --- a/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts +++ b/libs/auth/src/angular/login-via-auth-request/login-via-auth-request.component.ts @@ -12,6 +12,7 @@ import { AuthRequestServiceAbstraction, LoginEmailServiceAbstraction, LoginStrategyServiceAbstraction, + LoginSuccessHandlerService, } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; @@ -34,7 +35,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -88,9 +88,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { private passwordGenerationService: PasswordGenerationServiceAbstraction, private platformUtilsService: PlatformUtilsService, private router: Router, - private syncService: SyncService, private toastService: ToastService, private validationService: ValidationService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) { this.clientType = this.platformUtilsService.getClientType(); @@ -485,7 +485,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { const activeAccount = await firstValueFrom(this.accountService.activeAccount$); await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); - await this.handleSuccessfulLoginNavigation(); + await this.handleSuccessfulLoginNavigation(userId); } /** @@ -555,17 +555,17 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy { } else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) { await this.router.navigate(["update-temp-password"]); } else { - await this.handleSuccessfulLoginNavigation(); + await this.handleSuccessfulLoginNavigation(loginResponse.userId); } } - private async handleSuccessfulLoginNavigation() { + private async handleSuccessfulLoginNavigation(userId: UserId) { if (this.flow === Flow.StandardAuthRequest) { // Only need to set remembered email on standard login with auth req flow await this.loginEmailService.saveEmailSettings(); } - await this.syncService.fullSync(true); + await this.loginSuccessHandlerService.run(userId); await this.router.navigate(["vault"]); } } diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 93beef42bdb..33c167dcaed 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -10,6 +10,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { LoginEmailServiceAbstraction, LoginStrategyServiceAbstraction, + LoginSuccessHandlerService, PasswordLoginCredentials, RegisterRouteService, } from "@bitwarden/auth/common"; @@ -31,7 +32,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/platform/sync"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { AsyncActionsModule, @@ -127,11 +127,11 @@ export class LoginComponent implements OnInit, OnDestroy { private policyService: InternalPolicyService, private registerRouteService: RegisterRouteService, private router: Router, - private syncService: SyncService, private toastService: ToastService, private logService: LogService, private validationService: ValidationService, private configService: ConfigService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) { this.clientType = this.platformUtilsService.getClientType(); } @@ -280,7 +280,7 @@ export class LoginComponent implements OnInit, OnDestroy { return; } - await this.syncService.fullSync(true); + await this.loginSuccessHandlerService.run(authResult.userId); if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { this.loginEmailService.clearValues(); diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts index 88a13b490d6..c0dc500ddb9 100644 --- a/libs/auth/src/common/abstractions/index.ts +++ b/libs/auth/src/common/abstractions/index.ts @@ -5,3 +5,4 @@ export * from "./login-strategy.service"; export * from "./user-decryption-options.service.abstraction"; export * from "./auth-request.service.abstraction"; export * from "./login-approval-component.service.abstraction"; +export * from "./login-success-handler.service"; diff --git a/libs/auth/src/common/abstractions/login-success-handler.service.ts b/libs/auth/src/common/abstractions/login-success-handler.service.ts new file mode 100644 index 00000000000..8dee1dd32b9 --- /dev/null +++ b/libs/auth/src/common/abstractions/login-success-handler.service.ts @@ -0,0 +1,10 @@ +import { UserId } from "@bitwarden/common/types/guid"; + +export abstract class LoginSuccessHandlerService { + /** + * Runs any service calls required after a successful login. + * Service calls that should be included in this method are only those required to be awaited after successful login. + * @param userId The user id. + */ + abstract run(userId: UserId): Promise; +} diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index 41e0ba087ae..d1cedebcf36 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -6,3 +6,4 @@ export * from "./auth-request/auth-request.service"; export * from "./auth-request/auth-request-api.service"; export * from "./register-route.service"; export * from "./accounts/lock.service"; +export * from "./login-success-handler/default-login-success-handler.service"; diff --git a/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts new file mode 100644 index 00000000000..215329051df --- /dev/null +++ b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts @@ -0,0 +1,16 @@ +import { SyncService } from "@bitwarden/common/platform/sync"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserAsymmetricKeysRegenerationService } from "@bitwarden/key-management"; + +import { LoginSuccessHandlerService } from "../../abstractions/login-success-handler.service"; + +export class DefaultLoginSuccessHandlerService implements LoginSuccessHandlerService { + constructor( + private syncService: SyncService, + private userAsymmetricKeysRegenerationService: UserAsymmetricKeysRegenerationService, + ) {} + async run(userId: UserId): Promise { + await this.syncService.fullSync(true); + await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 2c3f81c9c75..cc2abed3ba1 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -42,6 +42,7 @@ export enum FeatureFlag { MacOsNativeCredentialSync = "macos-native-credential-sync", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", + PrivateKeyRegeneration = "pm-12241-private-key-regeneration", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -94,6 +95,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.MacOsNativeCredentialSync]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, [FeatureFlag.PM12443RemovePagingLogic]: FALSE, + [FeatureFlag.PrivateKeyRegeneration]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/key-management/src/index.ts b/libs/key-management/src/index.ts index a779d3a9caf..1734d857a0c 100644 --- a/libs/key-management/src/index.ts +++ b/libs/key-management/src/index.ts @@ -17,3 +17,5 @@ export { export { KdfConfigService } from "./abstractions/kdf-config.service"; export { DefaultKdfConfigService } from "./kdf-config.service"; export { KdfType } from "./enums/kdf-type.enum"; + +export * from "./user-asymmetric-key-regeneration"; diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration-api.service.ts b/libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration-api.service.ts new file mode 100644 index 00000000000..2b6e093d796 --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration-api.service.ts @@ -0,0 +1,8 @@ +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +export abstract class UserAsymmetricKeysRegenerationApiService { + abstract regenerateUserAsymmetricKeys( + userPublicKey: string, + userKeyEncryptedUserPrivateKey: EncString, + ): Promise; +} diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration.service.ts b/libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration.service.ts new file mode 100644 index 00000000000..4703d836db7 --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/abstractions/user-asymmetric-key-regeneration.service.ts @@ -0,0 +1,10 @@ +import { UserId } from "@bitwarden/common/types/guid"; + +export abstract class UserAsymmetricKeysRegenerationService { + /** + * Attempts to regenerate the user's asymmetric keys if they are invalid. + * Requires the PrivateKeyRegeneration feature flag to be enabled if not the method will do nothing. + * @param userId The user id. + */ + abstract regenerateIfNeeded(userId: UserId): Promise; +} diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/index.ts b/libs/key-management/src/user-asymmetric-key-regeneration/index.ts new file mode 100644 index 00000000000..8147d76b492 --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/index.ts @@ -0,0 +1,5 @@ +export { UserAsymmetricKeysRegenerationService } from "./abstractions/user-asymmetric-key-regeneration.service"; +export { DefaultUserAsymmetricKeysRegenerationService } from "./services/default-user-asymmetric-key-regeneration.service"; + +export { UserAsymmetricKeysRegenerationApiService } from "./abstractions/user-asymmetric-key-regeneration-api.service"; +export { DefaultUserAsymmetricKeysRegenerationApiService } from "./services/default-user-asymmetric-key-regeneration-api.service"; diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/models/requests/key-regeneration.request.ts b/libs/key-management/src/user-asymmetric-key-regeneration/models/requests/key-regeneration.request.ts new file mode 100644 index 00000000000..2d3b62aedad --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/models/requests/key-regeneration.request.ts @@ -0,0 +1,11 @@ +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +export class KeyRegenerationRequest { + userPublicKey: string; + userKeyEncryptedUserPrivateKey: EncString; + + constructor(userPublicKey: string, userKeyEncryptedUserPrivateKey: EncString) { + this.userPublicKey = userPublicKey; + this.userKeyEncryptedUserPrivateKey = userKeyEncryptedUserPrivateKey; + } +} diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration-api.service.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration-api.service.ts new file mode 100644 index 00000000000..d1fe89a74eb --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration-api.service.ts @@ -0,0 +1,29 @@ +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service"; +import { KeyRegenerationRequest } from "../models/requests/key-regeneration.request"; + +export class DefaultUserAsymmetricKeysRegenerationApiService + implements UserAsymmetricKeysRegenerationApiService +{ + constructor(private apiService: ApiService) {} + + async regenerateUserAsymmetricKeys( + userPublicKey: string, + userKeyEncryptedUserPrivateKey: EncString, + ): Promise { + const request: KeyRegenerationRequest = { + userPublicKey, + userKeyEncryptedUserPrivateKey, + }; + + await this.apiService.send( + "POST", + "/accounts/key-management/regenerate-keys", + request, + true, + true, + ); + } +} diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts new file mode 100644 index 00000000000..77d7ebbb814 --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.spec.ts @@ -0,0 +1,306 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { of, throwError } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { makeStaticByteArray, mockEnc } from "@bitwarden/common/spec"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { BitwardenClient, VerifyAsymmetricKeysResponse } from "@bitwarden/sdk-internal"; + +import { KeyService } from "../../abstractions/key.service"; +import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service"; + +import { DefaultUserAsymmetricKeysRegenerationService } from "./default-user-asymmetric-key-regeneration.service"; + +function setupVerificationResponse( + mockVerificationResponse: VerifyAsymmetricKeysResponse, + sdkService: MockProxy, +) { + const mockKeyPairResponse = { + userPublicKey: "userPublicKey", + userKeyEncryptedPrivateKey: "userKeyEncryptedPrivateKey", + }; + + sdkService.client$ = of({ + crypto: () => ({ + verify_asymmetric_keys: jest.fn().mockReturnValue(mockVerificationResponse), + make_key_pair: jest.fn().mockReturnValue(mockKeyPairResponse), + }), + free: jest.fn(), + echo: jest.fn(), + version: jest.fn(), + throw: jest.fn(), + catch: jest.fn(), + } as unknown as BitwardenClient); +} + +function setupUserKeyValidation( + cipherService: MockProxy, + keyService: MockProxy, + encryptService: MockProxy, +) { + const cipher = new Cipher(); + cipher.id = "id"; + cipher.edit = true; + cipher.viewPassword = true; + cipher.favorite = false; + cipher.name = mockEnc("EncryptedString"); + cipher.notes = mockEnc("EncryptedString"); + cipher.key = mockEnc("EncKey"); + cipherService.getAll.mockResolvedValue([cipher]); + encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64)); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); +} + +describe("regenerateIfNeeded", () => { + let sut: DefaultUserAsymmetricKeysRegenerationService; + const userId = "userId" as UserId; + + let keyService: MockProxy; + let cipherService: MockProxy; + let userAsymmetricKeysRegenerationApiService: MockProxy; + let logService: MockProxy; + let sdkService: MockProxy; + let apiService: MockProxy; + let configService: MockProxy; + let encryptService: MockProxy; + + beforeEach(() => { + keyService = mock(); + cipherService = mock(); + userAsymmetricKeysRegenerationApiService = mock(); + logService = mock(); + sdkService = mock(); + apiService = mock(); + configService = mock(); + encryptService = mock(); + + sut = new DefaultUserAsymmetricKeysRegenerationService( + keyService, + cipherService, + userAsymmetricKeysRegenerationApiService, + logService, + sdkService, + apiService, + configService, + ); + + configService.getFeatureFlag.mockResolvedValue(true); + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockEncryptedString = new SymmetricCryptoKey( + mockRandomBytes, + ).toString() as EncryptedString; + const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + keyService.userKey$.mockReturnValue(of(mockUserKey)); + keyService.userEncryptedPrivateKey$.mockReturnValue(of(mockEncryptedString)); + apiService.getUserPublicKey.mockResolvedValue({ + userId: "userId", + publicKey: "publicKey", + } as any); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should not call regeneration code when feature flag is off", async () => { + configService.getFeatureFlag.mockResolvedValue(false); + + await sut.regenerateIfNeeded(userId); + + expect(keyService.userKey$).not.toHaveBeenCalled(); + }); + + it("should not regenerate when top level error is thrown", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + keyService.userKey$.mockReturnValue(throwError(() => new Error("error"))); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not regenerate when private key is decryptable and valid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should regenerate when private key is decryptable and invalid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).toHaveBeenCalled(); + }); + + it("should not set private key on known API error", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys.mockRejectedValue( + new Error("Key regeneration not supported for this user."), + ); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not set private key on unknown API error", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: true, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys.mockRejectedValue( + new Error("error"), + ); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should regenerate when private key is not decryptable and user key is valid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and user key is invalid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + encryptService.decryptToBytes.mockRejectedValue(new Error("error")); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and no ciphers to check", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: true, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + cipherService.getAll.mockResolvedValue([]); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should regenerate when private key is not decryptable and invalid and user key is valid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).toHaveBeenCalled(); + expect(keyService.setPrivateKey).toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and invalid and user key is invalid", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + setupUserKeyValidation(cipherService, keyService, encryptService); + encryptService.decryptToBytes.mockRejectedValue(new Error("error")); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); + + it("should not regenerate when private key is not decryptable and invalid and no ciphers to check", async () => { + const mockVerificationResponse: VerifyAsymmetricKeysResponse = { + privateKeyDecryptable: false, + validPrivateKey: false, + }; + setupVerificationResponse(mockVerificationResponse, sdkService); + cipherService.getAll.mockResolvedValue([]); + + await sut.regenerateIfNeeded(userId); + + expect( + userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys, + ).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); + }); +}); diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts new file mode 100644 index 00000000000..ffaa3a82608 --- /dev/null +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts @@ -0,0 +1,158 @@ +import { combineLatest, firstValueFrom, map } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + +import { KeyService } from "../../abstractions/key.service"; +import { UserAsymmetricKeysRegenerationApiService } from "../abstractions/user-asymmetric-key-regeneration-api.service"; +import { UserAsymmetricKeysRegenerationService } from "../abstractions/user-asymmetric-key-regeneration.service"; + +export class DefaultUserAsymmetricKeysRegenerationService + implements UserAsymmetricKeysRegenerationService +{ + constructor( + private keyService: KeyService, + private cipherService: CipherService, + private userAsymmetricKeysRegenerationApiService: UserAsymmetricKeysRegenerationApiService, + private logService: LogService, + private sdkService: SdkService, + private apiService: ApiService, + private configService: ConfigService, + ) {} + + async regenerateIfNeeded(userId: UserId): Promise { + try { + const privateKeyRegenerationFlag = await this.configService.getFeatureFlag( + FeatureFlag.PrivateKeyRegeneration, + ); + + if (privateKeyRegenerationFlag) { + const shouldRegenerate = await this.shouldRegenerate(userId); + if (shouldRegenerate) { + await this.regenerateUserAsymmetricKeys(userId); + } + } + } catch (error) { + this.logService.error( + "[UserAsymmetricKeyRegeneration] An error occurred: " + + error + + " Skipping regeneration for the user.", + ); + } + } + + private async shouldRegenerate(userId: UserId): Promise { + const [userKey, userKeyEncryptedPrivateKey, publicKeyResponse] = await firstValueFrom( + combineLatest([ + this.keyService.userKey$(userId), + this.keyService.userEncryptedPrivateKey$(userId), + this.apiService.getUserPublicKey(userId), + ]), + ); + + const verificationResponse = await firstValueFrom( + this.sdkService.client$.pipe( + map((sdk) => { + if (sdk === undefined) { + throw new Error("SDK is undefined"); + } + return sdk.crypto().verify_asymmetric_keys({ + userKey: userKey.keyB64, + userPublicKey: publicKeyResponse.publicKey, + userKeyEncryptedPrivateKey: userKeyEncryptedPrivateKey, + }); + }), + ), + ); + + if (verificationResponse.privateKeyDecryptable) { + if (verificationResponse.validPrivateKey) { + // The private key is decryptable and valid. Should not regenerate. + return false; + } else { + // The private key is decryptable but not valid so we should regenerate it. + this.logService.info( + "[UserAsymmetricKeyRegeneration] User's private key is decryptable but not a valid key, attempting regeneration.", + ); + return true; + } + } + + // The private isn't decryptable, check to see if we can decrypt something with the userKey. + const userKeyCanDecrypt = await this.userKeyCanDecrypt(userKey); + if (userKeyCanDecrypt) { + this.logService.info( + "[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, attempting regeneration.", + ); + return true; + } + + this.logService.warning( + "[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, but unable to determine User Symmetric Key validity, skipping regeneration.", + ); + return false; + } + + private async regenerateUserAsymmetricKeys(userId: UserId): Promise { + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + const makeKeyPairResponse = await firstValueFrom( + this.sdkService.client$.pipe( + map((sdk) => { + if (sdk === undefined) { + throw new Error("SDK is undefined"); + } + return sdk.crypto().make_key_pair(userKey.keyB64); + }), + ), + ); + + try { + await this.userAsymmetricKeysRegenerationApiService.regenerateUserAsymmetricKeys( + makeKeyPairResponse.userPublicKey, + new EncString(makeKeyPairResponse.userKeyEncryptedPrivateKey), + ); + } catch (error: any) { + if (error?.message === "Key regeneration not supported for this user.") { + this.logService.info( + "[UserAsymmetricKeyRegeneration] Regeneration not supported for this user at this time.", + ); + } else { + this.logService.error( + "[UserAsymmetricKeyRegeneration] Regeneration error when submitting the request to the server: " + + error, + ); + } + return; + } + + await this.keyService.setPrivateKey(makeKeyPairResponse.userKeyEncryptedPrivateKey, userId); + this.logService.info( + "[UserAsymmetricKeyRegeneration] User's asymmetric keys successfully regenerated.", + ); + } + + private async userKeyCanDecrypt(userKey: UserKey): Promise { + const ciphers = await this.cipherService.getAll(); + const cipher = ciphers.find((cipher) => cipher.organizationId == null); + + if (cipher != null) { + try { + await cipher.decrypt(userKey); + return true; + } catch (error) { + this.logService.error( + "[UserAsymmetricKeyRegeneration] User Symmetric Key validation error: " + error, + ); + return false; + } + } + return false; + } +} diff --git a/package-lock.json b/package-lock.json index ff7dac2c461..28acb998fc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@bitwarden/sdk-internal": "0.2.0-main.3", + "@bitwarden/sdk-internal": "0.2.0-main.38", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", @@ -4298,9 +4298,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.3", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.3.tgz", - "integrity": "sha512-CYp98uaVMSFp6nr/QLw+Qw8ttnVtWark/bMpw59OhwMVhrCDKmpCgcR9G4oEdVO11IuFcYZieTBmtOEPhCpGaw==", + "version": "0.2.0-main.38", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.38.tgz", + "integrity": "sha512-bkN+BZC0YA4k0To8QiT33UTZX8peKDXud8Gzq3UHNPlU/vMSkP3Wn8q0GezzmYN3UNNIWXfreNCS0mJ+S51j/Q==", "license": "GPL-3.0" }, "node_modules/@bitwarden/vault": { diff --git a/package.json b/package.json index aa567f18df6..069644dea3f 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "@angular/platform-browser": "17.3.12", "@angular/platform-browser-dynamic": "17.3.12", "@angular/router": "17.3.12", - "@bitwarden/sdk-internal": "0.2.0-main.3", + "@bitwarden/sdk-internal": "0.2.0-main.38", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", From e7804856ab47e04fa956898786803589347ef73b Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:43:13 +0100 Subject: [PATCH 12/57] [PM-15919] Remove v1 import page after Extension refresh (#12343) * Remove v1 Import page and extension refresh conditional routing * Remove unused RouterLink import --------- Co-authored-by: Daniel James Smith --- apps/browser/src/popup/app-routing.module.ts | 6 ++-- .../import/import-browser-v2.component.ts | 3 +- .../import/import-browser.component.html | 26 --------------- .../import/import-browser.component.ts | 33 ------------------- 4 files changed, 4 insertions(+), 64 deletions(-) delete mode 100644 apps/browser/src/tools/popup/settings/import/import-browser.component.html delete mode 100644 apps/browser/src/tools/popup/settings/import/import-browser.component.ts diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 035bdb11431..f349ada1377 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -96,7 +96,6 @@ import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about- import { MoreFromBitwardenPageComponent } from "../tools/popup/settings/about-page/more-from-bitwarden-page.component"; import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-browser-v2.component"; import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component"; -import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard"; @@ -349,11 +348,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }), - ...extensionRefreshSwap(ImportBrowserComponent, ImportBrowserV2Component, { + { path: "import", + component: ImportBrowserV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "export", component: ExportBrowserV2Component, diff --git a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts index 16759057ed5..66cb5c62f48 100644 --- a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; +import { Router } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; @@ -16,7 +16,6 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page standalone: true, imports: [ CommonModule, - RouterLink, JslibModule, DialogModule, AsyncActionsModule, diff --git a/apps/browser/src/tools/popup/settings/import/import-browser.component.html b/apps/browser/src/tools/popup/settings/import/import-browser.component.html deleted file mode 100644 index 67b5eb348ae..00000000000 --- a/apps/browser/src/tools/popup/settings/import/import-browser.component.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
- -
-

- {{ "importData" | i18n }} -

-
- -
-
-
-
- -
-
diff --git a/apps/browser/src/tools/popup/settings/import/import-browser.component.ts b/apps/browser/src/tools/popup/settings/import/import-browser.component.ts deleted file mode 100644 index 7ee4877ce1a..00000000000 --- a/apps/browser/src/tools/popup/settings/import/import-browser.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { Router, RouterLink } from "@angular/router"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components"; -import { ImportComponent } from "@bitwarden/importer/ui"; - -@Component({ - templateUrl: "import-browser.component.html", - standalone: true, - imports: [ - CommonModule, - RouterLink, - JslibModule, - DialogModule, - AsyncActionsModule, - ButtonModule, - ImportComponent, - ], -}) -export class ImportBrowserComponent { - protected disabled = false; - protected loading = false; - - constructor(private router: Router) {} - - protected async onSuccessfulImport(organizationId: string): Promise { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/tabs/settings"]); - } -} From 05783249b2c80fab506ea47c1912a850c6ad0e63 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 16 Dec 2024 20:07:39 +0000 Subject: [PATCH 13/57] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- package-lock.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 647847db457..f2f426a803b 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.12.0", + "version": "2024.12.1", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 0ccc75cd5da..cd2d933a669 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.12.0", + "version": "2024.12.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 32f58b0cc52..b9c37303617 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.12.0", + "version": "2024.12.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/package-lock.json b/package-lock.json index 28acb998fc6..5156b0b8349 100644 --- a/package-lock.json +++ b/package-lock.json @@ -190,7 +190,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.12.0" + "version": "2024.12.1" }, "apps/cli": { "name": "@bitwarden/cli", From a4db5279b776e34fe1f65d44beae3cf79938d40b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 16 Dec 2024 16:10:32 -0500 Subject: [PATCH 14/57] [PM-16097] Separate copy buttons appearance setting (#12428) --------- Co-authored-by: William Martin --- apps/browser/src/_locales/en/messages.json | 3 + .../item-copy-actions.component.html | 162 ++++++++++++------ .../item-copy-actions.component.ts | 6 +- .../vault-popup-copy-buttons.service.ts | 39 +++++ .../settings/appearance-v2.component.html | 5 + .../settings/appearance-v2.component.spec.ts | 11 ++ .../popup/settings/appearance-v2.component.ts | 17 ++ .../src/platform/state/state-definitions.ts | 1 + 8 files changed, 194 insertions(+), 50 deletions(-) create mode 100644 apps/browser/src/vault/popup/services/vault-popup-copy-buttons.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f4a498f3e05..8eb2aecaf61 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4679,6 +4679,9 @@ "showNumberOfAutofillSuggestions": { "message": "Show number of login autofill suggestions on extension icon" }, + "showQuickCopyActions": { + "message": "Show quick copy actions on Vault" + }, "systemDefault": { "message": "System default" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html index 973b1f9f1a4..fbfebe8efff 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -1,53 +1,117 @@ - - - - - - - - + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -

+

{{ "claimedDomainsDesc" | i18n }} Date: Tue, 17 Dec 2024 16:54:04 +0100 Subject: [PATCH 20/57] Remove v1 tools owned settings pages and extension refresh conditional routing (#12350) Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 3 - apps/browser/src/popup/app-routing.module.ts | 18 ++-- apps/browser/src/popup/app.module.ts | 2 - .../about-page/about-page.component.html | 63 ------------ .../about-page/about-page.component.ts | 84 ---------------- .../more-from-bitwarden-page.component.html | 76 --------------- .../more-from-bitwarden-page.component.ts | 97 ------------------- .../popup/settings/settings.component.html | 63 ------------ .../popup/settings/settings.component.ts | 9 -- 9 files changed, 9 insertions(+), 406 deletions(-) delete mode 100644 apps/browser/src/tools/popup/settings/about-page/about-page.component.html delete mode 100644 apps/browser/src/tools/popup/settings/about-page/about-page.component.ts delete mode 100644 apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.html delete mode 100644 apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.ts delete mode 100644 apps/browser/src/tools/popup/settings/settings.component.html delete mode 100644 apps/browser/src/tools/popup/settings/settings.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8eb2aecaf61..c2e9ef60d8c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -648,9 +648,6 @@ "rateExtension": { "message": "Rate the extension" }, - "rateExtensionDesc": { - "message": "Please consider helping us out with a good review!" - }, "browserNotSupportClipboard": { "message": "Your web browser does not support easy clipboard copying. Copy it manually instead." }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index f349ada1377..85ae861c9d5 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -91,13 +91,10 @@ import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/s import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component"; import { SendV2Component } from "../tools/popup/send-v2/send-v2.component"; import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component"; -import { AboutPageComponent } from "../tools/popup/settings/about-page/about-page.component"; import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about-page/more-from-bitwarden-page-v2.component"; -import { MoreFromBitwardenPageComponent } from "../tools/popup/settings/about-page/more-from-bitwarden-page.component"; import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-browser-v2.component"; import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; -import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; @@ -702,16 +699,18 @@ const routes: Routes = [ canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, { + { path: "about", + component: AboutPageV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(MoreFromBitwardenPageComponent, MoreFromBitwardenPageV2Component, { + }, + { path: "more-from-bitwarden", + component: MoreFromBitwardenPageV2Component, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, - }), + }, ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", data: { elevation: 0 } satisfies RouteDataProperties, @@ -740,11 +739,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 0 } satisfies RouteDataProperties, }), - ...extensionRefreshSwap(SettingsComponent, SettingsV2Component, { + { path: "settings", + component: SettingsV2Component, canActivate: [authGuard], data: { elevation: 0 } satisfies RouteDataProperties, - }), + }, ...extensionRefreshSwap(SendGroupingsComponent, SendV2Component, { path: "send", canActivate: [authGuard], diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 760b43a879c..76bd06565c7 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -63,7 +63,6 @@ import { SendListComponent } from "../tools/popup/send/components/send-list.comp import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.component"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; -import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component"; @@ -174,7 +173,6 @@ import "../platform/popup/locales"; SendListComponent, SendTypeComponent, SetPasswordComponent, - SettingsComponent, VaultSettingsComponent, ShareComponent, SsoComponentV1, diff --git a/apps/browser/src/tools/popup/settings/about-page/about-page.component.html b/apps/browser/src/tools/popup/settings/about-page/about-page.component.html deleted file mode 100644 index 7537c75bd9e..00000000000 --- a/apps/browser/src/tools/popup/settings/about-page/about-page.component.html +++ /dev/null @@ -1,63 +0,0 @@ -

-
- -
-

- {{ "about" | i18n }} -

-
- -
-
-
-
-
- - - - - -
-
-
diff --git a/apps/browser/src/tools/popup/settings/about-page/about-page.component.ts b/apps/browser/src/tools/popup/settings/about-page/about-page.component.ts deleted file mode 100644 index 7c3e87a92fb..00000000000 --- a/apps/browser/src/tools/popup/settings/about-page/about-page.component.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { RouterModule } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { DeviceType } from "@bitwarden/common/enums"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; - -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; -import { AboutDialogComponent } from "../about-dialog/about-dialog.component"; - -const RateUrls = { - [DeviceType.ChromeExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.FirefoxExtension]: - "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", - [DeviceType.OperaExtension]: - "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", - [DeviceType.EdgeExtension]: - "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", - [DeviceType.VivaldiExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", -}; - -@Component({ - templateUrl: "about-page.component.html", - standalone: true, - imports: [CommonModule, JslibModule, RouterModule, PopOutComponent], -}) -export class AboutPageComponent { - constructor( - private dialogService: DialogService, - private environmentService: EnvironmentService, - private platformUtilsService: PlatformUtilsService, - ) {} - - about() { - this.dialogService.open(AboutDialogComponent); - } - - async launchHelp() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToHelpCenter" }, - content: { key: "continueToHelpCenterDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - await BrowserApi.createNewTab("https://bitwarden.com/help/"); - } - } - - async openWebVault() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToWebApp" }, - content: { key: "continueToWebAppDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - const env = await firstValueFrom(this.environmentService.environment$); - const url = env.getWebVaultUrl(); - await BrowserApi.createNewTab(url); - } - } - - async rate() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToBrowserExtensionStore" }, - content: { key: "continueToBrowserExtensionStoreDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - const deviceType = this.platformUtilsService.getDevice(); - await BrowserApi.createNewTab((RateUrls as any)[deviceType]); - } - } -} diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.html b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.html deleted file mode 100644 index 8e7b3495365..00000000000 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.html +++ /dev/null @@ -1,76 +0,0 @@ -
-
- -
-

- {{ "moreFromBitwarden" | i18n }} -

-
- -
-
-
-
-
-
- -
- - - - - -
-
-
diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.ts deleted file mode 100644 index 1f26d40b349..00000000000 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page.component.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { RouterModule } from "@angular/router"; -import { Observable, firstValueFrom } from "rxjs"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { DialogService } from "@bitwarden/components"; - -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; -import { FamiliesPolicyService } from "../../../../services/families-policy.service"; - -@Component({ - templateUrl: "more-from-bitwarden-page.component.html", - standalone: true, - imports: [CommonModule, JslibModule, RouterModule, PopOutComponent], -}) -export class MoreFromBitwardenPageComponent { - canAccessPremium$: Observable; - protected isFreeFamilyPolicyEnabled$: Observable; - protected hasSingleEnterpriseOrg$: Observable; - - constructor( - private dialogService: DialogService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - private environmentService: EnvironmentService, - private familiesPolicyService: FamiliesPolicyService, - ) { - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; - this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); - this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); - } - - async openFreeBitwardenFamiliesPage() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToWebApp" }, - content: { key: "freeBitwardenFamiliesPageDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - const env = await firstValueFrom(this.environmentService.environment$); - const url = env.getWebVaultUrl(); - await BrowserApi.createNewTab(url + "/#/settings/sponsored-families"); - } - } - - async openBitwardenForBusinessPage() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToBitwardenDotCom" }, - content: { key: "bitwardenForBusinessPageDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - await BrowserApi.createNewTab("https://bitwarden.com/products/business/"); - } - } - - async openAuthenticatorPage() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToBitwardenDotCom" }, - content: { key: "continueToAuthenticatorPageDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - await BrowserApi.createNewTab("https://bitwarden.com/products/authenticator"); - } - } - - async openSecretsManagerPage() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToBitwardenDotCom" }, - content: { key: "continueToSecretsManagerPageDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - await BrowserApi.createNewTab("https://bitwarden.com/products/secrets-manager"); - } - } - - async openPasswordlessDotDevPage() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToBitwardenDotCom" }, - content: { key: "continueToPasswordlessDotDevPageDesc" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - await BrowserApi.createNewTab("https://bitwarden.com/products/passwordless"); - } - } -} diff --git a/apps/browser/src/tools/popup/settings/settings.component.html b/apps/browser/src/tools/popup/settings/settings.component.html deleted file mode 100644 index c547229653e..00000000000 --- a/apps/browser/src/tools/popup/settings/settings.component.html +++ /dev/null @@ -1,63 +0,0 @@ - -
-

- {{ "settings" | i18n }} -

-
- -
-
-
-
-
- - - - - - -
-
-
diff --git a/apps/browser/src/tools/popup/settings/settings.component.ts b/apps/browser/src/tools/popup/settings/settings.component.ts deleted file mode 100644 index 973efc72038..00000000000 --- a/apps/browser/src/tools/popup/settings/settings.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from "@angular/core"; - -@Component({ - selector: "tools-settings", - templateUrl: "settings.component.html", -}) -export class SettingsComponent { - constructor() {} -} From c09f65afceb725203ec206e0208134cc896d2414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:00:36 +0000 Subject: [PATCH 21/57] [PM-15903] Fix icon alignment for bulk remove, bulk delete, and individual delete buttons in the members component (#12437) --- .../organizations/members/members.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 94ee97edc19..52315d30177 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -148,7 +148,7 @@ *ngIf="showBulkRemoveUsers" > - + {{ "remove" | i18n }} @@ -159,7 +159,7 @@ *ngIf="showBulkDeleteUsers" > - + {{ "delete" | i18n }} @@ -358,7 +358,7 @@ (click)="deleteUser(u)" > - + {{ "delete" | i18n }} From 1d874b447e61bb15825df3e9aadf5558a7ede9de Mon Sep 17 00:00:00 2001 From: Github Actions Date: Tue, 17 Dec 2024 16:25:12 +0000 Subject: [PATCH 22/57] Bumped Desktop client to 2024.12.2 --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 101e968ad6d..99953603e45 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.12.1", + "version": "2024.12.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 201f563db2d..7abf1d93928 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.12.1", + "version": "2024.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.12.1", + "version": "2024.12.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 29ee5dc47ef..91d4f4fca99 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.12.1", + "version": "2024.12.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 1ffc977a089..997cad9551e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -230,7 +230,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.12.1", + "version": "2024.12.2", "hasInstallScript": true, "license": "GPL-3.0" }, From d1fe72a4ab37bd87836e8a2ad03c6c66999c03de Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:34:30 +0100 Subject: [PATCH 23/57] Fix the maxlength org name bug (#12397) --- apps/web/src/locales/en/messages.json | 3 +++ .../providers/clients/create-client-dialog.component.html | 8 ++++++++ .../providers/clients/create-client-dialog.component.ts | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index abc3aa3a1d7..36143682fa2 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9969,5 +9969,8 @@ }, "domainClaimed": { "message": "Domain claimed" + }, + "organizationNameMaxLength": { + "message": "Organization name cannot exceed 50 characters." } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html index a08f5710f1e..78f2cb41bef 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html @@ -43,6 +43,14 @@

{{ planCard.name }}

{{ "organizationName" | i18n }} + + {{ "organizationNameMaxLength" | i18n }} + diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index 18910491a0c..2a27b1b32f3 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -47,7 +47,7 @@ export class CreateClientDialogComponent implements OnInit { protected discountPercentage: number; protected formGroup = new FormGroup({ clientOwnerEmail: new FormControl("", [Validators.required, Validators.email]), - organizationName: new FormControl("", [Validators.required]), + organizationName: new FormControl("", [Validators.required, Validators.maxLength(50)]), seats: new FormControl(null, [Validators.required, Validators.min(1)]), }); protected loading = true; From e2e9a7c345e0594efbf258f17f76d9dd6ecf83ae Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Tue, 17 Dec 2024 20:51:58 +0100 Subject: [PATCH 24/57] [PM-15483] PasswordXP-CSV-Importer: Add support for German and Dutch headers (#12216) * Add tests to verify importing German and Dutch headers work * Add method to translate the headers from (German/Dutch into English) while the CSV data is being parsed * Report "importFormatError" when header translation did not work, instead of a generic undefined error (startsWith) * Move passwordxp-csv-importer into a dedicated folder * Introduce files with the language header translations --------- Co-authored-by: Daniel James Smith --- .../spec/passwordxp-csv-importer.spec.ts | 80 ++++++++++++------- .../test-data/passwordxp-csv/dutch-headers.ts | 7 ++ .../passwordxp-csv/german-headers.ts | 7 ++ libs/importer/src/importers/index.ts | 2 +- .../importers/passsordxp/dutch-csv-headers.ts | 10 +++ .../passsordxp/german-csv-headers.ts | 11 +++ .../passwordxp-csv-importer.ts | 39 +++++++-- 7 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts create mode 100644 libs/importer/spec/test-data/passwordxp-csv/german-headers.ts create mode 100644 libs/importer/src/importers/passsordxp/dutch-csv-headers.ts create mode 100644 libs/importer/src/importers/passsordxp/german-csv-headers.ts rename libs/importer/src/importers/{ => passsordxp}/passwordxp-csv-importer.ts (68%) diff --git a/libs/importer/spec/passwordxp-csv-importer.spec.ts b/libs/importer/spec/passwordxp-csv-importer.spec.ts index f707b1138c5..fda323450c6 100644 --- a/libs/importer/spec/passwordxp-csv-importer.spec.ts +++ b/libs/importer/spec/passwordxp-csv-importer.spec.ts @@ -3,10 +3,46 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { PasswordXPCsvImporter } from "../src/importers"; import { ImportResult } from "../src/models/import-result"; +import { dutchHeaders } from "./test-data/passwordxp-csv/dutch-headers"; +import { germanHeaders } from "./test-data/passwordxp-csv/german-headers"; import { noFolder } from "./test-data/passwordxp-csv/no-folder.csv"; import { withFolders } from "./test-data/passwordxp-csv/passwordxp-with-folders.csv"; import { withoutFolders } from "./test-data/passwordxp-csv/passwordxp-without-folders.csv"; +async function importLoginWithCustomFields(importer: PasswordXPCsvImporter, csvData: string) { + const result: ImportResult = await importer.parse(csvData); + expect(result.success).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.name).toBe("Title2"); + expect(cipher.notes).toBe("Test Notes"); + expect(cipher.login.username).toBe("Username2"); + expect(cipher.login.password).toBe("12345678"); + expect(cipher.login.uris[0].uri).toBe("http://URL2.com"); + + expect(cipher.fields.length).toBe(5); + let field = cipher.fields.shift(); + expect(field.name).toBe("Account"); + expect(field.value).toBe("Account2"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Modified"); + expect(field.value).toBe("27-3-2024 08:11:21"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Created"); + expect(field.value).toBe("27-3-2024 08:11:21"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Expire on"); + expect(field.value).toBe("27-5-2024 08:11:21"); + + field = cipher.fields.shift(); + expect(field.name).toBe("Modified by"); + expect(field.value).toBe("someone"); +} + describe("PasswordXPCsvImporter", () => { let importer: PasswordXPCsvImporter; @@ -20,6 +56,12 @@ describe("PasswordXPCsvImporter", () => { expect(result.success).toBe(false); }); + it("should return success false if CSV headers did not get translated", async () => { + const data = germanHeaders.replace("Titel;", "UnknownTitle;"); + const result: ImportResult = await importer.parse(data); + expect(result.success).toBe(false); + }); + it("should skip rows starting with >>>", async () => { const result: ImportResult = await importer.parse(noFolder); expect(result.success).toBe(true); @@ -61,38 +103,16 @@ describe("PasswordXPCsvImporter", () => { expect(cipher.login.uris[0].uri).toBe("http://test"); }); - it("should parse CSV data and import unmapped columns as custom fields", async () => { - const result: ImportResult = await importer.parse(withoutFolders); - expect(result.success).toBe(true); - - const cipher = result.ciphers.shift(); - expect(cipher.type).toBe(CipherType.Login); - expect(cipher.name).toBe("Title2"); - expect(cipher.notes).toBe("Test Notes"); - expect(cipher.login.username).toBe("Username2"); - expect(cipher.login.password).toBe("12345678"); - expect(cipher.login.uris[0].uri).toBe("http://URL2.com"); - - expect(cipher.fields.length).toBe(5); - let field = cipher.fields.shift(); - expect(field.name).toBe("Account"); - expect(field.value).toBe("Account2"); - - field = cipher.fields.shift(); - expect(field.name).toBe("Modified"); - expect(field.value).toBe("27-3-2024 08:11:21"); - - field = cipher.fields.shift(); - expect(field.name).toBe("Created"); - expect(field.value).toBe("27-3-2024 08:11:21"); + it("should parse CSV data with English headers and import unmapped columns as custom fields", async () => { + await importLoginWithCustomFields(importer, withoutFolders); + }); - field = cipher.fields.shift(); - expect(field.name).toBe("Expire on"); - expect(field.value).toBe("27-5-2024 08:11:21"); + it("should parse CSV data with German headers and import unmapped columns as custom fields", async () => { + await importLoginWithCustomFields(importer, germanHeaders); + }); - field = cipher.fields.shift(); - expect(field.name).toBe("Modified by"); - expect(field.value).toBe("someone"); + it("should parse CSV data with Dutch headers and import unmapped columns as custom fields", async () => { + await importLoginWithCustomFields(importer, dutchHeaders); }); it("should parse CSV data with folders and assign items to them", async () => { diff --git a/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts b/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts new file mode 100644 index 00000000000..9cab04f1e6d --- /dev/null +++ b/libs/importer/spec/test-data/passwordxp-csv/dutch-headers.ts @@ -0,0 +1,7 @@ +export const dutchHeaders = `Titel;Gebruikersnaam;Account;URL;Wachtwoord;Gewijzigd;Gemaakt;Verloopt op;Beschrijving;Gewijzigd door +>>> +Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;27-5-2024 08:11:21;Test Notes;someone +Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;Test Notes 2; +Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;Test Notes Certicate 1; +test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;Test Notes 3; +`; diff --git a/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts b/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts new file mode 100644 index 00000000000..a6ac21c76d6 --- /dev/null +++ b/libs/importer/spec/test-data/passwordxp-csv/german-headers.ts @@ -0,0 +1,7 @@ +export const germanHeaders = `Titel;Benutzername;Konto;URL;Passwort;Geändert am;Erstellt am;Läuft ab am;Beschreibung;Geändert von +>>> +Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;27-5-2024 08:11:21;Test Notes;someone +Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;Test Notes 2; +Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;Test Notes Certicate 1; +test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;Test Notes 3; +`; diff --git a/libs/importer/src/importers/index.ts b/libs/importer/src/importers/index.ts index 19b22cfa80d..1ba3a0d9eb8 100644 --- a/libs/importer/src/importers/index.ts +++ b/libs/importer/src/importers/index.ts @@ -45,7 +45,7 @@ export { PasswordBossJsonImporter } from "./passwordboss-json-importer"; export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer"; export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer"; export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer"; -export { PasswordXPCsvImporter } from "./passwordxp-csv-importer"; +export { PasswordXPCsvImporter } from "./passsordxp/passwordxp-csv-importer"; export { ProtonPassJsonImporter } from "./protonpass/protonpass-json-importer"; export { PsonoJsonImporter } from "./psono/psono-json-importer"; export { RememBearCsvImporter } from "./remembear-csv-importer"; diff --git a/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts b/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts new file mode 100644 index 00000000000..7f9c219de56 --- /dev/null +++ b/libs/importer/src/importers/passsordxp/dutch-csv-headers.ts @@ -0,0 +1,10 @@ +export const dutchHeaderTranslations: { [key: string]: string } = { + Titel: "Title", + Gebruikersnaam: "Username", + Wachtwoord: "Password", + Gewijzigd: "Modified", + Gemaakt: "Created", + "Verloopt op": "Expire on", + Beschrijving: "Description", + "Gewijzigd door": "Modified by", +}; diff --git a/libs/importer/src/importers/passsordxp/german-csv-headers.ts b/libs/importer/src/importers/passsordxp/german-csv-headers.ts new file mode 100644 index 00000000000..584ad0badca --- /dev/null +++ b/libs/importer/src/importers/passsordxp/german-csv-headers.ts @@ -0,0 +1,11 @@ +export const germanHeaderTranslations: { [key: string]: string } = { + Titel: "Title", + Benutzername: "Username", + Konto: "Account", + Passwort: "Password", + "Geändert am": "Modified", + "Erstellt am": "Created", + "Läuft ab am": "Expire on", + Beschreibung: "Description", + "Geändert von": "Modified by", +}; diff --git a/libs/importer/src/importers/passwordxp-csv-importer.ts b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts similarity index 68% rename from libs/importer/src/importers/passwordxp-csv-importer.ts rename to libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts index 461432e98d4..226a284ec91 100644 --- a/libs/importer/src/importers/passwordxp-csv-importer.ts +++ b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.ts @@ -1,12 +1,28 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { ImportResult } from "../models/import-result"; - -import { BaseImporter } from "./base-importer"; -import { Importer } from "./importer"; +import { ImportResult } from "../../models/import-result"; +import { BaseImporter } from "../base-importer"; +import { Importer } from "../importer"; const _mappedColumns = new Set(["Title", "Username", "URL", "Password", "Description"]); +import { dutchHeaderTranslations } from "./dutch-csv-headers"; +import { germanHeaderTranslations } from "./german-csv-headers"; + +/* Translates the headers from non-English to English + * This is necessary because the parser only maps English headers to ciphers + * Currently only supports German and Dutch translations + */ +function translateIntoEnglishHeaders(header: string): string { + const translations: { [key: string]: string } = { + // The header column 'User name' is parsed by the parser, but cannot be used as a variable. This converts it to a valid variable name, prior to parsing. + "User name": "Username", + ...germanHeaderTranslations, + ...dutchHeaderTranslations, + }; + + return translations[header] || header; +} /** * PasswordXP CSV importer @@ -17,15 +33,22 @@ export class PasswordXPCsvImporter extends BaseImporter implements Importer { * @param data */ parse(data: string): Promise { - // The header column 'User name' is parsed by the parser, but cannot be used as a variable. This converts it to a valid variable name, prior to parsing. - data = data.replace(";User name;", ";Username;"); - const result = new ImportResult(); - const results = this.parseCsv(data, true, { skipEmptyLines: true }); + const results = this.parseCsv(data, true, { + skipEmptyLines: true, + transformHeader: translateIntoEnglishHeaders, + }); if (results == null) { result.success = false; return Promise.resolve(result); } + + // If the first row (header check) does not contain the column "Title", then the data is invalid (no translation found) + if (!results[0].Title) { + result.success = false; + return Promise.resolve(result); + } + let currentFolderName = ""; results.forEach((row) => { // Skip rows starting with '>>>' as they indicate items following have no folder assigned to them From c3f58b2e70e3576dfa0a5634086d10a368760ce6 Mon Sep 17 00:00:00 2001 From: Evan Bassler Date: Tue, 17 Dec 2024 13:55:28 -0600 Subject: [PATCH 25/57] fix large icon (#11896) Co-authored-by: Evan Bassler Co-authored-by: Matt Bishop --- .../src/autofill/popup/settings/notifications.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/autofill/popup/settings/notifications.component.html b/apps/browser/src/autofill/popup/settings/notifications.component.html index 86fe4923df8..c6446012d0c 100644 --- a/apps/browser/src/autofill/popup/settings/notifications.component.html +++ b/apps/browser/src/autofill/popup/settings/notifications.component.html @@ -50,7 +50,7 @@

{{ "vaultSaveOptionsTitle" | i18n }}

{{ "excludedDomains" | i18n }} - + From ac13cf7ce6fb23e1a81ead38008e4011e5217c5b Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:48:37 -0800 Subject: [PATCH 26/57] feat(auth): [PM-15945] Add logout option to TDE approval page (#12445) This PR adds a logout option to the TDE approval screen. A TDE user on this page cannot use the "Back" button or click the Bitwarden logo to navigate back to `/` because the user is currently authenticated, which means that navigating to the `/` route would activate the `redirectGuard` and simply route the user back to `/login-initiated`. So we must log the user out first before routing. Feature Flags: `UnauthenticatedExtensionUIRefresh` ON --- .../login-decryption-options.component.html | 4 ++++ .../login-decryption-options.component.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html index cb340f646f1..b3d218389bf 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html @@ -56,5 +56,9 @@ > {{ "requestAdminApproval" | i18n }} + + diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 070debf2205..5600077c363 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -30,6 +30,7 @@ import { AsyncActionsModule, ButtonModule, CheckboxModule, + DialogService, FormFieldModule, ToastService, TypographyModule, @@ -90,6 +91,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { private apiService: ApiService, private destroyRef: DestroyRef, private deviceTrustService: DeviceTrustServiceAbstraction, + private dialogService: DialogService, private formBuilder: FormBuilder, private i18nService: I18nService, private keyService: KeyService, @@ -298,4 +300,18 @@ export class LoginDecryptionOptionsComponent implements OnInit { this.loginEmailService.setLoginEmail(this.email); await this.router.navigate(["/admin-approval-requested"]); } + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (confirmed) { + this.messagingService.send("logout", { userId: userId }); + } + } } From 5a582dfc6f7311373e7d58034192a76fade3bb0b Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 17 Dec 2024 23:29:48 +0100 Subject: [PATCH 27/57] [CL-135] Migrate component library to standalone components (#12389) * Migrate component library to standalone components * Fix tests --- .../navigation-switcher.component.spec.ts | 11 +++------ .../password-health.component.spec.ts | 3 +-- .../src/async-actions/async-actions.module.ts | 5 +--- .../src/async-actions/bit-action.directive.ts | 1 + .../src/async-actions/bit-submit.directive.ts | 1 + .../async-actions/form-button.directive.ts | 1 + .../components/src/avatar/avatar.component.ts | 3 +++ libs/components/src/avatar/avatar.module.ts | 4 +--- .../src/badge-list/badge-list.component.ts | 6 ++++- .../src/badge-list/badge-list.module.ts | 6 +---- libs/components/src/badge/badge.directive.ts | 1 + libs/components/src/badge/badge.module.ts | 4 +--- .../src/banner/banner.component.spec.ts | 4 +--- .../components/src/banner/banner.component.ts | 6 +++++ libs/components/src/banner/banner.module.ts | 7 +----- .../src/breadcrumbs/breadcrumb.component.ts | 3 +++ .../src/breadcrumbs/breadcrumbs.component.ts | 8 +++++++ .../src/breadcrumbs/breadcrumbs.module.ts | 9 +------- .../components/src/button/button.component.ts | 3 +++ libs/components/src/button/button.module.ts | 4 +--- .../src/checkbox/checkbox.component.ts | 1 + .../src/checkbox/checkbox.module.ts | 7 +----- .../color-password.component.ts | 3 +++ .../color-password/color-password.module.ts | 4 +--- libs/components/src/dialog/dialog.module.ts | 23 ++----------------- .../src/dialog/dialog/dialog.component.ts | 15 ++++++++++++ .../directives/dialog-close.directive.ts | 1 + .../dialog-title-container.directive.ts | 1 + .../simple-configurable-dialog.component.ts | 17 +++++++++++++- .../simple-dialog/simple-dialog.component.ts | 10 +++++++- .../form-control/form-control.component.ts | 6 +++++ .../src/form-control/form-control.module.ts | 6 +---- .../src/form-control/hint.component.ts | 1 + .../src/form-field/error-summary.component.ts | 5 ++++ .../src/form-field/error.component.ts | 1 + .../src/form-field/form-field.component.ts | 4 ++++ .../src/form-field/form-field.module.ts | 17 +++++++------- .../password-input-toggle.directive.ts | 1 + .../src/form-field/prefix.directive.ts | 1 + .../src/form-field/suffix.directive.ts | 1 + .../src/icon-button/icon-button.component.ts | 3 +++ .../src/icon-button/icon-button.module.ts | 4 +--- libs/components/src/icon/icon.component.ts | 1 + .../src/icon/icon.components.spec.ts | 2 +- libs/components/src/icon/icon.module.ts | 4 +--- libs/components/src/input/input.directive.ts | 1 + libs/components/src/input/input.module.ts | 4 +--- libs/components/src/link/link.directive.ts | 2 ++ libs/components/src/link/link.module.ts | 4 +--- .../src/menu/menu-divider.component.ts | 1 + .../src/menu/menu-item.directive.ts | 3 +++ .../src/menu/menu-trigger-for.directive.ts | 1 + libs/components/src/menu/menu.component.ts | 4 +++- libs/components/src/menu/menu.module.ts | 6 +---- libs/components/src/menu/menu.stories.ts | 16 ++++--------- .../multi-select/multi-select.component.ts | 15 ++++++++++-- .../src/multi-select/multi-select.module.ts | 9 +------- .../src/navigation/nav-divider.component.ts | 3 +++ .../src/navigation/nav-group.component.ts | 12 +++++++++- .../src/navigation/nav-item.component.ts | 14 +++++++++-- .../src/navigation/nav-logo.component.ts | 6 +++++ .../src/navigation/navigation.module.ts | 19 --------------- .../src/navigation/side-nav.component.ts | 8 +++++++ .../src/no-items/no-items.component.ts | 3 +++ .../src/no-items/no-items.module.ts | 6 +---- .../src/progress/progress.component.ts | 3 +++ .../src/progress/progress.module.ts | 4 +--- .../radio-button/radio-button.component.ts | 5 ++++ .../src/radio-button/radio-button.module.ts | 5 +--- .../src/radio-button/radio-group.component.ts | 4 ++++ .../src/radio-button/radio-input.component.ts | 1 + .../components/src/search/search.component.ts | 11 ++++++++- libs/components/src/search/search.module.ts | 7 +----- .../components/src/select/option.component.ts | 1 + .../components/src/select/select.component.ts | 13 +++++++++-- libs/components/src/select/select.module.ts | 6 +---- libs/components/src/shared/i18n.pipe.ts | 1 + libs/components/src/shared/shared.module.ts | 3 +-- libs/components/src/table/cell.directive.ts | 1 + libs/components/src/table/row.directive.ts | 1 + .../src/table/sortable.component.ts | 3 +++ .../src/table/table-scroll.component.ts | 17 ++++++++++++++ libs/components/src/table/table.component.ts | 4 ++++ libs/components/src/table/table.module.ts | 6 +++-- .../src/tabs/shared/tab-header.component.ts | 1 + .../shared/tab-list-container.directive.ts | 1 + .../tabs/shared/tab-list-item.directive.ts | 5 +++- .../src/tabs/tab-group/tab-body.component.ts | 4 +++- .../src/tabs/tab-group/tab-group.component.ts | 12 ++++++++++ .../src/tabs/tab-group/tab-label.directive.ts | 1 + .../src/tabs/tab-group/tab.component.ts | 1 + .../tabs/tab-nav-bar/tab-link.component.ts | 4 +++- .../tabs/tab-nav-bar/tab-nav-bar.component.ts | 5 ++++ libs/components/src/tabs/tabs.module.ts | 16 ++++--------- libs/components/src/toast/toast.module.ts | 5 +--- libs/components/src/toast/toastr.component.ts | 4 ++++ .../toggle-group/toggle-group.component.ts | 1 + .../src/toggle-group/toggle-group.module.ts | 6 +---- .../src/toggle-group/toggle.component.ts | 3 +++ .../src/typography/typography.directive.ts | 1 + .../src/typography/typography.module.ts | 4 +--- 101 files changed, 325 insertions(+), 211 deletions(-) diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index a07f56db2d7..382ce8e026b 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -6,7 +6,7 @@ import { BehaviorSubject } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { BitIconButtonComponent } from "@bitwarden/components/src/icon-button/icon-button.component"; +import { IconButtonModule, NavigationModule } from "@bitwarden/components"; import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component"; import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service"; @@ -45,13 +45,8 @@ describe("NavigationProductSwitcherComponent", () => { mockProducts$.next({ bento: [], other: [] }); await TestBed.configureTestingModule({ - imports: [RouterModule], - declarations: [ - NavigationProductSwitcherComponent, - NavItemComponent, - BitIconButtonComponent, - I18nPipe, - ], + imports: [RouterModule, NavigationModule, IconButtonModule], + declarations: [NavigationProductSwitcherComponent, I18nPipe], providers: [ { provide: ProductSwitcherService, useValue: productSwitcherService }, { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts index 98637d0decb..1f1756731f6 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -13,7 +13,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; -import { TableBodyDirective } from "@bitwarden/components/src/table/table.component"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; @@ -27,7 +26,7 @@ describe("PasswordHealthComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], - declarations: [TableBodyDirective], + declarations: [], providers: [ { provide: CipherService, useValue: mock() }, { provide: I18nService, useValue: mock() }, diff --git a/libs/components/src/async-actions/async-actions.module.ts b/libs/components/src/async-actions/async-actions.module.ts index 8ff1deb2784..bff4286f890 100644 --- a/libs/components/src/async-actions/async-actions.module.ts +++ b/libs/components/src/async-actions/async-actions.module.ts @@ -1,14 +1,11 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../shared"; - import { BitActionDirective } from "./bit-action.directive"; import { BitSubmitDirective } from "./bit-submit.directive"; import { BitFormButtonDirective } from "./form-button.directive"; @NgModule({ - imports: [SharedModule], - declarations: [BitActionDirective, BitFormButtonDirective, BitSubmitDirective], + imports: [BitActionDirective, BitFormButtonDirective, BitSubmitDirective], exports: [BitActionDirective, BitFormButtonDirective, BitSubmitDirective], }) export class AsyncActionsModule {} diff --git a/libs/components/src/async-actions/bit-action.directive.ts b/libs/components/src/async-actions/bit-action.directive.ts index 32ac73f418d..3e793ae2ecd 100644 --- a/libs/components/src/async-actions/bit-action.directive.ts +++ b/libs/components/src/async-actions/bit-action.directive.ts @@ -15,6 +15,7 @@ import { FunctionReturningAwaitable, functionToObservable } from "../utils/funct */ @Directive({ selector: "[bitAction]", + standalone: true, }) export class BitActionDirective implements OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/async-actions/bit-submit.directive.ts b/libs/components/src/async-actions/bit-submit.directive.ts index 838d78af8b2..a38e76aaca6 100644 --- a/libs/components/src/async-actions/bit-submit.directive.ts +++ b/libs/components/src/async-actions/bit-submit.directive.ts @@ -14,6 +14,7 @@ import { FunctionReturningAwaitable, functionToObservable } from "../utils/funct */ @Directive({ selector: "[formGroup][bitSubmit]", + standalone: true, }) export class BitSubmitDirective implements OnInit, OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/async-actions/form-button.directive.ts b/libs/components/src/async-actions/form-button.directive.ts index e4685188693..7c92865b984 100644 --- a/libs/components/src/async-actions/form-button.directive.ts +++ b/libs/components/src/async-actions/form-button.directive.ts @@ -25,6 +25,7 @@ import { BitSubmitDirective } from "./bit-submit.directive"; */ @Directive({ selector: "button[bitFormButton]", + standalone: true, }) export class BitFormButtonDirective implements OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index e1758d795d6..76ff702e88b 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf, NgClass } from "@angular/common"; import { Component, Input, OnChanges } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @@ -18,6 +19,8 @@ const SizeClasses: Record = { @Component({ selector: "bit-avatar", template: ``, + standalone: true, + imports: [NgIf, NgClass], }) export class AvatarComponent implements OnChanges { @Input() border = false; diff --git a/libs/components/src/avatar/avatar.module.ts b/libs/components/src/avatar/avatar.module.ts index ea78ff3a1d2..4ef0978cbec 100644 --- a/libs/components/src/avatar/avatar.module.ts +++ b/libs/components/src/avatar/avatar.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { AvatarComponent } from "./avatar.component"; @NgModule({ - imports: [CommonModule], + imports: [AvatarComponent], exports: [AvatarComponent], - declarations: [AvatarComponent], }) export class AvatarModule {} diff --git a/libs/components/src/badge-list/badge-list.component.ts b/libs/components/src/badge-list/badge-list.component.ts index 9270e5e1238..ac8cb3281ab 100644 --- a/libs/components/src/badge-list/badge-list.component.ts +++ b/libs/components/src/badge-list/badge-list.component.ts @@ -1,12 +1,16 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges } from "@angular/core"; -import { BadgeVariant } from "../badge"; +import { BadgeModule, BadgeVariant } from "../badge"; +import { I18nPipe } from "../shared/i18n.pipe"; @Component({ selector: "bit-badge-list", templateUrl: "badge-list.component.html", + standalone: true, + imports: [CommonModule, BadgeModule, I18nPipe], }) export class BadgeListComponent implements OnChanges { private _maxItems: number; diff --git a/libs/components/src/badge-list/badge-list.module.ts b/libs/components/src/badge-list/badge-list.module.ts index d2a4ce211b1..9359fe2c5c5 100644 --- a/libs/components/src/badge-list/badge-list.module.ts +++ b/libs/components/src/badge-list/badge-list.module.ts @@ -1,13 +1,9 @@ import { NgModule } from "@angular/core"; -import { BadgeModule } from "../badge"; -import { SharedModule } from "../shared"; - import { BadgeListComponent } from "./badge-list.component"; @NgModule({ - imports: [SharedModule, BadgeModule], + imports: [BadgeListComponent], exports: [BadgeListComponent], - declarations: [BadgeListComponent], }) export class BadgeListModule {} diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index f39f8f87639..eef876a664d 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -31,6 +31,7 @@ const hoverStyles: Record = { @Directive({ selector: "span[bitBadge], a[bitBadge], button[bitBadge]", providers: [{ provide: FocusableElement, useExisting: BadgeDirective }], + standalone: true, }) export class BadgeDirective implements FocusableElement { @HostBinding("class") get classList() { diff --git a/libs/components/src/badge/badge.module.ts b/libs/components/src/badge/badge.module.ts index e1b8292363f..e7f3770785a 100644 --- a/libs/components/src/badge/badge.module.ts +++ b/libs/components/src/badge/badge.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BadgeDirective } from "./badge.directive"; @NgModule({ - imports: [CommonModule], + imports: [BadgeDirective], exports: [BadgeDirective], - declarations: [BadgeDirective], }) export class BadgeModule {} diff --git a/libs/components/src/banner/banner.component.spec.ts b/libs/components/src/banner/banner.component.spec.ts index 29f10016a15..2bbc7965642 100644 --- a/libs/components/src/banner/banner.component.spec.ts +++ b/libs/components/src/banner/banner.component.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SharedModule } from "../shared/shared.module"; import { I18nMockService } from "../utils/i18n-mock.service"; import { BannerComponent } from "./banner.component"; @@ -13,8 +12,7 @@ describe("BannerComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SharedModule], - declarations: [BannerComponent], + imports: [BannerComponent], providers: [ { provide: I18nService, diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index 7d59ceb0ee9..d3f64329978 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -1,7 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"; +import { IconButtonModule } from "../icon-button"; +import { I18nPipe } from "../shared/i18n.pipe"; + type BannerTypes = "premium" | "info" | "warning" | "danger"; const defaultIcon: Record = { @@ -14,6 +18,8 @@ const defaultIcon: Record = { @Component({ selector: "bit-banner", templateUrl: "./banner.component.html", + standalone: true, + imports: [CommonModule, IconButtonModule, I18nPipe], }) export class BannerComponent implements OnInit { @Input("bannerType") bannerType: BannerTypes = "info"; diff --git a/libs/components/src/banner/banner.module.ts b/libs/components/src/banner/banner.module.ts index 2c819fbc5b4..3301218ed1a 100644 --- a/libs/components/src/banner/banner.module.ts +++ b/libs/components/src/banner/banner.module.ts @@ -1,14 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { IconButtonModule } from "../icon-button"; -import { SharedModule } from "../shared/shared.module"; - import { BannerComponent } from "./banner.component"; @NgModule({ - imports: [CommonModule, SharedModule, IconButtonModule], + imports: [BannerComponent], exports: [BannerComponent], - declarations: [BannerComponent], }) export class BannerModule {} diff --git a/libs/components/src/breadcrumbs/breadcrumb.component.ts b/libs/components/src/breadcrumbs/breadcrumb.component.ts index d6128540442..ce18bde171f 100644 --- a/libs/components/src/breadcrumbs/breadcrumb.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumb.component.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, EventEmitter, Input, Output, TemplateRef, ViewChild } from "@angular/core"; import { QueryParamsHandling } from "@angular/router"; @Component({ selector: "bit-breadcrumb", templateUrl: "./breadcrumb.component.html", + standalone: true, + imports: [NgIf], }) export class BreadcrumbComponent { @Input() diff --git a/libs/components/src/breadcrumbs/breadcrumbs.component.ts b/libs/components/src/breadcrumbs/breadcrumbs.component.ts index 64ca8146c80..6e8fbf5c25a 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.component.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.component.ts @@ -1,10 +1,18 @@ +import { CommonModule } from "@angular/common"; import { Component, ContentChildren, Input, QueryList } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { IconButtonModule } from "../icon-button"; +import { LinkModule } from "../link"; +import { MenuModule } from "../menu"; import { BreadcrumbComponent } from "./breadcrumb.component"; @Component({ selector: "bit-breadcrumbs", templateUrl: "./breadcrumbs.component.html", + standalone: true, + imports: [CommonModule, LinkModule, RouterModule, IconButtonModule, MenuModule], }) export class BreadcrumbsComponent { @Input() diff --git a/libs/components/src/breadcrumbs/breadcrumbs.module.ts b/libs/components/src/breadcrumbs/breadcrumbs.module.ts index 0812b552f9a..89b57fd19b5 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.module.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.module.ts @@ -1,17 +1,10 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; - -import { IconButtonModule } from "../icon-button"; -import { LinkModule } from "../link"; -import { MenuModule } from "../menu"; import { BreadcrumbComponent } from "./breadcrumb.component"; import { BreadcrumbsComponent } from "./breadcrumbs.component"; @NgModule({ - imports: [CommonModule, LinkModule, IconButtonModule, MenuModule, RouterModule], - declarations: [BreadcrumbsComponent, BreadcrumbComponent], + imports: [BreadcrumbsComponent, BreadcrumbComponent], exports: [BreadcrumbsComponent, BreadcrumbComponent], }) export class BreadcrumbsModule {} diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 67b57d576ab..96311f91529 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass } from "@angular/common"; import { Input, HostBinding, Component } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; @@ -46,6 +47,8 @@ const buttonStyles: Record = { selector: "button[bitButton], a[bitButton]", templateUrl: "button.component.html", providers: [{ provide: ButtonLikeAbstraction, useExisting: ButtonComponent }], + standalone: true, + imports: [NgClass], }) export class ButtonComponent implements ButtonLikeAbstraction { @HostBinding("class") get classList() { diff --git a/libs/components/src/button/button.module.ts b/libs/components/src/button/button.module.ts index 448e7c9dcf6..f1a86eff3ab 100644 --- a/libs/components/src/button/button.module.ts +++ b/libs/components/src/button/button.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ButtonComponent } from "./button.component"; @NgModule({ - imports: [CommonModule], + imports: [ButtonComponent], exports: [ButtonComponent], - declarations: [ButtonComponent], }) export class ButtonModule {} diff --git a/libs/components/src/checkbox/checkbox.component.ts b/libs/components/src/checkbox/checkbox.component.ts index 1ca27e84b82..0ce6f1889b5 100644 --- a/libs/components/src/checkbox/checkbox.component.ts +++ b/libs/components/src/checkbox/checkbox.component.ts @@ -9,6 +9,7 @@ import { BitFormControlAbstraction } from "../form-control"; selector: "input[type=checkbox][bitCheckbox]", template: "", providers: [{ provide: BitFormControlAbstraction, useExisting: CheckboxComponent }], + standalone: true, }) export class CheckboxComponent implements BitFormControlAbstraction { @HostBinding("class") diff --git a/libs/components/src/checkbox/checkbox.module.ts b/libs/components/src/checkbox/checkbox.module.ts index d03b9cf5050..3abfb4b1bfd 100644 --- a/libs/components/src/checkbox/checkbox.module.ts +++ b/libs/components/src/checkbox/checkbox.module.ts @@ -1,14 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { FormControlModule } from "../form-control"; -import { SharedModule } from "../shared"; - import { CheckboxComponent } from "./checkbox.component"; @NgModule({ - imports: [SharedModule, CommonModule, FormControlModule], - declarations: [CheckboxComponent], + imports: [CheckboxComponent], exports: [CheckboxComponent], }) export class CheckboxModule {} diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index 35732760ac7..cbf746e9d73 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgFor, NgIf } from "@angular/common"; import { Component, HostBinding, Input } from "@angular/core"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -23,6 +24,8 @@ enum CharacterType { }} `, preserveWhitespaces: false, + standalone: true, + imports: [NgFor, NgIf], }) export class ColorPasswordComponent { @Input() password: string = null; diff --git a/libs/components/src/color-password/color-password.module.ts b/libs/components/src/color-password/color-password.module.ts index 692c206bb4c..3ebc1c80e12 100644 --- a/libs/components/src/color-password/color-password.module.ts +++ b/libs/components/src/color-password/color-password.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ColorPasswordComponent } from "./color-password.component"; @NgModule({ - imports: [CommonModule], + imports: [ColorPasswordComponent], exports: [ColorPasswordComponent], - declarations: [ColorPasswordComponent], }) export class ColorPasswordModule {} diff --git a/libs/components/src/dialog/dialog.module.ts b/libs/components/src/dialog/dialog.module.ts index bc37f749c05..f31fdd52060 100644 --- a/libs/components/src/dialog/dialog.module.ts +++ b/libs/components/src/dialog/dialog.module.ts @@ -1,44 +1,25 @@ import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog"; import { NgModule } from "@angular/core"; -import { ReactiveFormsModule } from "@angular/forms"; - -import { AsyncActionsModule } from "../async-actions"; -import { ButtonModule } from "../button"; -import { IconButtonModule } from "../icon-button"; -import { SharedModule } from "../shared"; -import { TypographyModule } from "../typography"; import { DialogComponent } from "./dialog/dialog.component"; import { DialogService } from "./dialog.service"; import { DialogCloseDirective } from "./directives/dialog-close.directive"; -import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive"; -import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component"; import { IconDirective, SimpleDialogComponent } from "./simple-dialog/simple-dialog.component"; @NgModule({ imports: [ - SharedModule, - AsyncActionsModule, - ButtonModule, CdkDialogModule, - IconButtonModule, - ReactiveFormsModule, - TypographyModule, - ], - declarations: [ DialogCloseDirective, - DialogTitleContainerDirective, DialogComponent, SimpleDialogComponent, - SimpleConfigurableDialogComponent, IconDirective, ], exports: [ CdkDialogModule, - DialogComponent, - SimpleDialogComponent, DialogCloseDirective, + DialogComponent, IconDirective, + SimpleDialogComponent, ], providers: [DialogService], }) diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 2f901d10d2d..ed47201805a 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -1,14 +1,29 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CommonModule } from "@angular/common"; import { Component, HostBinding, Input } from "@angular/core"; +import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; +import { I18nPipe } from "../../shared/i18n.pipe"; +import { TypographyDirective } from "../../typography/typography.directive"; import { fadeIn } from "../animations"; +import { DialogCloseDirective } from "../directives/dialog-close.directive"; +import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; @Component({ selector: "bit-dialog", templateUrl: "./dialog.component.html", animations: [fadeIn], + standalone: true, + imports: [ + CommonModule, + DialogTitleContainerDirective, + TypographyDirective, + BitIconButtonComponent, + DialogCloseDirective, + I18nPipe, + ], }) export class DialogComponent { /** Background color */ diff --git a/libs/components/src/dialog/directives/dialog-close.directive.ts b/libs/components/src/dialog/directives/dialog-close.directive.ts index 5e44ced7c21..5e5fda3e014 100644 --- a/libs/components/src/dialog/directives/dialog-close.directive.ts +++ b/libs/components/src/dialog/directives/dialog-close.directive.ts @@ -3,6 +3,7 @@ import { Directive, HostBinding, HostListener, Input, Optional } from "@angular/ @Directive({ selector: "[bitDialogClose]", + standalone: true, }) export class DialogCloseDirective { @Input("bitDialogClose") dialogResult: any; diff --git a/libs/components/src/dialog/directives/dialog-title-container.directive.ts b/libs/components/src/dialog/directives/dialog-title-container.directive.ts index e17487f2780..cf46396967b 100644 --- a/libs/components/src/dialog/directives/dialog-title-container.directive.ts +++ b/libs/components/src/dialog/directives/dialog-title-container.directive.ts @@ -6,6 +6,7 @@ let nextId = 0; @Directive({ selector: "[bitDialogTitleContainer]", + standalone: true, }) export class DialogTitleContainerDirective implements OnInit { @HostBinding("id") id = `bit-dialog-title-${nextId++}`; diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts index 29d52e9cf07..60b2e1c3a3f 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -1,12 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { NgIf } from "@angular/common"; import { Component, Inject } from "@angular/core"; -import { FormGroup } from "@angular/forms"; +import { FormGroup, ReactiveFormsModule } from "@angular/forms"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SimpleDialogOptions, SimpleDialogType, Translation } from "../.."; +import { BitSubmitDirective } from "../../../async-actions/bit-submit.directive"; +import { BitFormButtonDirective } from "../../../async-actions/form-button.directive"; +import { ButtonComponent } from "../../../button/button.component"; +import { SimpleDialogComponent, IconDirective } from "../simple-dialog.component"; const DEFAULT_ICON: Record = { primary: "bwi-business", @@ -26,6 +31,16 @@ const DEFAULT_COLOR: Record = { @Component({ templateUrl: "./simple-configurable-dialog.component.html", + standalone: true, + imports: [ + ReactiveFormsModule, + BitSubmitDirective, + SimpleDialogComponent, + IconDirective, + ButtonComponent, + BitFormButtonDirective, + NgIf, + ], }) export class SimpleConfigurableDialogComponent { get iconClasses() { diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts index 912b0299f66..c02a13bd150 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.component.ts @@ -1,14 +1,22 @@ +import { NgIf } from "@angular/common"; import { Component, ContentChild, Directive } from "@angular/core"; +import { TypographyDirective } from "../../typography/typography.directive"; import { fadeIn } from "../animations"; +import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; -@Directive({ selector: "[bitDialogIcon]" }) +@Directive({ + selector: "[bitDialogIcon]", + standalone: true, +}) export class IconDirective {} @Component({ selector: "bit-simple-dialog", templateUrl: "./simple-dialog.component.html", animations: [fadeIn], + standalone: true, + imports: [NgIf, DialogTitleContainerDirective, TypographyDirective], }) export class SimpleDialogComponent { @ContentChild(IconDirective) icon!: IconDirective; diff --git a/libs/components/src/form-control/form-control.component.ts b/libs/components/src/form-control/form-control.component.ts index 6c24e7a53e6..9b87c44157a 100644 --- a/libs/components/src/form-control/form-control.component.ts +++ b/libs/components/src/form-control/form-control.component.ts @@ -1,15 +1,21 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass, NgIf } from "@angular/common"; import { Component, ContentChild, HostBinding, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "../shared/i18n.pipe"; +import { TypographyDirective } from "../typography/typography.directive"; + import { BitFormControlAbstraction } from "./form-control.abstraction"; @Component({ selector: "bit-form-control", templateUrl: "form-control.component.html", + standalone: true, + imports: [NgClass, TypographyDirective, NgIf, I18nPipe], }) export class FormControlComponent { @Input() label: string; diff --git a/libs/components/src/form-control/form-control.module.ts b/libs/components/src/form-control/form-control.module.ts index f6969a97e9c..df168d8e98f 100644 --- a/libs/components/src/form-control/form-control.module.ts +++ b/libs/components/src/form-control/form-control.module.ts @@ -1,15 +1,11 @@ import { NgModule } from "@angular/core"; -import { SharedModule } from "../shared"; -import { TypographyModule } from "../typography"; - import { FormControlComponent } from "./form-control.component"; import { BitHintComponent } from "./hint.component"; import { BitLabel } from "./label.component"; @NgModule({ - imports: [SharedModule, BitLabel, TypographyModule], - declarations: [FormControlComponent, BitHintComponent], + imports: [BitLabel, FormControlComponent, BitHintComponent], exports: [FormControlComponent, BitLabel, BitHintComponent], }) export class FormControlModule {} diff --git a/libs/components/src/form-control/hint.component.ts b/libs/components/src/form-control/hint.component.ts index c1f21bf2545..4fee0d4560f 100644 --- a/libs/components/src/form-control/hint.component.ts +++ b/libs/components/src/form-control/hint.component.ts @@ -8,6 +8,7 @@ let nextId = 0; host: { class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1 tw-text-xs", }, + standalone: true, }) export class BitHintComponent { @HostBinding() id = `bit-hint-${nextId++}`; diff --git a/libs/components/src/form-field/error-summary.component.ts b/libs/components/src/form-field/error-summary.component.ts index f374740b20e..beed32a88ac 100644 --- a/libs/components/src/form-field/error-summary.component.ts +++ b/libs/components/src/form-field/error-summary.component.ts @@ -1,8 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; import { AbstractControl, UntypedFormGroup } from "@angular/forms"; +import { I18nPipe } from "../shared/i18n.pipe"; + @Component({ selector: "bit-error-summary", template: ` @@ -12,6 +15,8 @@ import { AbstractControl, UntypedFormGroup } from "@angular/forms"; class: "tw-block tw-text-danger tw-mt-2", "aria-live": "assertive", }, + standalone: true, + imports: [NgIf, I18nPipe], }) export class BitErrorSummary { @Input() diff --git a/libs/components/src/form-field/error.component.ts b/libs/components/src/form-field/error.component.ts index a0f7906b366..27adbf7d313 100644 --- a/libs/components/src/form-field/error.component.ts +++ b/libs/components/src/form-field/error.component.ts @@ -14,6 +14,7 @@ let nextId = 0; class: "tw-block tw-mt-1 tw-text-danger tw-text-xs", "aria-live": "assertive", }, + standalone: true, }) export class BitErrorComponent { @HostBinding() id = `bit-error-${nextId++}`; diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 6f425e41496..9f41c6cf6ac 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CommonModule } from "@angular/common"; import { AfterContentChecked, booleanAttribute, @@ -16,6 +17,7 @@ import { import { BitHintComponent } from "../form-control/hint.component"; import { BitLabel } from "../form-control/label.component"; import { inputBorderClasses } from "../input/input.directive"; +import { I18nPipe } from "../shared/i18n.pipe"; import { BitErrorComponent } from "./error.component"; import { BitFormFieldControl } from "./form-field-control"; @@ -23,6 +25,8 @@ import { BitFormFieldControl } from "./form-field-control"; @Component({ selector: "bit-form-field", templateUrl: "./form-field.component.html", + standalone: true, + imports: [CommonModule, BitErrorComponent, I18nPipe], }) export class BitFormFieldComponent implements AfterContentChecked { @ContentChild(BitFormFieldControl) input: BitFormFieldControl; diff --git a/libs/components/src/form-field/form-field.module.ts b/libs/components/src/form-field/form-field.module.ts index 989375167d4..88d7ffcc78b 100644 --- a/libs/components/src/form-field/form-field.module.ts +++ b/libs/components/src/form-field/form-field.module.ts @@ -1,11 +1,8 @@ import { NgModule } from "@angular/core"; import { FormControlModule } from "../form-control"; -import { BitInputDirective } from "../input/input.directive"; import { InputModule } from "../input/input.module"; -import { MultiSelectComponent } from "../multi-select/multi-select.component"; import { MultiSelectModule } from "../multi-select/multi-select.module"; -import { SharedModule } from "../shared"; import { BitErrorSummary } from "./error-summary.component"; import { BitErrorComponent } from "./error.component"; @@ -15,8 +12,11 @@ import { BitPrefixDirective } from "./prefix.directive"; import { BitSuffixDirective } from "./suffix.directive"; @NgModule({ - imports: [SharedModule, FormControlModule, InputModule, MultiSelectModule], - declarations: [ + imports: [ + FormControlModule, + InputModule, + MultiSelectModule, + BitErrorComponent, BitErrorSummary, BitFormFieldComponent, @@ -25,15 +25,16 @@ import { BitSuffixDirective } from "./suffix.directive"; BitSuffixDirective, ], exports: [ + FormControlModule, + InputModule, + MultiSelectModule, + BitErrorComponent, BitErrorSummary, BitFormFieldComponent, - BitInputDirective, BitPasswordInputToggleDirective, BitPrefixDirective, BitSuffixDirective, - MultiSelectComponent, - FormControlModule, ], }) export class FormFieldModule {} diff --git a/libs/components/src/form-field/password-input-toggle.directive.ts b/libs/components/src/form-field/password-input-toggle.directive.ts index a696a88c468..933722db5b4 100644 --- a/libs/components/src/form-field/password-input-toggle.directive.ts +++ b/libs/components/src/form-field/password-input-toggle.directive.ts @@ -18,6 +18,7 @@ import { BitFormFieldComponent } from "./form-field.component"; @Directive({ selector: "[bitPasswordInputToggle]", + standalone: true, }) export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges { /** diff --git a/libs/components/src/form-field/prefix.directive.ts b/libs/components/src/form-field/prefix.directive.ts index 34fcbf85233..b44e90cbaad 100644 --- a/libs/components/src/form-field/prefix.directive.ts +++ b/libs/components/src/form-field/prefix.directive.ts @@ -4,6 +4,7 @@ import { BitIconButtonComponent } from "../icon-button/icon-button.component"; @Directive({ selector: "[bitPrefix]", + standalone: true, }) export class BitPrefixDirective implements OnInit { @HostBinding("class") @Input() get classList() { diff --git a/libs/components/src/form-field/suffix.directive.ts b/libs/components/src/form-field/suffix.directive.ts index 28736ce78a9..baf1afce763 100644 --- a/libs/components/src/form-field/suffix.directive.ts +++ b/libs/components/src/form-field/suffix.directive.ts @@ -4,6 +4,7 @@ import { BitIconButtonComponent } from "../icon-button/icon-button.component"; @Directive({ selector: "[bitSuffix]", + standalone: true, }) export class BitSuffixDirective implements OnInit { @HostBinding("class") @Input() get classList() { diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 97016f9fd0c..ac7dff0408b 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgClass } from "@angular/common"; import { Component, ElementRef, HostBinding, Input } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; @@ -134,6 +135,8 @@ const sizes: Record = { { provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }, { provide: FocusableElement, useExisting: BitIconButtonComponent }, ], + standalone: true, + imports: [NgClass], }) export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @Input("bitIconButton") icon: string; diff --git a/libs/components/src/icon-button/icon-button.module.ts b/libs/components/src/icon-button/icon-button.module.ts index fb4e8589717..26f48cdb177 100644 --- a/libs/components/src/icon-button/icon-button.module.ts +++ b/libs/components/src/icon-button/icon-button.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BitIconButtonComponent } from "./icon-button.component"; @NgModule({ - imports: [CommonModule], - declarations: [BitIconButtonComponent], + imports: [BitIconButtonComponent], exports: [BitIconButtonComponent], }) export class IconButtonModule {} diff --git a/libs/components/src/icon/icon.component.ts b/libs/components/src/icon/icon.component.ts index 55615d4dae3..2382d197bec 100644 --- a/libs/components/src/icon/icon.component.ts +++ b/libs/components/src/icon/icon.component.ts @@ -8,6 +8,7 @@ import { Icon, isIcon } from "./icon"; @Component({ selector: "bit-icon", template: ``, + standalone: true, }) export class BitIconComponent { @Input() set icon(icon: Icon) { diff --git a/libs/components/src/icon/icon.components.spec.ts b/libs/components/src/icon/icon.components.spec.ts index 351ed5f0218..7d499cdd419 100644 --- a/libs/components/src/icon/icon.components.spec.ts +++ b/libs/components/src/icon/icon.components.spec.ts @@ -9,7 +9,7 @@ describe("IconComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [BitIconComponent], + imports: [BitIconComponent], }).compileComponents(); fixture = TestBed.createComponent(BitIconComponent); diff --git a/libs/components/src/icon/icon.module.ts b/libs/components/src/icon/icon.module.ts index 32e95fd0468..3d15b5bb3c3 100644 --- a/libs/components/src/icon/icon.module.ts +++ b/libs/components/src/icon/icon.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BitIconComponent } from "./icon.component"; @NgModule({ - imports: [CommonModule], - declarations: [BitIconComponent], + imports: [BitIconComponent], exports: [BitIconComponent], }) export class IconModule {} diff --git a/libs/components/src/input/input.directive.ts b/libs/components/src/input/input.directive.ts index 4a6a03295d4..f6c6c3d542e 100644 --- a/libs/components/src/input/input.directive.ts +++ b/libs/components/src/input/input.directive.ts @@ -30,6 +30,7 @@ export function inputBorderClasses(error: boolean) { @Directive({ selector: "input[bitInput], select[bitInput], textarea[bitInput]", providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }], + standalone: true, }) export class BitInputDirective implements BitFormFieldControl { @HostBinding("class") @Input() get classList() { diff --git a/libs/components/src/input/input.module.ts b/libs/components/src/input/input.module.ts index cfc49cefb7d..9399cb06517 100644 --- a/libs/components/src/input/input.module.ts +++ b/libs/components/src/input/input.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { BitInputDirective } from "./input.directive"; @NgModule({ - imports: [CommonModule], - declarations: [BitInputDirective], + imports: [BitInputDirective], exports: [BitInputDirective], }) export class InputModule {} diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts index b127d80fedf..52aba557661 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.directive.ts @@ -68,6 +68,7 @@ abstract class LinkDirective { @Directive({ selector: "a[bitLink]", + standalone: true, }) export class AnchorLinkDirective extends LinkDirective { @HostBinding("class") get classList() { @@ -79,6 +80,7 @@ export class AnchorLinkDirective extends LinkDirective { @Directive({ selector: "button[bitLink]", + standalone: true, }) export class ButtonLinkDirective extends LinkDirective { @HostBinding("class") get classList() { diff --git a/libs/components/src/link/link.module.ts b/libs/components/src/link/link.module.ts index b8b54d57c00..52d2f29e53c 100644 --- a/libs/components/src/link/link.module.ts +++ b/libs/components/src/link/link.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { AnchorLinkDirective, ButtonLinkDirective } from "./link.directive"; @NgModule({ - imports: [CommonModule], + imports: [AnchorLinkDirective, ButtonLinkDirective], exports: [AnchorLinkDirective, ButtonLinkDirective], - declarations: [AnchorLinkDirective, ButtonLinkDirective], }) export class LinkModule {} diff --git a/libs/components/src/menu/menu-divider.component.ts b/libs/components/src/menu/menu-divider.component.ts index 194506ee50f..55b5c013c93 100644 --- a/libs/components/src/menu/menu-divider.component.ts +++ b/libs/components/src/menu/menu-divider.component.ts @@ -3,5 +3,6 @@ import { Component } from "@angular/core"; @Component({ selector: "bit-menu-divider", templateUrl: "./menu-divider.component.html", + standalone: true, }) export class MenuDividerComponent {} diff --git a/libs/components/src/menu/menu-item.directive.ts b/libs/components/src/menu/menu-item.directive.ts index 5fdc8fabfce..d0975e8e391 100644 --- a/libs/components/src/menu/menu-item.directive.ts +++ b/libs/components/src/menu/menu-item.directive.ts @@ -1,10 +1,13 @@ import { FocusableOption } from "@angular/cdk/a11y"; import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass } from "@angular/common"; import { Component, ElementRef, HostBinding, Input } from "@angular/core"; @Component({ selector: "[bitMenuItem]", templateUrl: "menu-item.component.html", + standalone: true, + imports: [NgClass], }) export class MenuItemDirective implements FocusableOption { @HostBinding("class") classList = [ diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index d318a77ef00..786554e981c 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -19,6 +19,7 @@ import { MenuComponent } from "./menu.component"; @Directive({ selector: "[bitMenuTriggerFor]", exportAs: "menuTrigger", + standalone: true, }) export class MenuTriggerForDirective implements OnDestroy { @HostBinding("attr.aria-expanded") isOpen = false; diff --git a/libs/components/src/menu/menu.component.ts b/libs/components/src/menu/menu.component.ts index f0bf4f81df9..a39dceb4454 100644 --- a/libs/components/src/menu/menu.component.ts +++ b/libs/components/src/menu/menu.component.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { FocusKeyManager } from "@angular/cdk/a11y"; +import { FocusKeyManager, CdkTrapFocus } from "@angular/cdk/a11y"; import { Component, Output, @@ -19,6 +19,8 @@ import { MenuItemDirective } from "./menu-item.directive"; selector: "bit-menu", templateUrl: "./menu.component.html", exportAs: "menuComponent", + standalone: true, + imports: [CdkTrapFocus], }) export class MenuComponent implements AfterContentInit { @ViewChild(TemplateRef) templateRef: TemplateRef; diff --git a/libs/components/src/menu/menu.module.ts b/libs/components/src/menu/menu.module.ts index b165629e6c5..117460df559 100644 --- a/libs/components/src/menu/menu.module.ts +++ b/libs/components/src/menu/menu.module.ts @@ -1,6 +1,3 @@ -import { A11yModule } from "@angular/cdk/a11y"; -import { OverlayModule } from "@angular/cdk/overlay"; -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { MenuDividerComponent } from "./menu-divider.component"; @@ -9,8 +6,7 @@ import { MenuTriggerForDirective } from "./menu-trigger-for.directive"; import { MenuComponent } from "./menu.component"; @NgModule({ - imports: [A11yModule, CommonModule, OverlayModule], - declarations: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent], + imports: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent], exports: [MenuComponent, MenuTriggerForDirective, MenuItemDirective, MenuDividerComponent], }) export class MenuModule {} diff --git a/libs/components/src/menu/menu.stories.ts b/libs/components/src/menu/menu.stories.ts index c5d232b2057..65fafd2d04d 100644 --- a/libs/components/src/menu/menu.stories.ts +++ b/libs/components/src/menu/menu.stories.ts @@ -3,23 +3,15 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { ButtonModule } from "../button/button.module"; -import { MenuDividerComponent } from "./menu-divider.component"; -import { MenuItemDirective } from "./menu-item.directive"; import { MenuTriggerForDirective } from "./menu-trigger-for.directive"; -import { MenuComponent } from "./menu.component"; +import { MenuModule } from "./menu.module"; export default { title: "Component Library/Menu", component: MenuTriggerForDirective, decorators: [ moduleMetadata({ - declarations: [ - MenuTriggerForDirective, - MenuComponent, - MenuItemDirective, - MenuDividerComponent, - ], - imports: [OverlayModule, ButtonModule], + imports: [MenuModule, OverlayModule, ButtonModule], }), ], parameters: { @@ -51,7 +43,7 @@ export const OpenMenu: Story = { Disabled button - +
@@ -67,7 +59,7 @@ export const ClosedMenu: Story = {
- + Anchor link Another link diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index a18d5aa0b60..53e51bfe2f9 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { hasModifierKey } from "@angular/cdk/keycodes"; +import { NgIf } from "@angular/common"; import { Component, Input, @@ -13,12 +14,20 @@ import { Optional, Self, } from "@angular/core"; -import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; -import { NgSelectComponent } from "@ng-select/ng-select"; +import { + ControlValueAccessor, + NgControl, + Validators, + ReactiveFormsModule, + FormsModule, +} from "@angular/forms"; +import { NgSelectComponent, NgSelectModule } from "@ng-select/ng-select"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { BadgeModule } from "../badge"; import { BitFormFieldControl } from "../form-field/form-field-control"; +import { I18nPipe } from "../shared/i18n.pipe"; import { SelectItemView } from "./models/select-item-view"; @@ -29,6 +38,8 @@ let nextId = 0; selector: "bit-multi-select", templateUrl: "./multi-select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }], + standalone: true, + imports: [NgSelectModule, ReactiveFormsModule, FormsModule, BadgeModule, NgIf, I18nPipe], }) /** * This component has been implemented to only support Multi-select list events diff --git a/libs/components/src/multi-select/multi-select.module.ts b/libs/components/src/multi-select/multi-select.module.ts index 88de53b5481..c8cc899db00 100644 --- a/libs/components/src/multi-select/multi-select.module.ts +++ b/libs/components/src/multi-select/multi-select.module.ts @@ -1,16 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; -import { NgSelectModule } from "@ng-select/ng-select"; - -import { BadgeModule } from "../badge"; -import { SharedModule } from "../shared"; import { MultiSelectComponent } from "./multi-select.component"; @NgModule({ - imports: [CommonModule, FormsModule, NgSelectModule, BadgeModule, SharedModule], + imports: [MultiSelectComponent], exports: [MultiSelectComponent], - declarations: [MultiSelectComponent], }) export class MultiSelectModule {} diff --git a/libs/components/src/navigation/nav-divider.component.ts b/libs/components/src/navigation/nav-divider.component.ts index 008d3f46c35..eff381e1c94 100644 --- a/libs/components/src/navigation/nav-divider.component.ts +++ b/libs/components/src/navigation/nav-divider.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { SideNavService } from "./side-nav.service"; @@ -5,6 +6,8 @@ import { SideNavService } from "./side-nav.service"; @Component({ selector: "bit-nav-divider", templateUrl: "./nav-divider.component.html", + standalone: true, + imports: [CommonModule], }) export class NavDividerComponent { constructor(protected sideNavService: SideNavService) {} diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 07494c0b7da..58d93ddd3a4 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from "@angular/common"; import { AfterContentInit, booleanAttribute, @@ -11,13 +12,22 @@ import { SkipSelf, } from "@angular/core"; +import { IconButtonModule } from "../icon-button"; +import { I18nPipe } from "../shared/i18n.pipe"; + import { NavBaseComponent } from "./nav-base.component"; +import { NavGroupAbstraction, NavItemComponent } from "./nav-item.component"; import { SideNavService } from "./side-nav.service"; @Component({ selector: "bit-nav-group", templateUrl: "./nav-group.component.html", - providers: [{ provide: NavBaseComponent, useExisting: NavGroupComponent }], + providers: [ + { provide: NavBaseComponent, useExisting: NavGroupComponent }, + { provide: NavGroupAbstraction, useExisting: NavGroupComponent }, + ], + standalone: true, + imports: [CommonModule, NavItemComponent, IconButtonModule, I18nPipe], }) export class NavGroupComponent extends NavBaseComponent implements AfterContentInit { @ContentChildren(NavBaseComponent, { diff --git a/libs/components/src/navigation/nav-item.component.ts b/libs/components/src/navigation/nav-item.component.ts index 8348638568b..c8d464119ce 100644 --- a/libs/components/src/navigation/nav-item.component.ts +++ b/libs/components/src/navigation/nav-item.component.ts @@ -1,14 +1,24 @@ +import { CommonModule } from "@angular/common"; import { Component, HostListener, Input, Optional } from "@angular/core"; +import { RouterModule } from "@angular/router"; import { BehaviorSubject, map } from "rxjs"; +import { IconButtonModule } from "../icon-button"; + import { NavBaseComponent } from "./nav-base.component"; -import { NavGroupComponent } from "./nav-group.component"; import { SideNavService } from "./side-nav.service"; +// Resolves a circular dependency between `NavItemComponent` and `NavItemGroup` when using standalone components. +export abstract class NavGroupAbstraction { + abstract setOpen(open: boolean): void; +} + @Component({ selector: "bit-nav-item", templateUrl: "./nav-item.component.html", providers: [{ provide: NavBaseComponent, useExisting: NavItemComponent }], + standalone: true, + imports: [CommonModule, IconButtonModule, RouterModule], }) export class NavItemComponent extends NavBaseComponent { /** Forces active styles to be shown, regardless of the `routerLinkActiveOptions` */ @@ -52,7 +62,7 @@ export class NavItemComponent extends NavBaseComponent { constructor( protected sideNavService: SideNavService, - @Optional() private parentNavGroup: NavGroupComponent, + @Optional() private parentNavGroup: NavGroupAbstraction, ) { super(); } diff --git a/libs/components/src/navigation/nav-logo.component.ts b/libs/components/src/navigation/nav-logo.component.ts index cbad5b869e7..8a84970500c 100644 --- a/libs/components/src/navigation/nav-logo.component.ts +++ b/libs/components/src/navigation/nav-logo.component.ts @@ -1,14 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; +import { RouterLinkActive, RouterLink } from "@angular/router"; import { Icon } from "../icon"; +import { BitIconComponent } from "../icon/icon.component"; +import { NavItemComponent } from "./nav-item.component"; import { SideNavService } from "./side-nav.service"; @Component({ selector: "bit-nav-logo", templateUrl: "./nav-logo.component.html", + standalone: true, + imports: [NgIf, RouterLinkActive, RouterLink, BitIconComponent, NavItemComponent], }) export class NavLogoComponent { /** Icon that is displayed when the side nav is closed */ diff --git a/libs/components/src/navigation/navigation.module.ts b/libs/components/src/navigation/navigation.module.ts index 852bd1c0a25..a08fbaddb98 100644 --- a/libs/components/src/navigation/navigation.module.ts +++ b/libs/components/src/navigation/navigation.module.ts @@ -1,13 +1,4 @@ -import { A11yModule } from "@angular/cdk/a11y"; -import { OverlayModule } from "@angular/cdk/overlay"; -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; - -import { IconModule } from "../icon"; -import { IconButtonModule } from "../icon-button/icon-button.module"; -import { LinkModule } from "../link"; -import { SharedModule } from "../shared/shared.module"; import { NavDividerComponent } from "./nav-divider.component"; import { NavGroupComponent } from "./nav-group.component"; @@ -17,16 +8,6 @@ import { SideNavComponent } from "./side-nav.component"; @NgModule({ imports: [ - CommonModule, - SharedModule, - IconButtonModule, - OverlayModule, - RouterModule, - IconModule, - A11yModule, - LinkModule, - ], - declarations: [ NavDividerComponent, NavGroupComponent, NavItemComponent, diff --git a/libs/components/src/navigation/side-nav.component.ts b/libs/components/src/navigation/side-nav.component.ts index a4af51772b3..c86a517100f 100644 --- a/libs/components/src/navigation/side-nav.component.ts +++ b/libs/components/src/navigation/side-nav.component.ts @@ -1,7 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { CdkTrapFocus } from "@angular/cdk/a11y"; +import { CommonModule } from "@angular/common"; import { Component, ElementRef, Input, ViewChild } from "@angular/core"; +import { BitIconButtonComponent } from "../icon-button/icon-button.component"; +import { I18nPipe } from "../shared/i18n.pipe"; + +import { NavDividerComponent } from "./nav-divider.component"; import { SideNavService } from "./side-nav.service"; export type SideNavVariant = "primary" | "secondary"; @@ -9,6 +15,8 @@ export type SideNavVariant = "primary" | "secondary"; @Component({ selector: "bit-side-nav", templateUrl: "side-nav.component.html", + standalone: true, + imports: [CommonModule, CdkTrapFocus, NavDividerComponent, BitIconButtonComponent, I18nPipe], }) export class SideNavComponent { @Input() variant: SideNavVariant = "primary"; diff --git a/libs/components/src/no-items/no-items.component.ts b/libs/components/src/no-items/no-items.component.ts index d85c6a34571..ee9e0ee0581 100644 --- a/libs/components/src/no-items/no-items.component.ts +++ b/libs/components/src/no-items/no-items.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from "@angular/core"; import { Icons } from ".."; +import { BitIconComponent } from "../icon/icon.component"; /** * Component for displaying a message when there are no items to display. Expects title, description and button slots. @@ -8,6 +9,8 @@ import { Icons } from ".."; @Component({ selector: "bit-no-items", templateUrl: "./no-items.component.html", + standalone: true, + imports: [BitIconComponent], }) export class NoItemsComponent { @Input() icon = Icons.Search; diff --git a/libs/components/src/no-items/no-items.module.ts b/libs/components/src/no-items/no-items.module.ts index 9fe6eb37aa9..49c3c73f133 100644 --- a/libs/components/src/no-items/no-items.module.ts +++ b/libs/components/src/no-items/no-items.module.ts @@ -1,13 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { IconModule } from "../icon"; - import { NoItemsComponent } from "./no-items.component"; @NgModule({ - imports: [CommonModule, IconModule], + imports: [NoItemsComponent], exports: [NoItemsComponent], - declarations: [NoItemsComponent], }) export class NoItemsModule {} diff --git a/libs/components/src/progress/progress.component.ts b/libs/components/src/progress/progress.component.ts index 37206dc6ae4..04e535158b1 100644 --- a/libs/components/src/progress/progress.component.ts +++ b/libs/components/src/progress/progress.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; type SizeTypes = "small" | "default" | "large"; @@ -19,6 +20,8 @@ const BackgroundClasses: Record = { @Component({ selector: "bit-progress", templateUrl: "./progress.component.html", + standalone: true, + imports: [CommonModule], }) export class ProgressComponent { @Input() barWidth = 0; diff --git a/libs/components/src/progress/progress.module.ts b/libs/components/src/progress/progress.module.ts index 8ab09189d19..cc93c4c3bd0 100644 --- a/libs/components/src/progress/progress.module.ts +++ b/libs/components/src/progress/progress.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { ProgressComponent } from "./progress.component"; @NgModule({ - imports: [CommonModule], + imports: [ProgressComponent], exports: [ProgressComponent], - declarations: [ProgressComponent], }) export class ProgressModule {} diff --git a/libs/components/src/radio-button/radio-button.component.ts b/libs/components/src/radio-button/radio-button.component.ts index dc294103d42..042a54edf47 100644 --- a/libs/components/src/radio-button/radio-button.component.ts +++ b/libs/components/src/radio-button/radio-button.component.ts @@ -1,12 +1,17 @@ import { Component, HostBinding, Input } from "@angular/core"; +import { FormControlModule } from "../form-control/form-control.module"; + import { RadioGroupComponent } from "./radio-group.component"; +import { RadioInputComponent } from "./radio-input.component"; let nextId = 0; @Component({ selector: "bit-radio-button", templateUrl: "radio-button.component.html", + standalone: true, + imports: [FormControlModule, RadioInputComponent], }) export class RadioButtonComponent { @HostBinding("attr.id") @Input() id = `bit-radio-button-${nextId++}`; diff --git a/libs/components/src/radio-button/radio-button.module.ts b/libs/components/src/radio-button/radio-button.module.ts index 21fd9427046..7b05c27b4ff 100644 --- a/libs/components/src/radio-button/radio-button.module.ts +++ b/libs/components/src/radio-button/radio-button.module.ts @@ -1,16 +1,13 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormControlModule } from "../form-control"; -import { SharedModule } from "../shared"; import { RadioButtonComponent } from "./radio-button.component"; import { RadioGroupComponent } from "./radio-group.component"; import { RadioInputComponent } from "./radio-input.component"; @NgModule({ - imports: [CommonModule, SharedModule, FormControlModule], - declarations: [RadioInputComponent, RadioButtonComponent, RadioGroupComponent], + imports: [FormControlModule, RadioInputComponent, RadioButtonComponent, RadioGroupComponent], exports: [FormControlModule, RadioInputComponent, RadioButtonComponent, RadioGroupComponent], }) export class RadioButtonModule {} diff --git a/libs/components/src/radio-button/radio-group.component.ts b/libs/components/src/radio-button/radio-group.component.ts index 2cddb4fb7bc..b9e48f46445 100644 --- a/libs/components/src/radio-button/radio-group.component.ts +++ b/libs/components/src/radio-button/radio-group.component.ts @@ -1,15 +1,19 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf, NgTemplateOutlet } from "@angular/common"; import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core"; import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; import { BitLabel } from "../form-control/label.component"; +import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; @Component({ selector: "bit-radio-group", templateUrl: "radio-group.component.html", + standalone: true, + imports: [NgIf, NgTemplateOutlet, I18nPipe], }) export class RadioGroupComponent implements ControlValueAccessor { selected: unknown; diff --git a/libs/components/src/radio-button/radio-input.component.ts b/libs/components/src/radio-button/radio-input.component.ts index 580e5bca25e..4a9f5dede60 100644 --- a/libs/components/src/radio-button/radio-input.component.ts +++ b/libs/components/src/radio-button/radio-input.component.ts @@ -11,6 +11,7 @@ let nextId = 0; selector: "input[type=radio][bitRadio]", template: "", providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }], + standalone: true, }) export class RadioInputComponent implements BitFormControlAbstraction { @HostBinding("attr.id") @Input() id = `bit-radio-input-${nextId++}`; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index bc98e5a293b..6ec79eaa84e 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,11 +1,18 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, ElementRef, Input, ViewChild } from "@angular/core"; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + ReactiveFormsModule, + FormsModule, +} from "@angular/forms"; import { isBrowserSafariApi } from "@bitwarden/platform"; +import { InputModule } from "../input/input.module"; import { FocusableElement } from "../shared/focusable-element"; +import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; @@ -23,6 +30,8 @@ let nextId = 0; useExisting: SearchComponent, }, ], + standalone: true, + imports: [InputModule, ReactiveFormsModule, FormsModule, I18nPipe], }) export class SearchComponent implements ControlValueAccessor, FocusableElement { private notifyOnChange: (v: string) => void; diff --git a/libs/components/src/search/search.module.ts b/libs/components/src/search/search.module.ts index 62072774900..cb9761eae6b 100644 --- a/libs/components/src/search/search.module.ts +++ b/libs/components/src/search/search.module.ts @@ -1,14 +1,9 @@ import { NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; - -import { InputModule } from "../input/input.module"; -import { SharedModule } from "../shared"; import { SearchComponent } from "./search.component"; @NgModule({ - imports: [SharedModule, InputModule, FormsModule], - declarations: [SearchComponent], + imports: [SearchComponent], exports: [SearchComponent], }) export class SearchModule {} diff --git a/libs/components/src/select/option.component.ts b/libs/components/src/select/option.component.ts index b32b124be25..841ceda3648 100644 --- a/libs/components/src/select/option.component.ts +++ b/libs/components/src/select/option.component.ts @@ -7,6 +7,7 @@ import { Option } from "./option"; @Component({ selector: "bit-option", template: ``, + standalone: true, }) export class OptionComponent implements Option { @Input() diff --git a/libs/components/src/select/select.component.ts b/libs/components/src/select/select.component.ts index cdcf794e489..8f75c5be42b 100644 --- a/libs/components/src/select/select.component.ts +++ b/libs/components/src/select/select.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgIf } from "@angular/common"; import { Component, ContentChildren, @@ -12,8 +13,14 @@ import { Output, EventEmitter, } from "@angular/core"; -import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; -import { NgSelectComponent } from "@ng-select/ng-select"; +import { + ControlValueAccessor, + NgControl, + Validators, + ReactiveFormsModule, + FormsModule, +} from "@angular/forms"; +import { NgSelectComponent, NgSelectModule } from "@ng-select/ng-select"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -28,6 +35,8 @@ let nextId = 0; selector: "bit-select", templateUrl: "select.component.html", providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }], + standalone: true, + imports: [NgSelectModule, ReactiveFormsModule, FormsModule, NgIf], }) export class SelectComponent implements BitFormFieldControl, ControlValueAccessor { @ViewChild(NgSelectComponent) select: NgSelectComponent; diff --git a/libs/components/src/select/select.module.ts b/libs/components/src/select/select.module.ts index 4391a518174..8807ed63a48 100644 --- a/libs/components/src/select/select.module.ts +++ b/libs/components/src/select/select.module.ts @@ -1,14 +1,10 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { FormsModule } from "@angular/forms"; -import { NgSelectModule } from "@ng-select/ng-select"; import { OptionComponent } from "./option.component"; import { SelectComponent } from "./select.component"; @NgModule({ - imports: [CommonModule, NgSelectModule, FormsModule], - declarations: [SelectComponent, OptionComponent], + imports: [SelectComponent, OptionComponent], exports: [SelectComponent, OptionComponent], }) export class SelectModule {} diff --git a/libs/components/src/shared/i18n.pipe.ts b/libs/components/src/shared/i18n.pipe.ts index f428d9297c0..91bf0b3198d 100644 --- a/libs/components/src/shared/i18n.pipe.ts +++ b/libs/components/src/shared/i18n.pipe.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic */ @Pipe({ name: "i18n", + standalone: true, }) export class I18nPipe implements PipeTransform { constructor(private i18nService: I18nService) {} diff --git a/libs/components/src/shared/shared.module.ts b/libs/components/src/shared/shared.module.ts index dcf2e2bc05f..253b049f8fe 100644 --- a/libs/components/src/shared/shared.module.ts +++ b/libs/components/src/shared/shared.module.ts @@ -4,8 +4,7 @@ import { NgModule } from "@angular/core"; import { I18nPipe } from "./i18n.pipe"; @NgModule({ - imports: [CommonModule], - declarations: [I18nPipe], + imports: [CommonModule, I18nPipe], exports: [CommonModule, I18nPipe], }) export class SharedModule {} diff --git a/libs/components/src/table/cell.directive.ts b/libs/components/src/table/cell.directive.ts index 61c75571063..8928fe7c095 100644 --- a/libs/components/src/table/cell.directive.ts +++ b/libs/components/src/table/cell.directive.ts @@ -2,6 +2,7 @@ import { Directive, HostBinding } from "@angular/core"; @Directive({ selector: "th[bitCell], td[bitCell]", + standalone: true, }) export class CellDirective { @HostBinding("class") get classList() { diff --git a/libs/components/src/table/row.directive.ts b/libs/components/src/table/row.directive.ts index 19f3d3f775b..23347224af9 100644 --- a/libs/components/src/table/row.directive.ts +++ b/libs/components/src/table/row.directive.ts @@ -2,6 +2,7 @@ import { Directive, HostBinding, Input } from "@angular/core"; @Directive({ selector: "tr[bitRow]", + standalone: true, }) export class RowDirective { @Input() alignContent: "top" | "middle" | "bottom" | "baseline" = "middle"; diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index dc3d8dc14f0..d3309c03aa9 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { coerceBooleanProperty } from "@angular/cdk/coercion"; +import { NgClass } from "@angular/common"; import { Component, HostBinding, Input, OnInit } from "@angular/core"; import type { SortDirection, SortFn } from "./table-data-source"; @@ -14,6 +15,8 @@ import { TableComponent } from "./table.component"; `, + standalone: true, + imports: [NgClass], }) export class SortableComponent implements OnInit { /** diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts index 9e308b7da59..34cd8c5d9ca 100644 --- a/libs/components/src/table/table-scroll.component.ts +++ b/libs/components/src/table/table-scroll.component.ts @@ -1,5 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { + CdkVirtualScrollViewport, + CdkVirtualScrollableWindow, + CdkFixedSizeVirtualScroll, + CdkVirtualForOf, +} from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; import { AfterContentChecked, Component, @@ -14,6 +21,7 @@ import { TrackByFunction, } from "@angular/core"; +import { RowDirective } from "./row.directive"; import { TableComponent } from "./table.component"; /** @@ -42,6 +50,15 @@ export class BitRowDef { selector: "bit-table-scroll", templateUrl: "./table-scroll.component.html", providers: [{ provide: TableComponent, useExisting: TableScrollComponent }], + standalone: true, + imports: [ + CommonModule, + CdkVirtualScrollViewport, + CdkVirtualScrollableWindow, + CdkFixedSizeVirtualScroll, + CdkVirtualForOf, + RowDirective, + ], }) export class TableScrollComponent extends TableComponent diff --git a/libs/components/src/table/table.component.ts b/libs/components/src/table/table.component.ts index 8bc7754b16b..cd0a2a6c65e 100644 --- a/libs/components/src/table/table.component.ts +++ b/libs/components/src/table/table.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { isDataSource } from "@angular/cdk/collections"; +import { CommonModule } from "@angular/common"; import { AfterContentChecked, Component, @@ -16,6 +17,7 @@ import { TableDataSource } from "./table-data-source"; @Directive({ selector: "ng-template[body]", + standalone: true, }) export class TableBodyDirective { // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility @@ -25,6 +27,8 @@ export class TableBodyDirective { @Component({ selector: "bit-table", templateUrl: "./table.component.html", + standalone: true, + imports: [CommonModule], }) export class TableComponent implements OnDestroy, AfterContentChecked { @Input() dataSource: TableDataSource; diff --git a/libs/components/src/table/table.module.ts b/libs/components/src/table/table.module.ts index 1f1b705c69e..68993612772 100644 --- a/libs/components/src/table/table.module.ts +++ b/libs/components/src/table/table.module.ts @@ -9,8 +9,10 @@ import { BitRowDef, TableScrollComponent } from "./table-scroll.component"; import { TableBodyDirective, TableComponent } from "./table.component"; @NgModule({ - imports: [CommonModule, ScrollingModule, BitRowDef], - declarations: [ + imports: [ + CommonModule, + ScrollingModule, + BitRowDef, CellDirective, RowDirective, SortableComponent, diff --git a/libs/components/src/tabs/shared/tab-header.component.ts b/libs/components/src/tabs/shared/tab-header.component.ts index 4712df0549a..c45bafb3d52 100644 --- a/libs/components/src/tabs/shared/tab-header.component.ts +++ b/libs/components/src/tabs/shared/tab-header.component.ts @@ -10,5 +10,6 @@ import { Component } from "@angular/core"; "tw-h-16 tw-pl-4 tw-bg-background-alt tw-flex tw-items-end tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300", }, template: ``, + standalone: true, }) export class TabHeaderComponent {} diff --git a/libs/components/src/tabs/shared/tab-list-container.directive.ts b/libs/components/src/tabs/shared/tab-list-container.directive.ts index 1cf8a762d58..cedae44e582 100644 --- a/libs/components/src/tabs/shared/tab-list-container.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-container.directive.ts @@ -8,5 +8,6 @@ import { Directive } from "@angular/core"; host: { class: "tw-inline-flex tw-flex-wrap tw-leading-5", }, + standalone: true, }) export class TabListContainerDirective {} diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts index 7514f5417e6..87435133a23 100644 --- a/libs/components/src/tabs/shared/tab-list-item.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -7,7 +7,10 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; * Directive used for styling tab header items for both nav links (anchor tags) * and content tabs (button tags) */ -@Directive({ selector: "[bitTabListItem]" }) +@Directive({ + selector: "[bitTabListItem]", + standalone: true, +}) export class TabListItemDirective implements FocusableOption { @Input() active: boolean; @Input() disabled: boolean; diff --git a/libs/components/src/tabs/tab-group/tab-body.component.ts b/libs/components/src/tabs/tab-group/tab-body.component.ts index 7cb6664b7c5..45a6a05e7c2 100644 --- a/libs/components/src/tabs/tab-group/tab-body.component.ts +++ b/libs/components/src/tabs/tab-group/tab-body.component.ts @@ -1,11 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TemplatePortal } from "@angular/cdk/portal"; +import { TemplatePortal, CdkPortalOutlet } from "@angular/cdk/portal"; import { Component, HostBinding, Input } from "@angular/core"; @Component({ selector: "bit-tab-body", templateUrl: "tab-body.component.html", + standalone: true, + imports: [CdkPortalOutlet], }) export class TabBodyComponent { private _firstRender: boolean; diff --git a/libs/components/src/tabs/tab-group/tab-group.component.ts b/libs/components/src/tabs/tab-group/tab-group.component.ts index 7b0cb60bb12..54d00343b38 100644 --- a/libs/components/src/tabs/tab-group/tab-group.component.ts +++ b/libs/components/src/tabs/tab-group/tab-group.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { FocusKeyManager } from "@angular/cdk/a11y"; import { coerceNumberProperty } from "@angular/cdk/coercion"; +import { CommonModule } from "@angular/common"; import { AfterContentChecked, AfterContentInit, @@ -17,8 +18,11 @@ import { } from "@angular/core"; import { Subject, takeUntil } from "rxjs"; +import { TabHeaderComponent } from "../shared/tab-header.component"; +import { TabListContainerDirective } from "../shared/tab-list-container.directive"; import { TabListItemDirective } from "../shared/tab-list-item.directive"; +import { TabBodyComponent } from "./tab-body.component"; import { TabComponent } from "./tab.component"; /** Used to generate unique ID's for each tab component */ @@ -27,6 +31,14 @@ let nextId = 0; @Component({ selector: "bit-tab-group", templateUrl: "./tab-group.component.html", + standalone: true, + imports: [ + CommonModule, + TabHeaderComponent, + TabListContainerDirective, + TabListItemDirective, + TabBodyComponent, + ], }) export class TabGroupComponent implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy diff --git a/libs/components/src/tabs/tab-group/tab-label.directive.ts b/libs/components/src/tabs/tab-group/tab-label.directive.ts index 45da163631b..9a0e59845a1 100644 --- a/libs/components/src/tabs/tab-group/tab-label.directive.ts +++ b/libs/components/src/tabs/tab-group/tab-label.directive.ts @@ -16,6 +16,7 @@ import { Directive, TemplateRef } from "@angular/core"; */ @Directive({ selector: "[bitTabLabel]", + standalone: true, }) export class TabLabelDirective { constructor(public templateRef: TemplateRef) {} diff --git a/libs/components/src/tabs/tab-group/tab.component.ts b/libs/components/src/tabs/tab-group/tab.component.ts index 260cb0c8193..b2c9b1999bc 100644 --- a/libs/components/src/tabs/tab-group/tab.component.ts +++ b/libs/components/src/tabs/tab-group/tab.component.ts @@ -19,6 +19,7 @@ import { TabLabelDirective } from "./tab-label.directive"; host: { role: "tabpanel", }, + standalone: true, }) export class TabComponent implements OnInit { @Input() disabled = false; diff --git a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts index 483aa9600b3..0dac6681475 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts +++ b/libs/components/src/tabs/tab-nav-bar/tab-link.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { FocusableOption } from "@angular/cdk/a11y"; import { AfterViewInit, Component, HostListener, Input, OnDestroy, ViewChild } from "@angular/core"; -import { IsActiveMatchOptions, RouterLinkActive } from "@angular/router"; +import { IsActiveMatchOptions, RouterLinkActive, RouterModule } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; import { TabListItemDirective } from "../shared/tab-list-item.directive"; @@ -12,6 +12,8 @@ import { TabNavBarComponent } from "./tab-nav-bar.component"; @Component({ selector: "bit-tab-link", templateUrl: "tab-link.component.html", + standalone: true, + imports: [TabListItemDirective, RouterModule], }) export class TabLinkComponent implements FocusableOption, AfterViewInit, OnDestroy { private destroy$ = new Subject(); diff --git a/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts b/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts index 81f7f1d4947..305196a0c69 100644 --- a/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts +++ b/libs/components/src/tabs/tab-nav-bar/tab-nav-bar.component.ts @@ -10,6 +10,9 @@ import { QueryList, } from "@angular/core"; +import { TabHeaderComponent } from "../shared/tab-header.component"; +import { TabListContainerDirective } from "../shared/tab-list-container.directive"; + import { TabLinkComponent } from "./tab-link.component"; @Component({ @@ -18,6 +21,8 @@ import { TabLinkComponent } from "./tab-link.component"; host: { class: "tw-block", }, + standalone: true, + imports: [TabHeaderComponent, TabListContainerDirective], }) export class TabNavBarComponent implements AfterContentInit { @ContentChildren(forwardRef(() => TabLinkComponent)) tabLabels: QueryList; diff --git a/libs/components/src/tabs/tabs.module.ts b/libs/components/src/tabs/tabs.module.ts index fee1a8a7d08..ef1537db67e 100644 --- a/libs/components/src/tabs/tabs.module.ts +++ b/libs/components/src/tabs/tabs.module.ts @@ -1,11 +1,6 @@ -import { PortalModule } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { RouterModule } from "@angular/router"; -import { TabHeaderComponent } from "./shared/tab-header.component"; -import { TabListContainerDirective } from "./shared/tab-list-container.directive"; -import { TabListItemDirective } from "./shared/tab-list-item.directive"; import { TabBodyComponent } from "./tab-group/tab-body.component"; import { TabGroupComponent } from "./tab-group/tab-group.component"; import { TabLabelDirective } from "./tab-group/tab-label.directive"; @@ -14,24 +9,21 @@ import { TabLinkComponent } from "./tab-nav-bar/tab-link.component"; import { TabNavBarComponent } from "./tab-nav-bar/tab-nav-bar.component"; @NgModule({ - imports: [CommonModule, RouterModule, PortalModule], - exports: [ + imports: [ + CommonModule, TabGroupComponent, TabComponent, TabLabelDirective, TabNavBarComponent, TabLinkComponent, + TabBodyComponent, ], - declarations: [ + exports: [ TabGroupComponent, TabComponent, TabLabelDirective, - TabListContainerDirective, - TabListItemDirective, - TabHeaderComponent, TabNavBarComponent, TabLinkComponent, - TabBodyComponent, ], }) export class TabsModule {} diff --git a/libs/components/src/toast/toast.module.ts b/libs/components/src/toast/toast.module.ts index bf39a0be9ad..bf17fde223f 100644 --- a/libs/components/src/toast/toast.module.ts +++ b/libs/components/src/toast/toast.module.ts @@ -1,13 +1,10 @@ -import { CommonModule } from "@angular/common"; import { ModuleWithProviders, NgModule } from "@angular/core"; import { DefaultNoComponentGlobalConfig, GlobalConfig, TOAST_CONFIG } from "ngx-toastr"; -import { ToastComponent } from "./toast.component"; import { BitwardenToastrComponent } from "./toastr.component"; @NgModule({ - imports: [CommonModule, ToastComponent], - declarations: [BitwardenToastrComponent], + imports: [BitwardenToastrComponent], exports: [BitwardenToastrComponent], }) export class ToastModule { diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts index 0656b68d863..24209054948 100644 --- a/libs/components/src/toast/toastr.component.ts +++ b/libs/components/src/toast/toastr.component.ts @@ -2,6 +2,8 @@ import { animate, state, style, transition, trigger } from "@angular/animations" import { Component } from "@angular/core"; import { Toast as BaseToastrComponent } from "ngx-toastr"; +import { ToastComponent } from "./toast.component"; + @Component({ template: ` { private id = nextId++; diff --git a/libs/components/src/toggle-group/toggle-group.module.ts b/libs/components/src/toggle-group/toggle-group.module.ts index fe1ce0ec52f..654149611f0 100644 --- a/libs/components/src/toggle-group/toggle-group.module.ts +++ b/libs/components/src/toggle-group/toggle-group.module.ts @@ -1,14 +1,10 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { BadgeModule } from "../badge"; - import { ToggleGroupComponent } from "./toggle-group.component"; import { ToggleComponent } from "./toggle.component"; @NgModule({ - imports: [CommonModule, BadgeModule], + imports: [ToggleGroupComponent, ToggleComponent], exports: [ToggleGroupComponent, ToggleComponent], - declarations: [ToggleGroupComponent, ToggleComponent], }) export class ToggleGroupModule {} diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts index c7d9dc5bf38..7bd62056763 100644 --- a/libs/components/src/toggle-group/toggle.component.ts +++ b/libs/components/src/toggle-group/toggle.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { NgClass } from "@angular/common"; import { AfterContentChecked, AfterViewInit, @@ -19,6 +20,8 @@ let nextId = 0; selector: "bit-toggle", templateUrl: "./toggle.component.html", preserveWhitespaces: false, + standalone: true, + imports: [NgClass], }) export class ToggleComponent implements AfterContentChecked, AfterViewInit { id = nextId++; diff --git a/libs/components/src/typography/typography.directive.ts b/libs/components/src/typography/typography.directive.ts index e48ef67001f..36d6b996dbe 100644 --- a/libs/components/src/typography/typography.directive.ts +++ b/libs/components/src/typography/typography.directive.ts @@ -31,6 +31,7 @@ const margins: Record = { @Directive({ selector: "[bitTypography]", + standalone: true, }) export class TypographyDirective { @Input("bitTypography") bitTypography: TypographyType; diff --git a/libs/components/src/typography/typography.module.ts b/libs/components/src/typography/typography.module.ts index 7ee66906360..74d1d4d6e6a 100644 --- a/libs/components/src/typography/typography.module.ts +++ b/libs/components/src/typography/typography.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { TypographyDirective } from "./typography.directive"; @NgModule({ - imports: [CommonModule], + imports: [TypographyDirective], exports: [TypographyDirective], - declarations: [TypographyDirective], }) export class TypographyModule {} From 903b5c8d93bb3d19d5960f0eb60ca20b7d604c05 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 18 Dec 2024 05:33:14 -0800 Subject: [PATCH 28/57] [PM-16116] Add comment about mv2 routing loop (#12447) * Add comment about mv2 routing loop * Add more details to comment --- apps/browser/src/popup/app-routing.module.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 85ae861c9d5..3ec2667cd8c 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -661,6 +661,10 @@ const routes: Routes = [ * This ensures that in a passkey flow the `/fido2?` URL does not get * overwritten in the `BrowserRouterService` by the `/lockV2` route. This way, after * unlocking, the user can be redirected back to the `/fido2?` URL. + * + * Also, this prevents a routing loop when using biometrics to unlock the vault in MV2 (Firefox), + * locking up the browser (https://bitwarden.atlassian.net/browse/PM-16116). This involves the + * `popup-router-cache.service` pushing the `lockV2` route to the history. */ doNotSaveUrl: true, } satisfies ExtensionAnonLayoutWrapperData & RouteDataProperties, From 12b698b11dfd40ad951dc3243e14edefe9cd6a04 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:31:16 +0100 Subject: [PATCH 29/57] organization status changed code changes (#12249) * organization status changed code changes * Remove the stop so a reconnect can be made --- apps/web/src/app/app.component.ts | 14 ++++++++++++++ .../adjust-payment-dialog.component.ts | 1 - libs/common/src/enums/notification-type.enum.ts | 1 + .../src/models/response/notification.response.ts | 14 ++++++++++++++ libs/common/src/services/notifications.service.ts | 5 +++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index a1ebd141e1d..1075655af9e 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -243,6 +243,20 @@ export class AppComponent implements OnDestroy, OnInit { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/remove-password"]); break; + case "syncOrganizationStatusChanged": { + const { organizationId, enabled } = message; + const organizations = await firstValueFrom(this.organizationService.organizations$); + const organization = organizations.find((org) => org.id === organizationId); + + if (organization) { + const updatedOrganization = { + ...organization, + enabled: enabled, + }; + await this.organizationService.upsert(updatedOrganization); + } + break; + } default: break; } diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index 1cc9f5b4e02..b8826f0626a 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -76,7 +76,6 @@ export class AdjustPaymentDialogComponent { } }); await response; - await new Promise((resolve) => setTimeout(resolve, 10000)); this.toastService.showToast({ variant: "success", title: null, diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index c5853cbe2c0..69cbdff9dd2 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -22,4 +22,5 @@ export enum NotificationType { AuthRequestResponse = 16, SyncOrganizations = 17, + SyncOrganizationStatusChanged = 18, } diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index af79b883f08..473e6fc1d10 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -42,6 +42,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.AuthRequestResponse: this.payload = new AuthRequestPushNotification(payload); break; + case NotificationType.SyncOrganizationStatusChanged: + this.payload = new OrganizationStatusPushNotification(payload); + break; default: break; } @@ -112,3 +115,14 @@ export class AuthRequestPushNotification extends BaseResponse { this.userId = this.getResponseProperty("UserId"); } } + +export class OrganizationStatusPushNotification extends BaseResponse { + organizationId: string; + enabled: boolean; + + constructor(response: any) { + super(response); + this.organizationId = this.getResponseProperty("OrganizationId"); + this.enabled = this.getResponseProperty("Enabled"); + } +} diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index e240886cf29..6f7c5c9f262 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -218,6 +218,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { }); } break; + case NotificationType.SyncOrganizationStatusChanged: + if (isAuthenticated) { + await this.syncService.fullSync(true); + } + break; default: break; } From 12eb77fd45c18a078454b62428f42c8be2947fca Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:13:13 +0100 Subject: [PATCH 30/57] [PM-13455] Wire up results from RiskInsightsReportService (#12206) * Risk insights aggregation in a new service. Initial PR. * Wire up results from RiskInsightsReportService * Ignoring all non-login items and refactoring into a method * Cleaning up the documentation a little * logic for generating the report summary * application summary to list at risk applications not passwords * Adding more documentation and moving types to it's own file * Awaiting the raw data report and adding the start of the test file * Extend access-intelligence.module to provide needed services * Register access-intelligence.module with bit-web AppModule * Use provided RiskInsightsService instead of new'ing one in the component * Removing the injectable attribute from RiskInsightsReportService * Fix tests * Adding more test cases * Removing unnecessary file * Test cases update * Fixing memeber details test to have new member * Fixing password health tests * Moving to observables * removing commented code * commented code * Switching from ternary to if/else * nullable types * one more nullable type * Adding the fixme for strict types * moving the fixme * No need to access the password use map and switching to the observable * PM-13455 fixes to unit tests --------- Co-authored-by: Tom Co-authored-by: Daniel James Smith Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com> Co-authored-by: voommen-livefront --- .../risk-insights-report.service.spec.ts | 56 ++++++++----------- .../services/risk-insights-report.service.ts | 5 +- .../bit-web/src/app/app.module.ts | 2 + .../access-intelligence.module.ts | 24 ++++++++ .../password-health.component.html | 10 ++-- .../password-health.component.spec.ts | 22 +------- .../password-health.component.ts | 41 +++----------- 7 files changed, 64 insertions(+), 96 deletions(-) diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts index 7505b692a8f..705eb1231a9 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -1,5 +1,6 @@ -import { TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; +import { ZXCVBNResult } from "zxcvbn"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -12,42 +13,31 @@ import { RiskInsightsReportService } from "./risk-insights-report.service"; describe("RiskInsightsReportService", () => { let service: RiskInsightsReportService; + const pwdStrengthService = mock(); + const auditService = mock(); + const cipherService = mock(); + const memberCipherDetailsService = mock(); beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - RiskInsightsReportService, - { - provide: PasswordStrengthServiceAbstraction, - useValue: { - getPasswordStrength: (password: string) => { - const score = password.length < 4 ? 1 : 4; - return { score }; - }, - }, - }, - { - provide: AuditService, - useValue: { - passwordLeaked: (password: string) => Promise.resolve(password === "123" ? 100 : 0), - }, - }, - { - provide: CipherService, - useValue: { - getAllFromApiForOrganization: jest.fn().mockResolvedValue(mockCiphers), - }, - }, - { - provide: MemberCipherDetailsApiService, - useValue: { - getMemberCipherDetails: jest.fn().mockResolvedValue(mockMemberCipherDetails), - }, - }, - ], + pwdStrengthService.getPasswordStrength.mockImplementation((password: string) => { + const score = password.length < 4 ? 1 : 4; + return { score } as ZXCVBNResult; }); - service = TestBed.inject(RiskInsightsReportService); + auditService.passwordLeaked.mockImplementation((password: string) => + Promise.resolve(password === "123" ? 100 : 0), + ); + + cipherService.getAllFromApiForOrganization.mockResolvedValue(mockCiphers); + + memberCipherDetailsService.getMemberCipherDetails.mockResolvedValue(mockMemberCipherDetails); + + service = new RiskInsightsReportService( + pwdStrengthService, + auditService, + cipherService, + memberCipherDetailsService, + ); }); it("should generate the raw data report correctly", async () => { diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts index f4b30735584..45746cd2f5a 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts @@ -1,7 +1,5 @@ -// FIXME: Update this file to be type safe and remove this and next line +// FIXME: Update this file to be type safe // @ts-strict-ignore - -import { Injectable } from "@angular/core"; import { concatMap, first, from, map, Observable, zip } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -24,7 +22,6 @@ import { import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; -@Injectable() export class RiskInsightsReportService { constructor( private passwordStrengthService: PasswordStrengthServiceAbstraction, diff --git a/bitwarden_license/bit-web/src/app/app.module.ts b/bitwarden_license/bit-web/src/app/app.module.ts index 4db1e2f5e20..fd1a3b0b84c 100644 --- a/bitwarden_license/bit-web/src/app/app.module.ts +++ b/bitwarden_license/bit-web/src/app/app.module.ts @@ -20,6 +20,7 @@ import { MaximumVaultTimeoutPolicyComponent } from "./admin-console/policies/max import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { FreeFamiliesSponsorshipPolicyComponent } from "./billing/policies/free-families-sponsorship.component"; +import { AccessIntelligenceModule } from "./tools/access-intelligence/access-intelligence.module"; /** * This is the AppModule for the commercial version of Bitwarden. @@ -41,6 +42,7 @@ import { FreeFamiliesSponsorshipPolicyComponent } from "./billing/policies/free- AppRoutingModule, OssRoutingModule, OrganizationsModule, // Must be after OssRoutingModule for competing routes to resolve properly + AccessIntelligenceModule, RouterModule, WildcardRoutingModule, // Needs to be last to catch all non-existing routes ], diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts index 3f177119aa8..87b75dee70c 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts @@ -1,9 +1,33 @@ import { NgModule } from "@angular/core"; +import { + MemberCipherDetailsApiService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights/services"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module"; import { RiskInsightsComponent } from "./risk-insights.component"; @NgModule({ imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule], + providers: [ + { + provide: MemberCipherDetailsApiService, + deps: [ApiService], + }, + { + provide: RiskInsightsReportService, + deps: [ + PasswordStrengthServiceAbstraction, + AuditService, + CipherService, + MemberCipherDetailsApiService, + ], + }, + ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html index 5b1fe4610d9..aeaa9f33197 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.html @@ -34,10 +34,10 @@ - {{ passwordStrengthMap.get(r.id)[0] | i18n }} + {{ r.weakPasswordDetail?.detailValue.label | i18n }} @@ -46,8 +46,8 @@ - - {{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }} + + {{ "exposedXTimes" | i18n: r.exposedPasswordDetail?.exposedXTimes }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts index 1f1756731f6..4329cfbde14 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -3,15 +3,8 @@ import { ActivatedRoute, convertToParamMap } from "@angular/router"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { - MemberCipherDetailsApiService, - PasswordHealthService, -} from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { RiskInsightsReportService } from "@bitwarden/bit-common/tools/reports/risk-insights"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TableModule } from "@bitwarden/components"; import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; @@ -28,19 +21,8 @@ describe("PasswordHealthComponent", () => { imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], declarations: [], providers: [ - { provide: CipherService, useValue: mock() }, + { provide: RiskInsightsReportService, useValue: mock() }, { provide: I18nService, useValue: mock() }, - { provide: AuditService, useValue: mock() }, - { provide: ApiService, useValue: mock() }, - { provide: MemberCipherDetailsApiService, useValue: mock() }, - { - provide: PasswordStrengthServiceAbstraction, - useValue: mock(), - }, - { - provide: PasswordHealthService, - useValue: mock(), - }, { provide: ActivatedRoute, useValue: { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts index 06f7de439cf..62d543a080d 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/password-health.component.ts @@ -4,21 +4,14 @@ import { CommonModule } from "@angular/common"; import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; -import { map } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - MemberCipherDetailsApiService, - PasswordHealthService, -} from "@bitwarden/bit-common/tools/reports/risk-insights"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { RiskInsightsReportService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { CipherHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeModule, - BadgeVariant, ContainerComponent, TableDataSource, TableModule, @@ -41,28 +34,19 @@ import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pip HeaderModule, TableModule, ], - providers: [PasswordHealthService, MemberCipherDetailsApiService], }) export class PasswordHealthComponent implements OnInit { - passwordStrengthMap = new Map(); - passwordUseMap = new Map(); - - exposedPasswordMap = new Map(); - - dataSource = new TableDataSource(); + dataSource = new TableDataSource(); loading = true; private destroyRef = inject(DestroyRef); constructor( - protected cipherService: CipherService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, - protected auditService: AuditService, + protected riskInsightsReportService: RiskInsightsReportService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, - protected memberCipherDetailsApiService: MemberCipherDetailsApiService, ) {} ngOnInit() { @@ -78,20 +62,9 @@ export class PasswordHealthComponent implements OnInit { } async setCiphers(organizationId: string) { - const passwordHealthService = new PasswordHealthService( - this.passwordStrengthService, - this.auditService, - this.cipherService, - this.memberCipherDetailsApiService, - organizationId, + this.dataSource.data = await firstValueFrom( + this.riskInsightsReportService.generateRawDataReport$(organizationId), ); - - await passwordHealthService.generateReport(); - - this.dataSource.data = passwordHealthService.reportCiphers; - this.exposedPasswordMap = passwordHealthService.exposedPasswordMap; - this.passwordStrengthMap = passwordHealthService.passwordStrengthMap; - this.passwordUseMap = passwordHealthService.passwordUseMap; this.loading = false; } } From fc37d6df6b888dc649023a08ddd02de400adf66a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 18 Dec 2024 16:41:40 +0000 Subject: [PATCH 31/57] Bumped client version(s) --- apps/browser/package.json | 2 +- apps/browser/src/manifest.json | 2 +- apps/browser/src/manifest.v3.json | 2 +- package-lock.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/browser/package.json b/apps/browser/package.json index 4610bcb2df8..2b3edd96b6e 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.12.2", + "version": "2024.12.3", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index ab14819e807..32fa5135226 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.12.2", + "version": "2024.12.3", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 0a6687b4fb5..87a08bc89e3 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2024.12.2", + "version": "2024.12.3", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/package-lock.json b/package-lock.json index 997cad9551e..0d743316bd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -190,7 +190,7 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.12.2" + "version": "2024.12.3" }, "apps/cli": { "name": "@bitwarden/cli", From da9e12dae69c53da61611532ede2a5feb20df015 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:03:15 -0800 Subject: [PATCH 32/57] [PM-15498] - add risk insight data service (#12361) * Adding more test cases * Removing unnecessary file * Test cases update * Adding the fixme for strict types * moving the fixme * add risk insight data service and wire up components to it * hook up all applications to risk insights report service. add loading state * link up remaining props to risk insight report * wire up children to risk insight data service * add missing copy. remove loading state from risk insights * fix types * fix DI issue * remove @Injectable from RiskInsightsDataService --------- Co-authored-by: Tom Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com> --- .../layouts/organization-layout.component.ts | 1 - apps/web/src/locales/en/messages.json | 3 + .../risk-insights/models/password-health.ts | 2 +- .../reports/risk-insights/services/index.ts | 1 + .../services/risk-insights-data.service.ts | 60 ++++++++++ .../services/risk-insights-report.service.ts | 1 + .../access-intelligence.module.ts | 5 + .../all-applications.component.html | 49 ++++---- .../all-applications.component.ts | 101 ++++++++++------- .../risk-insights-loading.component.html | 8 ++ .../risk-insights-loading.component.ts | 14 +++ .../risk-insights.component.html | 105 ++++++++++-------- .../risk-insights.component.ts | 101 +++++++++++------ 13 files changed, 301 insertions(+), 150 deletions(-) create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts create mode 100644 bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html create mode 100644 bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 8c4f5ce8c46..0b024817edc 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -57,7 +57,6 @@ export class OrganizationLayoutComponent implements OnInit { showPaymentAndHistory$: Observable; hideNewOrgButton$: Observable; organizationIsUnmanaged$: Observable; - isAccessIntelligenceFeatureEnabled = false; enterpriseOrganization$: Observable; constructor( diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 36143682fa2..abd5779339f 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3882,6 +3882,9 @@ "updateBrowser": { "message": "Update browser" }, + "generatingRiskInsights": { + "message": "Generating your risk insights..." + }, "updateBrowserDesc": { "message": "You are using an unsupported web browser. The web vault may not function properly." }, diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts index 427cb06d9e0..b8d5852088a 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts @@ -25,7 +25,7 @@ export type ApplicationHealthReportDetail = { passwordCount: number; atRiskPasswordCount: number; memberCount: number; - + atRiskMemberCount: number; memberDetails: MemberDetailsFlat[]; atRiskMemberDetails: MemberDetailsFlat[]; }; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts index e930c7666e8..a8e62437b9d 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts @@ -1,3 +1,4 @@ export * from "./member-cipher-details-api.service"; export * from "./password-health.service"; export * from "./risk-insights-report.service"; +export * from "./risk-insights-data.service"; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts new file mode 100644 index 00000000000..42bab69fca4 --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-data.service.ts @@ -0,0 +1,60 @@ +import { BehaviorSubject } from "rxjs"; +import { finalize } from "rxjs/operators"; + +import { ApplicationHealthReportDetail } from "../models/password-health"; + +import { RiskInsightsReportService } from "./risk-insights-report.service"; + +export class RiskInsightsDataService { + private applicationsSubject = new BehaviorSubject(null); + + applications$ = this.applicationsSubject.asObservable(); + + private isLoadingSubject = new BehaviorSubject(false); + isLoading$ = this.isLoadingSubject.asObservable(); + + private isRefreshingSubject = new BehaviorSubject(false); + isRefreshing$ = this.isRefreshingSubject.asObservable(); + + private errorSubject = new BehaviorSubject(null); + error$ = this.errorSubject.asObservable(); + + private dataLastUpdatedSubject = new BehaviorSubject(null); + dataLastUpdated$ = this.dataLastUpdatedSubject.asObservable(); + + constructor(private reportService: RiskInsightsReportService) {} + + /** + * Fetches the applications report and updates the applicationsSubject. + * @param organizationId The ID of the organization. + */ + fetchApplicationsReport(organizationId: string, isRefresh?: boolean): void { + if (isRefresh) { + this.isRefreshingSubject.next(true); + } else { + this.isLoadingSubject.next(true); + } + this.reportService + .generateApplicationsReport$(organizationId) + .pipe( + finalize(() => { + this.isLoadingSubject.next(false); + this.isRefreshingSubject.next(false); + this.dataLastUpdatedSubject.next(new Date()); + }), + ) + .subscribe({ + next: (reports: ApplicationHealthReportDetail[]) => { + this.applicationsSubject.next(reports); + this.errorSubject.next(null); + }, + error: () => { + this.applicationsSubject.next([]); + }, + }); + } + + refreshApplicationsReport(organizationId: string): void { + this.fetchApplicationsReport(organizationId, true); + } +} diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts index 45746cd2f5a..c5e9e3625de 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts @@ -287,6 +287,7 @@ export class RiskInsightsReportService { : newUriDetail.cipherMembers, atRiskMemberDetails: existingUriDetail ? existingUriDetail.atRiskMemberDetails : [], atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0, + atRiskMemberCount: existingUriDetail ? existingUriDetail.atRiskMemberDetails.length : 0, } as ApplicationHealthReportDetail; if (isAtRisk) { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts index 87b75dee70c..2db7af4bb46 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core"; import { MemberCipherDetailsApiService, + RiskInsightsDataService, RiskInsightsReportService, } from "@bitwarden/bit-common/tools/reports/risk-insights/services"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -28,6 +29,10 @@ import { RiskInsightsComponent } from "./risk-insights.component"; MemberCipherDetailsApiService, ], }, + { + provide: RiskInsightsDataService, + deps: [RiskInsightsReportService], + }, ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html index 4ed31adea78..ea1a4f9db31 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html @@ -1,16 +1,11 @@ -
- - {{ "loading" | i18n }} +
+
-
+

- {{ "noAppsInOrgTitle" | i18n: organization.name }} + {{ "noAppsInOrgTitle" | i18n: organization?.name }}

@@ -28,21 +23,21 @@

-
+

{{ "allApplications" | i18n }}

@@ -57,7 +52,7 @@

{{ "allApplications" | i18n }}

type="button" buttonType="secondary" bitButton - *ngIf="isCritialAppsFeatureEnabled" + *ngIf="isCriticalAppsFeatureEnabled" [disabled]="!selectedIds.size" [loading]="markingAsCritical" (click)="markAppsAsCritical()" @@ -69,17 +64,17 @@

{{ "allApplications" | i18n }}

- - {{ "application" | i18n }} - {{ "atRiskPasswords" | i18n }} - {{ "totalPasswords" | i18n }} - {{ "atRiskMembers" | i18n }} - {{ "totalMembers" | i18n }} + + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} - + {{ "allApplications" | i18n }} /> - {{ r.name }} + {{ r.applicationName }} - {{ r.atRiskPasswords }} + {{ r.atRiskPasswordCount }} - {{ r.totalPasswords }} + {{ r.passwordCount }} - {{ r.atRiskMembers }} + {{ r.atRiskMemberCount }} - {{ r.totalMembers }} + {{ r.memberCount }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts index 6ee2ecf1690..f4d3656071d 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts @@ -1,20 +1,23 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, DestroyRef, inject, OnInit } from "@angular/core"; +import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { debounceTime, firstValueFrom, map } from "rxjs"; +import { debounceTime, map, Observable, of, Subscription } from "rxjs"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { + RiskInsightsDataService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + ApplicationHealthReportDetail, + ApplicationHealthReportSummary, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Icons, NoItemsModule, @@ -27,60 +30,76 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { applicationTableMockData } from "./application-table.mock"; +import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; @Component({ standalone: true, selector: "tools-all-applications", templateUrl: "./all-applications.component.html", - imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule], + imports: [ + ApplicationsLoadingComponent, + HeaderModule, + CardComponent, + SearchModule, + PipesModule, + NoItemsModule, + SharedModule, + ], }) -export class AllApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); +export class AllApplicationsComponent implements OnInit, OnDestroy { + protected dataSource = new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); - private destroyRef = inject(DestroyRef); - protected loading = false; - protected organization: Organization; + protected loading = true; + protected organization = {} as Organization; noItemsIcon = Icons.Security; protected markingAsCritical = false; - isCritialAppsFeatureEnabled = false; + protected applicationSummary = {} as ApplicationHealthReportSummary; + private subscription = new Subscription(); - // MOCK DATA - protected mockData = applicationTableMockData; - protected mockAtRiskMembersCount = 0; - protected mockAtRiskAppsCount = 0; - protected mockTotalMembersCount = 0; - protected mockTotalAppsCount = 0; + destroyRef = inject(DestroyRef); + isLoading$: Observable = of(false); + isCriticalAppsFeatureEnabled = false; async ngOnInit() { - this.activatedRoute.paramMap - .pipe( - takeUntilDestroyed(this.destroyRef), - map(async (params) => { - const organizationId = params.get("organizationId"); - this.organization = await firstValueFrom(this.organizationService.get$(organizationId)); - // TODO: use organizationId to fetch data - }), - ) - .subscribe(); - - this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( + this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag( FeatureFlag.CriticalApps, ); + + const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); + + if (organizationId) { + this.organization = await this.organizationService.get(organizationId); + this.subscription = this.dataService.applications$ + .pipe( + map((applications) => { + if (applications) { + this.dataSource.data = applications; + this.applicationSummary = + this.reportService.generateApplicationsSummary(applications); + } + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe(); + this.isLoading$ = this.dataService.isLoading$; + } + } + + ngOnDestroy(): void { + this.subscription?.unsubscribe(); } constructor( protected cipherService: CipherService, - protected passwordStrengthService: PasswordStrengthServiceAbstraction, - protected auditService: AuditService, protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected toastService: ToastService, - protected organizationService: OrganizationService, protected configService: ConfigService, + protected dataService: RiskInsightsDataService, + protected organizationService: OrganizationService, + protected reportService: RiskInsightsReportService, ) { - this.dataSource.data = applicationTableMockData; this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); @@ -90,7 +109,7 @@ export class AllApplicationsComponent implements OnInit { // TODO: implement this.toastService.showToast({ variant: "warning", - title: null, + title: "", message: "Not yet implemented", }); }; @@ -103,7 +122,7 @@ export class AllApplicationsComponent implements OnInit { this.selectedIds.clear(); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("appsMarkedAsCritical"), }); resolve(true); @@ -112,8 +131,8 @@ export class AllApplicationsComponent implements OnInit { }); }; - trackByFunction(_: number, item: CipherView) { - return item.id; + trackByFunction(_: number, item: ApplicationHealthReportDetail) { + return item.applicationName; } onCheckboxChange(id: number, event: Event) { diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html new file mode 100644 index 00000000000..d6f945bfb92 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.html @@ -0,0 +1,8 @@ +
+ +

{{ "generatingRiskInsights" | i18n }}

+
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts new file mode 100644 index 00000000000..1cafa62c608 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights-loading.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; + +@Component({ + selector: "tools-risk-insights-loading", + standalone: true, + imports: [CommonModule, JslibModule], + templateUrl: "./risk-insights-loading.component.html", +}) +export class ApplicationsLoadingComponent { + constructor() {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index 6df47e3c46f..e0618c525a7 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -1,49 +1,58 @@ -
{{ "accessIntelligence" | i18n }}
-

{{ "riskInsights" | i18n }}

-
- {{ "reviewAtRiskPasswords" | i18n }} -  {{ "learnMore" | i18n }} -
-
- - {{ - "dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a") - }} - +
{{ "accessIntelligence" | i18n }}
+

{{ "riskInsights" | i18n }}

+
+
- {{ "refresh" | i18n }} - -
- - - - - - - - {{ "criticalApplicationsWithCount" | i18n: criticalApps.length }} - - - - - - - - - - - - - - + + {{ + "dataLastUpdated" | i18n: (dataLastUpdated$ | async | date: "MMMM d, y 'at' h:mm a") + }} + + + {{ "refresh" | i18n }} + + + + + +
+ + + + + + + + {{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }} + + + + + + + + + + + + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 5ea39bd0513..1a90e18f0df 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -1,11 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component, DestroyRef, OnInit, inject } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; +import { Observable, EMPTY } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; @@ -43,45 +45,80 @@ export enum RiskInsightsTabType { ], }) export class RiskInsightsComponent implements OnInit { - tabIndex: RiskInsightsTabType; - dataLastUpdated = new Date(); - isCritialAppsFeatureEnabled = false; + tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps; - apps: any[] = []; - criticalApps: any[] = []; - notifiedMembers: any[] = []; + dataLastUpdated: Date = new Date(); - async refreshData() { - // TODO: Implement - return new Promise((resolve) => - setTimeout(() => { - this.dataLastUpdated = new Date(); - resolve(true); - }, 1000), - ); - } + isCriticalAppsFeatureEnabled: boolean = false; - onTabChange = async (newIndex: number) => { - await this.router.navigate([], { - relativeTo: this.route, - queryParams: { tabIndex: newIndex }, - queryParamsHandling: "merge", + appsCount: number = 0; + criticalAppsCount: number = 0; + notifiedMembersCount: number = 0; + + private organizationId: string | null = null; + private destroyRef = inject(DestroyRef); + isLoading$: Observable = new Observable(); + isRefreshing$: Observable = new Observable(); + dataLastUpdated$: Observable = new Observable(); + refetching: boolean = false; + + constructor( + private route: ActivatedRoute, + private router: Router, + private configService: ConfigService, + private dataService: RiskInsightsDataService, + ) { + this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { + this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps; }); - }; + } async ngOnInit() { - this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag( + this.isCriticalAppsFeatureEnabled = await this.configService.getFeatureFlag( FeatureFlag.CriticalApps, ); + + this.route.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap((orgId: string | null) => { + if (orgId) { + this.organizationId = orgId; + this.dataService.fetchApplicationsReport(orgId); + this.isLoading$ = this.dataService.isLoading$; + this.isRefreshing$ = this.dataService.isRefreshing$; + this.dataLastUpdated$ = this.dataService.dataLastUpdated$; + return this.dataService.applications$; + } else { + return EMPTY; + } + }), + ) + .subscribe({ + next: (applications: ApplicationHealthReportDetail[] | null) => { + if (applications) { + this.appsCount = applications.length; + } + }, + }); } - constructor( - protected route: ActivatedRoute, - private router: Router, - private configService: ConfigService, - ) { - route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { - this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps; + /** + * Refreshes the data by re-fetching the applications report. + * This will automatically notify child components subscribed to the RiskInsightsDataService observables. + */ + refreshData(): void { + if (this.organizationId) { + this.dataService.refreshApplicationsReport(this.organizationId); + } + } + + async onTabChange(newIndex: number): Promise { + await this.router.navigate([], { + relativeTo: this.route, + queryParams: { tabIndex: newIndex }, + queryParamsHandling: "merge", }); } } From 179c26434b10d957ca264ec1685198700aa89807 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:45:27 -0500 Subject: [PATCH 33/57] PM-16172 - Web - LockV2 - make page title consistent with other clients (#12462) --- apps/web/src/app/oss-routing.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 1903759f959..649f1aba534 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -534,7 +534,7 @@ const routes: Routes = [ ], data: { pageTitle: { - key: "yourAccountIsLocked", + key: "yourVaultIsLockedV2", }, pageIcon: LockIcon, showReadonlyHostname: true, From 2bb807c5ce07e6c40609be323a63402d0c78e8b6 Mon Sep 17 00:00:00 2001 From: albertboyd5 Date: Wed, 18 Dec 2024 13:20:17 -0600 Subject: [PATCH 34/57] Update Password history button label to Generator history (#12452) --- apps/desktop/src/main/menu/menu.view.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/main/menu/menu.view.ts b/apps/desktop/src/main/menu/menu.view.ts index c78d2347bcd..962c57fdb60 100644 --- a/apps/desktop/src/main/menu/menu.view.ts +++ b/apps/desktop/src/main/menu/menu.view.ts @@ -76,7 +76,7 @@ export class ViewMenu implements IMenubarMenu { private get passwordHistory(): MenuItemConstructorOptions { return { id: "passwordHistory", - label: this.localize("passwordHistory"), + label: this.localize("generatorHistory"), click: () => this.sendMessage("openPasswordHistory"), enabled: !this._isLocked, }; From 6eb30c98c4d8436248560d39a08984ae443584f9 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Wed, 18 Dec 2024 14:25:03 -0500 Subject: [PATCH 35/57] Fix imports for standalone component stories (#12464) --- libs/components/src/async-actions/in-forms.stories.ts | 9 +++------ libs/components/src/async-actions/standalone.stories.ts | 3 +-- libs/components/src/badge/badge.stories.ts | 3 +-- libs/components/src/breadcrumbs/breadcrumbs.stories.ts | 3 +-- libs/components/src/dialog/dialog.service.stories.ts | 9 ++++++--- libs/components/src/toggle-group/toggle-group.stories.ts | 3 +-- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index ec6005dd607..fb94e43b196 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -109,20 +109,17 @@ export default { title: "Component Library/Async Actions/In Forms", decorators: [ moduleMetadata({ - declarations: [ + declarations: [PromiseExampleComponent, ObservableExampleComponent], + imports: [ BitSubmitDirective, BitFormButtonDirective, - PromiseExampleComponent, - ObservableExampleComponent, - BitActionDirective, - ], - imports: [ FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule, IconButtonModule, + BitActionDirective, ], providers: [ { diff --git a/libs/components/src/async-actions/standalone.stories.ts b/libs/components/src/async-actions/standalone.stories.ts index 5e15135dc5d..f658dfb0f01 100644 --- a/libs/components/src/async-actions/standalone.stories.ts +++ b/libs/components/src/async-actions/standalone.stories.ts @@ -56,12 +56,11 @@ export default { decorators: [ moduleMetadata({ declarations: [ - BitActionDirective, PromiseExampleComponent, ObservableExampleComponent, RejectedPromiseExampleComponent, ], - imports: [ButtonModule, IconButtonModule], + imports: [ButtonModule, IconButtonModule, BitActionDirective], providers: [ { provide: ValidationService, diff --git a/libs/components/src/badge/badge.stories.ts b/libs/components/src/badge/badge.stories.ts index 6c57bc0cbfb..b8ac7ec8efe 100644 --- a/libs/components/src/badge/badge.stories.ts +++ b/libs/components/src/badge/badge.stories.ts @@ -8,8 +8,7 @@ export default { component: BadgeDirective, decorators: [ moduleMetadata({ - imports: [CommonModule], - declarations: [BadgeDirective], + imports: [CommonModule, BadgeDirective], }), ], args: { diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 300369f2454..9c8ccbccd3f 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -25,8 +25,7 @@ export default { component: BreadcrumbsComponent, decorators: [ moduleMetadata({ - declarations: [BreadcrumbComponent], - imports: [LinkModule, MenuModule, IconButtonModule, RouterModule], + imports: [LinkModule, MenuModule, IconButtonModule, RouterModule, BreadcrumbComponent], }), applicationConfig({ providers: [ diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index e28f0ac4b19..5e938412804 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -64,13 +64,16 @@ export default { component: StoryDialogComponent, decorators: [ moduleMetadata({ - declarations: [ + declarations: [StoryDialogContentComponent], + imports: [ + SharedModule, + ButtonModule, + DialogModule, + IconButtonModule, DialogCloseDirective, DialogComponent, DialogTitleContainerDirective, - StoryDialogContentComponent, ], - imports: [SharedModule, ButtonModule, DialogModule, IconButtonModule], providers: [ DialogService, { diff --git a/libs/components/src/toggle-group/toggle-group.stories.ts b/libs/components/src/toggle-group/toggle-group.stories.ts index edfa832d6ce..fc8ea0ea929 100644 --- a/libs/components/src/toggle-group/toggle-group.stories.ts +++ b/libs/components/src/toggle-group/toggle-group.stories.ts @@ -13,8 +13,7 @@ export default { }, decorators: [ moduleMetadata({ - declarations: [ToggleGroupComponent, ToggleComponent], - imports: [BadgeModule], + imports: [BadgeModule, ToggleGroupComponent, ToggleComponent], }), ], parameters: { From ef8e8bfcbcb2f826b47a49b0b5ce20e7d23b0fa4 Mon Sep 17 00:00:00 2001 From: albertboyd5 Date: Wed, 18 Dec 2024 14:17:50 -0600 Subject: [PATCH 36/57] [PM-15571] Help link on import vault (#12448) * [PM-15571] BW help Link on import vault page has wrong styling in dark mode * Remove useless code --- libs/importer/src/components/import.component.html | 8 +++++++- libs/importer/src/components/import.component.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 33056265de4..0da8127369e 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -80,7 +80,13 @@

{{ "data" | i18n }}

See detailed instructions on our help site at - + https://bitwarden.com/help/export-your-data/ diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 247b33aa6a9..b50be773251 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -53,6 +53,7 @@ import { SectionHeaderComponent, SelectModule, ToastService, + LinkModule, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -115,6 +116,7 @@ const safeProviders: SafeProvider[] = [ ContainerComponent, SectionHeaderComponent, SectionComponent, + LinkModule, ], providers: safeProviders, }) From 456046e095b9bee3938df4e67daf277569e508a2 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:28:13 -0500 Subject: [PATCH 37/57] Make Sync Optional on SW Start (#12467) --- apps/browser/src/background/main.background.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 019be2923b6..ba9776b80c5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1330,7 +1330,7 @@ export default class MainBackground { return new Promise((resolve) => { setTimeout(async () => { await this.refreshBadge(); - await this.fullSync(true); + await this.fullSync(false); this.taskSchedulerService.setInterval( ScheduledTaskNames.scheduleNextSyncInterval, 5 * 60 * 1000, // check every 5 minutes From 51f6594d4b680f250207e8f3dcac9fcc7bb26e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Thu, 19 Dec 2024 09:00:21 +0100 Subject: [PATCH 38/57] [PM-9473] Add messaging for macOS passkey extension and desktop (#10768) * Add messaging for macos passkey provider * fix: credential id conversion * Make build.sh executable Co-authored-by: Colton Hurst * chore: add TODO --------- Co-authored-by: Andreas Coroiu Co-authored-by: Andreas Coroiu Co-authored-by: Colton Hurst --- .../fido2/background/fido2.background.spec.ts | 5 +- .../fido2/background/fido2.background.ts | 5 +- .../browser-fido2-user-interface.service.ts | 6 +- .../browser/src/background/main.background.ts | 15 +- apps/desktop/desktop_native/Cargo.lock | 485 +++++++++++++++++- apps/desktop/desktop_native/Cargo.toml | 2 +- .../desktop_native/macos_provider/.gitignore | 1 + .../desktop_native/macos_provider/Cargo.toml | 30 ++ .../desktop_native/macos_provider/build.sh | 43 ++ .../macos_provider/src/assertion.rs | 46 ++ .../desktop_native/macos_provider/src/lib.rs | 205 ++++++++ .../macos_provider/src/registration.rs | 43 ++ .../macos_provider/uniffi-bindgen.rs | 3 + .../desktop_native/macos_provider/uniffi.toml | 4 + apps/desktop/desktop_native/napi/Cargo.toml | 2 + apps/desktop/desktop_native/napi/index.d.ts | 52 ++ apps/desktop/desktop_native/napi/src/lib.rs | 244 +++++++++ apps/desktop/macos/.gitignore | 1 + .../CredentialProviderViewController.swift | 184 ++++++- .../autofill_extension.entitlements | 4 + .../macos/desktop.xcodeproj/project.pbxproj | 8 + apps/desktop/package.json | 2 +- .../src/app/services/services.module.ts | 29 +- apps/desktop/src/autofill/preload.ts | 83 +++ .../services/desktop-autofill.service.ts | 166 +++++- apps/desktop/src/main.ts | 2 +- .../main/autofill/native-autofill.main.ts | 55 +- .../desktop-fido2-user-interface.service.ts | 125 +++++ ...fido2-authenticator.service.abstraction.ts | 6 +- .../fido2/fido2-client.service.abstraction.ts | 6 +- ...ido2-user-interface.service.abstraction.ts | 4 +- .../fido2/fido2-authenticator.service.spec.ts | 75 +-- .../fido2/fido2-authenticator.service.ts | 14 +- .../services/fido2/fido2-autofill-utils.ts | 10 +- .../fido2/fido2-client.service.spec.ts | 96 ++-- .../services/fido2/fido2-client.service.ts | 23 +- .../noop-fido2-user-interface.service.ts | 2 +- 37 files changed, 1936 insertions(+), 150 deletions(-) create mode 100644 apps/desktop/desktop_native/macos_provider/.gitignore create mode 100644 apps/desktop/desktop_native/macos_provider/Cargo.toml create mode 100755 apps/desktop/desktop_native/macos_provider/build.sh create mode 100644 apps/desktop/desktop_native/macos_provider/src/assertion.rs create mode 100644 apps/desktop/desktop_native/macos_provider/src/lib.rs create mode 100644 apps/desktop/desktop_native/macos_provider/src/registration.rs create mode 100644 apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs create mode 100644 apps/desktop/desktop_native/macos_provider/uniffi.toml create mode 100644 apps/desktop/macos/.gitignore create mode 100644 apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts index 99ed4619954..144af0c0a35 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.spec.ts @@ -25,6 +25,7 @@ import { BrowserScriptInjectorService } from "../../../platform/services/browser import { AbortManager } from "../../../vault/background/abort-manager"; import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; import { Fido2PortName } from "../enums/fido2-port-name.enum"; +import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service"; import { Fido2ExtensionMessage } from "./abstractions/fido2.background"; import { Fido2Background } from "./fido2.background"; @@ -56,7 +57,7 @@ describe("Fido2Background", () => { let senderMock!: MockProxy; let logService!: MockProxy; let fido2ActiveRequestManager: MockProxy; - let fido2ClientService!: MockProxy; + let fido2ClientService!: MockProxy>; let vaultSettingsService!: MockProxy; let scriptInjectorServiceMock!: MockProxy; let configServiceMock!: MockProxy; @@ -73,7 +74,7 @@ describe("Fido2Background", () => { }); senderMock = mock({ id: "1", tab: tabMock }); logService = mock(); - fido2ClientService = mock(); + fido2ClientService = mock>(); vaultSettingsService = mock(); abortManagerMock = mock(); abortController = mock(); diff --git a/apps/browser/src/autofill/fido2/background/fido2.background.ts b/apps/browser/src/autofill/fido2/background/fido2.background.ts index f84b7d29a66..e20a0584d20 100644 --- a/apps/browser/src/autofill/fido2/background/fido2.background.ts +++ b/apps/browser/src/autofill/fido2/background/fido2.background.ts @@ -23,10 +23,11 @@ import { ScriptInjectorService } from "../../../platform/services/abstractions/s import { AbortManager } from "../../../vault/background/abort-manager"; import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; import { Fido2PortName } from "../enums/fido2-port-name.enum"; +import { BrowserFido2ParentWindowReference } from "../services/browser-fido2-user-interface.service"; import { - Fido2Background as Fido2BackgroundInterface, Fido2BackgroundExtensionMessageHandlers, + Fido2Background as Fido2BackgroundInterface, Fido2ExtensionMessage, SharedFido2ScriptInjectionDetails, SharedFido2ScriptRegistrationOptions, @@ -56,7 +57,7 @@ export class Fido2Background implements Fido2BackgroundInterface { constructor( private logService: LogService, private fido2ActiveRequestManager: Fido2ActiveRequestManager, - private fido2ClientService: Fido2ClientService, + private fido2ClientService: Fido2ClientService, private vaultSettingsService: VaultSettingsService, private scriptInjectorService: ScriptInjectorService, private configService: ConfigService, diff --git a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts index 872bb1bb52a..04b09a7df32 100644 --- a/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts +++ b/apps/browser/src/autofill/fido2/services/browser-fido2-user-interface.service.ts @@ -111,11 +111,15 @@ export type BrowserFido2Message = { sessionId: string } & ( } ); +export type BrowserFido2ParentWindowReference = chrome.tabs.Tab; + /** * Browser implementation of the {@link Fido2UserInterfaceService}. * The user interface is implemented as a popout and the service uses the browser's messaging API to communicate with it. */ -export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { +export class BrowserFido2UserInterfaceService + implements Fido2UserInterfaceServiceAbstraction +{ constructor(private authService: AuthService) {} async newSession( diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index ba9776b80c5..616e18601af 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -201,11 +201,11 @@ import { ImportServiceAbstraction, } from "@bitwarden/importer/core"; import { - DefaultKdfConfigService, - KdfConfigService, BiometricStateService, BiometricsService, DefaultBiometricStateService, + DefaultKdfConfigService, + KdfConfigService, KeyService as KeyServiceAbstraction, } from "@bitwarden/key-management"; import { @@ -232,7 +232,10 @@ import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-ha import LegacyOverlayBackground from "../autofill/deprecated/background/overlay.background.deprecated"; import { Fido2Background as Fido2BackgroundAbstraction } from "../autofill/fido2/background/abstractions/fido2.background"; import { Fido2Background } from "../autofill/fido2/background/fido2.background"; -import { BrowserFido2UserInterfaceService } from "../autofill/fido2/services/browser-fido2-user-interface.service"; +import { + BrowserFido2ParentWindowReference, + BrowserFido2UserInterfaceService, +} from "../autofill/fido2/services/browser-fido2-user-interface.service"; import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; import AutofillService from "../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service"; @@ -337,10 +340,10 @@ export default class MainBackground { policyApiService: PolicyApiServiceAbstraction; sendApiService: SendApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction; - fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; - fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; + fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; + fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; fido2ActiveRequestManager: Fido2ActiveRequestManagerAbstraction; - fido2ClientService: Fido2ClientServiceAbstraction; + fido2ClientService: Fido2ClientServiceAbstraction; avatarService: AvatarServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; cipherContextMenuHandler: CipherContextMenuHandler; diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 09d3d15e897..1fac307262a 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -62,12 +62,55 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.94" @@ -103,6 +146,47 @@ dependencies = [ "zeroize", ] +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -318,6 +402,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + [[package]] name = "bcrypt-pbkdf" version = "0.10.0" @@ -329,6 +422,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -422,6 +524,38 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cbc" version = "0.1.2" @@ -487,6 +621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -495,11 +630,24 @@ version = "4.5.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" dependencies = [ + "anstream", "anstyle", "clap_lex", "strsim", ] +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.7.3" @@ -525,6 +673,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -724,6 +878,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.9" @@ -815,6 +982,8 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "serde", + "serde_json", "tokio", "tokio-stream", "tokio-util", @@ -1035,6 +1204,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "futures" version = "0.3.31" @@ -1190,12 +1368,35 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -1245,7 +1446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -1273,6 +1474,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -1372,6 +1579,21 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "macos_provider" +version = "0.0.0" +dependencies = [ + "desktop_core", + "futures", + "log", + "oslog", + "serde", + "serde_json", + "tokio", + "tokio-util", + "uniffi", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1397,6 +1619,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1811,6 +2049,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "oslog" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" +dependencies = [ + "cc", + "dashmap", + "log", +] + [[package]] name = "parking" version = "2.2.1" @@ -1851,6 +2100,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1967,6 +2222,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "polling" version = "3.7.4" @@ -2235,6 +2496,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "salsa20" version = "0.10.2" @@ -2262,6 +2529,26 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -2301,6 +2588,9 @@ name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -2322,6 +2612,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -2391,6 +2693,12 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -2406,6 +2714,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.8" @@ -2544,6 +2858,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2648,6 +2971,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -2726,6 +3058,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-ident" version = "1.0.14" @@ -2744,6 +3082,136 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "once_cell", + "paste", + "serde", + "textwrap", + "toml", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7cf32576e08104b7dc2a6a5d815f37616e66c6866c2a639fe16e6d2286b75b" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -2754,6 +3222,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" @@ -2839,6 +3313,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + [[package]] name = "widestring" version = "1.1.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 6525b38162d..6230a6bfe15 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["napi", "core", "proxy"] +members = ["napi", "core", "proxy", "macos_provider"] diff --git a/apps/desktop/desktop_native/macos_provider/.gitignore b/apps/desktop/desktop_native/macos_provider/.gitignore new file mode 100644 index 00000000000..73f0a6381d8 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/.gitignore @@ -0,0 +1 @@ +BitwardenMacosProviderFFI.xcframework diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml new file mode 100644 index 00000000000..28cc6372c62 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "macos_provider" +license = "GPL-3.0" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "uniffi-bindgen" +path = "uniffi-bindgen.rs" + +[lib] +crate-type = ["staticlib", "cdylib"] +bench = false + +[dependencies] +desktop_core = { path = "../core" } +futures = "=0.3.31" +log = "0.4.22" +serde = { version = "1.0.205", features = ["derive"] } +serde_json = "1.0.122" +tokio = { version = "1.39.2", features = ["sync"] } +tokio-util = "0.7.11" +uniffi = { version = "0.28.0", features = ["cli"] } + +[target.'cfg(target_os = "macos")'.dependencies] +oslog = "0.2.0" + +[build-dependencies] +uniffi = { version = "0.28.0", features = ["build"] } diff --git a/apps/desktop/desktop_native/macos_provider/build.sh b/apps/desktop/desktop_native/macos_provider/build.sh new file mode 100755 index 00000000000..21e2e045af4 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/build.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +cd "$(dirname "$0")" + +rm -r BitwardenMacosProviderFFI.xcframework +rm -r tmp + +mkdir -p ./tmp/target/universal-darwin/release/ + + +cargo build --package macos_provider --target aarch64-apple-darwin --release +cargo build --package macos_provider --target x86_64-apple-darwin --release + +# Create universal libraries +lipo -create ../target/aarch64-apple-darwin/release/libmacos_provider.a \ + ../target/x86_64-apple-darwin/release/libmacos_provider.a \ + -output ./tmp/target/universal-darwin/release/libmacos_provider.a + +# Generate swift bindings +cargo run --bin uniffi-bindgen --features uniffi/cli generate \ + ../target/aarch64-apple-darwin/release/libmacos_provider.dylib \ + --library \ + --language swift \ + --no-format \ + --out-dir tmp/bindings + +# Move generated swift bindings +mkdir -p ../../macos/autofill-extension/ +mv ./tmp/bindings/*.swift ../../macos/autofill-extension/ + +# Massage the generated files to fit xcframework +mkdir tmp/Headers +mv ./tmp/bindings/*.h ./tmp/Headers/ +cat ./tmp/bindings/*.modulemap > ./tmp/Headers/module.modulemap + +# Build xcframework +xcodebuild -create-xcframework \ + -library ./tmp/target/universal-darwin/release/libmacos_provider.a \ + -headers ./tmp/Headers \ + -output ./BitwardenMacosProviderFFI.xcframework + +# Cleanup temporary files +rm -r tmp diff --git a/apps/desktop/desktop_native/macos_provider/src/assertion.rs b/apps/desktop/desktop_native/macos_provider/src/assertion.rs new file mode 100644 index 00000000000..762ceaaed48 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/src/assertion.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::{BitwardenError, Callback, UserVerification}; + +#[derive(uniffi::Record, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionRequest { + rp_id: String, + credential_id: Vec, + user_name: String, + user_handle: Vec, + record_identifier: Option, + client_data_hash: Vec, + user_verification: UserVerification, +} + +#[derive(uniffi::Record, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyAssertionResponse { + rp_id: String, + user_handle: Vec, + signature: Vec, + client_data_hash: Vec, + authenticator_data: Vec, + credential_id: Vec, +} + +#[uniffi::export(with_foreign)] +pub trait PreparePasskeyAssertionCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyAssertionResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyAssertionCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyAssertionCallback::on_error(self.as_ref(), error); + } +} diff --git a/apps/desktop/desktop_native/macos_provider/src/lib.rs b/apps/desktop/desktop_native/macos_provider/src/lib.rs new file mode 100644 index 00000000000..5623436d874 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/src/lib.rs @@ -0,0 +1,205 @@ +#![cfg(target_os = "macos")] + +use std::{ + collections::HashMap, + sync::{atomic::AtomicU32, Arc, Mutex}, + time::Instant, +}; + +use futures::FutureExt; +use log::{error, info}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +uniffi::setup_scaffolding!(); + +mod assertion; +mod registration; + +use assertion::{PasskeyAssertionRequest, PreparePasskeyAssertionCallback}; +use registration::{PasskeyRegistrationRequest, PreparePasskeyRegistrationCallback}; + +#[derive(uniffi::Enum, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum UserVerification { + Preferred, + Required, + Discouraged, +} + +#[derive(Debug, uniffi::Error, Serialize, Deserialize)] +pub enum BitwardenError { + Internal(String), +} + +// TODO: These have to be named differently than the actual Uniffi traits otherwise +// the generated code will lead to ambiguous trait implementations +// These are only used internally, so it doesn't matter that much +trait Callback: Send + Sync { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error>; + fn error(&self, error: BitwardenError); +} + +#[derive(uniffi::Object)] +pub struct MacOSProviderClient { + to_server_send: tokio::sync::mpsc::Sender, + + // We need to keep track of the callbacks so we can call them when we receive a response + response_callbacks_counter: AtomicU32, + #[allow(clippy::type_complexity)] + response_callbacks_queue: Arc, Instant)>>>, +} + +#[uniffi::export] +impl MacOSProviderClient { + #[uniffi::constructor] + pub fn connect() -> Self { + let _ = oslog::OsLogger::new("com.bitwarden.desktop.autofill-extension") + .level_filter(log::LevelFilter::Trace) + .init(); + + let (from_server_send, mut from_server_recv) = tokio::sync::mpsc::channel(32); + let (to_server_send, to_server_recv) = tokio::sync::mpsc::channel(32); + + let client = MacOSProviderClient { + to_server_send, + response_callbacks_counter: AtomicU32::new(0), + response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())), + }; + + let path = desktop_core::ipc::path("autofill"); + + let queue = client.response_callbacks_queue.clone(); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Can't create runtime"); + + rt.spawn( + desktop_core::ipc::client::connect(path, from_server_send, to_server_recv) + .map(|r| r.map_err(|e| e.to_string())), + ); + + rt.block_on(async move { + while let Some(message) = from_server_recv.recv().await { + match serde_json::from_str::(&message) { + Ok(SerializedMessage::Command(CommandMessage::Connected)) => { + info!("Connected to server"); + } + Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => { + info!("Disconnected from server"); + } + Ok(SerializedMessage::Message { + sequence_number, + value, + }) => match queue.lock().unwrap().remove(&sequence_number) { + Some((cb, request_start_time)) => { + info!( + "Time to process request: {:?}", + request_start_time.elapsed() + ); + match value { + Ok(value) => { + if let Err(e) = cb.complete(value) { + error!("Error deserializing message: {e}"); + } + } + Err(e) => { + error!("Error processing message: {e:?}"); + cb.error(e) + } + } + } + None => { + error!("No callback found for sequence number: {sequence_number}") + } + }, + Err(e) => { + error!("Error deserializing message: {e}"); + } + }; + } + }); + }); + + client + } + + pub fn prepare_passkey_registration( + &self, + request: PasskeyRegistrationRequest, + callback: Arc, + ) { + self.send_message(request, Box::new(callback)); + } + + pub fn prepare_passkey_assertion( + &self, + request: PasskeyAssertionRequest, + callback: Arc, + ) { + self.send_message(request, Box::new(callback)); + } +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "command", rename_all = "camelCase")] +enum CommandMessage { + Connected, + Disconnected, +} + +#[derive(Serialize, Deserialize)] +#[serde(untagged, rename_all = "camelCase")] +enum SerializedMessage { + Command(CommandMessage), + Message { + sequence_number: u32, + value: Result, + }, +} + +impl MacOSProviderClient { + fn add_callback(&self, callback: Box) -> u32 { + let sequence_number = self + .response_callbacks_counter + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + + self.response_callbacks_queue + .lock() + .unwrap() + .insert(sequence_number, (callback, Instant::now())); + + sequence_number + } + + fn send_message( + &self, + message: impl Serialize + DeserializeOwned, + callback: Box, + ) { + let sequence_number = self.add_callback(callback); + + let message = serde_json::to_string(&SerializedMessage::Message { + sequence_number, + value: Ok(serde_json::to_value(message).unwrap()), + }) + .expect("Can't serialize message"); + + if let Err(e) = self.to_server_send.blocking_send(message) { + // Make sure we remove the callback from the queue if we can't send the message + if let Some((cb, _)) = self + .response_callbacks_queue + .lock() + .unwrap() + .remove(&sequence_number) + { + cb.error(BitwardenError::Internal(format!( + "Error sending message: {}", + e + ))); + } + } + } +} diff --git a/apps/desktop/desktop_native/macos_provider/src/registration.rs b/apps/desktop/desktop_native/macos_provider/src/registration.rs new file mode 100644 index 00000000000..d484af58b6c --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/src/registration.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; + +use crate::{BitwardenError, Callback, UserVerification}; + +#[derive(uniffi::Record, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationRequest { + rp_id: String, + user_name: String, + user_handle: Vec, + client_data_hash: Vec, + user_verification: UserVerification, + supported_algorithms: Vec, +} + +#[derive(uniffi::Record, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasskeyRegistrationResponse { + rp_id: String, + client_data_hash: Vec, + credential_id: Vec, + attestation_object: Vec, +} + +#[uniffi::export(with_foreign)] +pub trait PreparePasskeyRegistrationCallback: Send + Sync { + fn on_complete(&self, credential: PasskeyRegistrationResponse); + fn on_error(&self, error: BitwardenError); +} + +impl Callback for Arc { + fn complete(&self, credential: serde_json::Value) -> Result<(), serde_json::Error> { + let credential = serde_json::from_value(credential)?; + PreparePasskeyRegistrationCallback::on_complete(self.as_ref(), credential); + Ok(()) + } + + fn error(&self, error: BitwardenError) { + PreparePasskeyRegistrationCallback::on_error(self.as_ref(), error); + } +} diff --git a/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs b/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs new file mode 100644 index 00000000000..f6cff6cf1d9 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/apps/desktop/desktop_native/macos_provider/uniffi.toml b/apps/desktop/desktop_native/macos_provider/uniffi.toml new file mode 100644 index 00000000000..ba696b8ec15 --- /dev/null +++ b/apps/desktop/desktop_native/macos_provider/uniffi.toml @@ -0,0 +1,4 @@ +[bindings.swift] +ffi_module_name = "BitwardenMacosProviderFFI" +module_name = "BitwardenMacosProvider" +generate_immutable_records = true diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 08664eb6a53..6a656cdc574 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -20,6 +20,8 @@ anyhow = "=1.0.94" desktop_core = { path = "../core" } napi = { version = "=2.16.13", features = ["async"] } napi-derive = "=2.16.13" +serde = { version = "1.0.209", features = ["derive"] } +serde_json = "1.0.127" tokio = { version = "=1.41.1" } tokio-util = "=0.7.12" tokio-stream = "=0.1.15" diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index b884829e77d..1f37563e4fe 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -124,6 +124,58 @@ export declare namespace ipc { } export declare namespace autofill { export function runCommand(value: string): Promise + export const enum UserVerification { + Preferred = 'preferred', + Required = 'required', + Discouraged = 'discouraged' + } + export interface PasskeyRegistrationRequest { + rpId: string + userName: string + userHandle: Array + clientDataHash: Array + userVerification: UserVerification + supportedAlgorithms: Array + } + export interface PasskeyRegistrationResponse { + rpId: string + clientDataHash: Array + credentialId: Array + attestationObject: Array + } + export interface PasskeyAssertionRequest { + rpId: string + credentialId: Array + userName: string + userHandle: Array + recordIdentifier?: string + clientDataHash: Array + userVerification: UserVerification + } + export interface PasskeyAssertionResponse { + rpId: string + userHandle: Array + signature: Array + clientDataHash: Array + authenticatorData: Array + credentialId: Array + } + export class IpcServer { + /** + * Create and start the IPC server without blocking. + * + * @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + * @param callback This function will be called whenever a message is received from a client. + */ + static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void): Promise + /** Return the path to the IPC server. */ + getPath(): string + /** Stop the IPC server. */ + stop(): void + completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number + completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number + completeError(clientId: number, sequenceNumber: number, error: string): number + } } export declare namespace crypto { export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index a7e2144b1dc..170d7bca4f9 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -545,12 +545,256 @@ pub mod ipc { #[napi] pub mod autofill { + use desktop_core::ipc::server::{Message, MessageType}; + use napi::threadsafe_function::{ + ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + use serde::{de::DeserializeOwned, Deserialize, Serialize}; + #[napi] pub async fn run_command(value: String) -> napi::Result { desktop_core::autofill::run_command(value) .await .map_err(|e| napi::Error::from_reason(e.to_string())) } + + #[derive(Debug, serde::Serialize, serde:: Deserialize)] + pub enum BitwardenError { + Internal(String), + } + + #[napi(string_enum)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub enum UserVerification { + #[napi(value = "preferred")] + Preferred, + #[napi(value = "required")] + Required, + #[napi(value = "discouraged")] + Discouraged, + } + + #[derive(Serialize, Deserialize)] + #[serde(bound = "T: Serialize + DeserializeOwned")] + pub struct PasskeyMessage { + pub sequence_number: u32, + pub value: Result, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationRequest { + pub rp_id: String, + pub user_name: String, + pub user_handle: Vec, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + pub supported_algorithms: Vec, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyRegistrationResponse { + pub rp_id: String, + pub client_data_hash: Vec, + pub credential_id: Vec, + pub attestation_object: Vec, + } + + #[napi(object)] + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionRequest { + pub rp_id: String, + pub credential_id: Vec, + pub user_name: String, + pub user_handle: Vec, + pub record_identifier: Option, + pub client_data_hash: Vec, + pub user_verification: UserVerification, + } + + #[napi(object)] + #[derive(Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct PasskeyAssertionResponse { + pub rp_id: String, + pub user_handle: Vec, + pub signature: Vec, + pub client_data_hash: Vec, + pub authenticator_data: Vec, + pub credential_id: Vec, + } + + #[napi] + pub struct IpcServer { + server: desktop_core::ipc::server::Server, + } + + #[napi] + impl IpcServer { + /// Create and start the IPC server without blocking. + /// + /// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client. + /// @param callback This function will be called whenever a message is received from a client. + #[napi(factory)] + pub async fn listen( + name: String, + // Ideally we'd have a single callback that has an enum containing the request values, + // but NAPI doesn't support that just yet + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void" + )] + registration_callback: ThreadsafeFunction< + (u32, u32, PasskeyRegistrationRequest), + ErrorStrategy::CalleeHandled, + >, + #[napi( + ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void" + )] + assertion_callback: ThreadsafeFunction< + (u32, u32, PasskeyAssertionRequest), + ErrorStrategy::CalleeHandled, + >, + ) -> napi::Result { + let (send, mut recv) = tokio::sync::mpsc::channel::(32); + tokio::spawn(async move { + while let Some(Message { + client_id, + kind, + message, + }) = recv.recv().await + { + match kind { + // TODO: We're ignoring the connection and disconnection messages for now + MessageType::Connected | MessageType::Disconnected => continue, + MessageType::Message => { + let Some(message) = message else { + println!("[ERROR] Message is empty"); + continue; + }; + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + + assertion_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + println!("[ERROR] Error deserializing message1: {e}"); + } + } + + match serde_json::from_str::>( + &message, + ) { + Ok(msg) => { + let value = msg + .value + .map(|value| (client_id, msg.sequence_number, value)) + .map_err(|e| napi::Error::from_reason(format!("{e:?}"))); + registration_callback + .call(value, ThreadsafeFunctionCallMode::NonBlocking); + continue; + } + Err(e) => { + println!("[ERROR] Error deserializing message2: {e}"); + } + } + + println!("[ERROR] Received an unknown message2: {message:?}"); + } + } + } + }); + + let path = desktop_core::ipc::path(&name); + + let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| { + napi::Error::from_reason(format!( + "Error listening to server - Path: {path:?} - Error: {e} - {e:?}" + )) + })?; + + Ok(IpcServer { server }) + } + + /// Return the path to the IPC server. + #[napi] + pub fn get_path(&self) -> String { + self.server.path.to_string_lossy().to_string() + } + + /// Stop the IPC server. + #[napi] + pub fn stop(&self) -> napi::Result<()> { + self.server.stop(); + Ok(()) + } + + #[napi] + pub fn complete_registration( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyRegistrationResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_assertion( + &self, + client_id: u32, + sequence_number: u32, + response: PasskeyAssertionResponse, + ) -> napi::Result { + let message = PasskeyMessage { + sequence_number, + value: Ok(response), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + #[napi] + pub fn complete_error( + &self, + client_id: u32, + sequence_number: u32, + error: String, + ) -> napi::Result { + let message: PasskeyMessage<()> = PasskeyMessage { + sequence_number, + value: Err(BitwardenError::Internal(error)), + }; + self.send(client_id, serde_json::to_string(&message).unwrap()) + } + + // TODO: Add a way to send a message to a specific client? + fn send(&self, _client_id: u32, message: String) -> napi::Result { + self.server + .send(message) + .map_err(|e| { + napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}")) + }) + // NAPI doesn't support u64 or usize, so we need to convert to u32 + .map(|u| u32::try_from(u).unwrap_or_default()) + } + } } #[napi] diff --git a/apps/desktop/macos/.gitignore b/apps/desktop/macos/.gitignore new file mode 100644 index 00000000000..e5d4324b213 --- /dev/null +++ b/apps/desktop/macos/.gitignore @@ -0,0 +1 @@ +BitwardenMacosProvider.swift diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index 4d03bf97e6c..dbaa8517086 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -9,8 +9,46 @@ import AuthenticationServices import os class CredentialProviderViewController: ASCredentialProviderViewController { - let logger = Logger() + let logger: Logger + + // There is something a bit strange about the initialization/deinitialization in this class. + // Sometimes deinit won't be called after a request has successfully finished, + // which would leave this class hanging in memory and the IPC connection open. + // + // If instead I make this a static, the deinit gets called correctly after each request. + // I think we still might want a static regardless, to be able to reuse the connection if possible. + static let client: MacOsProviderClient = { + let instance = MacOsProviderClient.connect() + // setup code + return instance + }() + + init() { + logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider") + + logger.log("[autofill-extension] initializing extension") + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + logger.log("[autofill-extension] deinitializing extension") + } + + + @IBAction func cancel(_ sender: AnyObject?) { + self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) + } + + @IBAction func passwordSelected(_ sender: AnyObject?) { + let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") + self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) + } + /* Implement this method if your extension supports showing credentials in the QuickType bar. When the user selects a credential from your app, this method will be called with the @@ -21,7 +59,14 @@ class CredentialProviderViewController: ASCredentialProviderViewController { */ + // Deprecated override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) { + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction called \(credentialIdentity)") + logger.log("[autofill-extension] user \(credentialIdentity.user)") + logger.log("[autofill-extension] id \(credentialIdentity.recordIdentifier ?? "")") + logger.log("[autofill-extension] sid \(credentialIdentity.serviceIdentifier.identifier)") + logger.log("[autofill-extension] sidt \(credentialIdentity.serviceIdentifier.type.rawValue)") + // let databaseIsUnlocked = true // if (databaseIsUnlocked) { let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234") @@ -31,6 +76,67 @@ class CredentialProviderViewController: ASCredentialProviderViewController { // } } + override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) { + if let request = credentialRequest as? ASPasskeyCredentialRequest { + if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity { + + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(passkey) called \(request)") + + class CallbackImpl: PreparePasskeyAssertionCallback { + let ctx: ASCredentialProviderExtensionContext + required init(_ ctx: ASCredentialProviderExtensionContext) { + self.ctx = ctx + } + + func onComplete(credential: PasskeyAssertionResponse) { + ctx.completeAssertionRequest(using: ASPasskeyAssertionCredential( + userHandle: credential.userHandle, + relyingParty: credential.rpId, + signature: credential.signature, + clientDataHash: credential.clientDataHash, + authenticatorData: credential.authenticatorData, + credentialID: credential.credentialId + )) + } + + func onError(error: BitwardenError) { + ctx.cancelRequest(withError: error) + } + } + + let userVerification = switch request.userVerificationPreference { + case .preferred: + UserVerification.preferred + case .required: + UserVerification.required + default: + UserVerification.discouraged + } + + let req = PasskeyAssertionRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + credentialId: passkeyIdentity.credentialID, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + recordIdentifier: passkeyIdentity.recordIdentifier, + clientDataHash: request.clientDataHash, + userVerification: userVerification + ) + + CredentialProviderViewController.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext)) + return + } + } + + if let request = credentialRequest as? ASPasswordCredentialRequest { + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(password) called \(request)") + return; + } + + logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2 called wrong") + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request")) + } + /* Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with ASExtensionError.userInteractionRequired. In this case, the system may present your extension's @@ -41,34 +147,65 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } */ - @IBAction func cancel(_ sender: AnyObject?) { - self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue)) - } - @IBAction func passwordSelected(_ sender: AnyObject?) { - let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234") - self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil) - } - override func prepareInterfaceForExtensionConfiguration() { logger.log("[autofill-extension] prepareInterfaceForExtensionConfiguration called") } override func prepareInterface(forPasskeyRegistration registrationRequest: ASCredentialRequest) { - logger.log("[autofill-extension] prepare interface for registration request \(registrationRequest.description)") - -// self.extensionContext.cancelRequest(withError: ExampleError.nope) - } + if let request = registrationRequest as? ASPasskeyCredentialRequest { + if let passkeyIdentity = registrationRequest.credentialIdentity as? ASPasskeyCredentialIdentity { + class CallbackImpl: PreparePasskeyRegistrationCallback { + let ctx: ASCredentialProviderExtensionContext + required init(_ ctx: ASCredentialProviderExtensionContext) { + self.ctx = ctx + } + + func onComplete(credential: PasskeyRegistrationResponse) { + ctx.completeRegistrationRequest(using: ASPasskeyRegistrationCredential( + relyingParty: credential.rpId, + clientDataHash: credential.clientDataHash, + credentialID: credential.credentialId, + attestationObject: credential.attestationObject + )) + } + + func onError(error: BitwardenError) { + ctx.cancelRequest(withError: error) + } + } + + let userVerification = switch request.userVerificationPreference { + case .preferred: + UserVerification.preferred + case .required: + UserVerification.required + default: + UserVerification.discouraged + } + + let req = PasskeyRegistrationRequest( + rpId: passkeyIdentity.relyingPartyIdentifier, + userName: passkeyIdentity.userName, + userHandle: passkeyIdentity.userHandle, + clientDataHash: request.clientDataHash, + userVerification: userVerification, + supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) } + ) + CredentialProviderViewController.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext)) + return + } + } - override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) { - logger.log("[autofill-extension] prepare interface for credential request \(credentialRequest.description)") + // If we didn't get a passkey, return an error + self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid registration request")) } /* - Prepare your UI to list available credentials for the user to choose from. The items in - 'serviceIdentifiers' describe the service the user is logging in to, so your extension can - prioritize the most relevant credentials in the list. - */ + Prepare your UI to list available credentials for the user to choose from. The items in + 'serviceIdentifiers' describe the service the user is logging in to, so your extension can + prioritize the most relevant credentials in the list. + */ override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) { logger.log("[autofill-extension] prepareCredentialList for serviceIdentifiers: \(serviceIdentifiers.count)") @@ -77,18 +214,13 @@ class CredentialProviderViewController: ASCredentialProviderViewController { } } - override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) { - logger.log("[autofill-extension] prepareInterfaceToProvideCredential for credentialIdentity: \(credentialIdentity.user)") - } - override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier], requestParameters: ASPasskeyCredentialRequestParameters) { logger.log("[autofill-extension] prepareCredentialList(passkey) for serviceIdentifiers: \(serviceIdentifiers.count)") - + logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)") + for serviceIdentifier in serviceIdentifiers { logger.log(" service: \(serviceIdentifier.identifier)") } - - logger.log("request parameters: \(requestParameters.relyingPartyIdentifier)") } } diff --git a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements index 2e600a8d529..86c7195768e 100644 --- a/apps/desktop/macos/autofill-extension/autofill_extension.entitlements +++ b/apps/desktop/macos/autofill-extension/autofill_extension.entitlements @@ -6,5 +6,9 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + LTZ2PFU5D6.com.bitwarden.desktop + diff --git a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj index 313b158895c..2ac467f3289 100644 --- a/apps/desktop/macos/desktop.xcodeproj/project.pbxproj +++ b/apps/desktop/macos/desktop.xcodeproj/project.pbxproj @@ -7,12 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */; }; + 3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */; }; E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; }; E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; }; E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = ""; }; + 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = ""; }; 968ED08A2C52A47200FFFEE6 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; @@ -28,6 +32,7 @@ buildActionMask = 2147483647; files = ( E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */, + 3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -56,6 +61,7 @@ isa = PBXGroup; children = ( E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */, + 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */, ); name = Frameworks; sourceTree = ""; @@ -63,6 +69,7 @@ E1DF71402B342F6900F29026 /* autofill-extension */ = { isa = PBXGroup; children = ( + 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */, E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */, E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */, E1DF71462B342F6900F29026 /* Info.plist */, @@ -140,6 +147,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */, E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 99953603e45..10d5ded3448 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -23,7 +23,7 @@ "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", "build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch", - "build:macos-extension": "node scripts/build-macos-extension.js", + "build:macos-extension": "./desktop_native/macos_provider/build.sh && node scripts/build-macos-extension.js", "build:main": "cross-env NODE_ENV=production webpack --config webpack.main.js", "build:main:dev": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js", "build:main:watch": "npm run build-native && cross-env NODE_ENV=development webpack --config webpack.main.js --watch", diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index ccce1e3bd7c..8fa33215eb5 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -56,6 +56,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction"; +import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -73,6 +75,7 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; +import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory"; import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory"; @@ -80,6 +83,7 @@ import { SystemService } from "@bitwarden/common/platform/services/system.servic import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -99,6 +103,7 @@ import { DesktopAutofillSettingsService } from "../../autofill/services/desktop- import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service"; import { flagEnabled } from "../../platform/flags"; +import { DesktopFido2UserInterfaceService } from "../../platform/services/desktop-fido2-user-interface.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { ElectronKeyService } from "../../platform/services/electron-key.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; @@ -309,7 +314,29 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: DesktopAutofillService, - deps: [LogService, CipherServiceAbstraction, ConfigService], + deps: [ + LogService, + CipherServiceAbstraction, + ConfigService, + Fido2AuthenticatorServiceAbstraction, + AccountService, + ], + }), + safeProvider({ + provide: Fido2UserInterfaceServiceAbstraction, + useClass: DesktopFido2UserInterfaceService, + deps: [AuthServiceAbstraction, CipherServiceAbstraction, AccountService, LogService], + }), + safeProvider({ + provide: Fido2AuthenticatorServiceAbstraction, + useClass: Fido2AuthenticatorService, + deps: [ + CipherServiceAbstraction, + Fido2UserInterfaceServiceAbstraction, + SyncService, + AccountService, + LogService, + ], }), safeProvider({ provide: NativeMessagingManifestService, diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 9ce5e1319fd..494544f5858 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -1,9 +1,92 @@ import { ipcRenderer } from "electron"; +import type { autofill } from "@bitwarden/desktop-napi"; + import { Command } from "../platform/main/autofill/command"; import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main"; export default { runCommand: (params: RunCommandParams): Promise> => ipcRenderer.invoke("autofill.runCommand", params), + + listenPasskeyRegistration: ( + fn: ( + clientId: number, + sequenceNumber: number, + request: autofill.PasskeyRegistrationRequest, + completeCallback: ( + error: Error | null, + response: autofill.PasskeyRegistrationResponse, + ) => void, + ) => void, + ) => { + ipcRenderer.on( + "autofill.passkeyRegistration", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + request: autofill.PasskeyRegistrationRequest; + }, + ) => { + const { clientId, sequenceNumber, request } = data; + fn(clientId, sequenceNumber, request, (error, response) => { + if (error) { + ipcRenderer.send("autofill.completeError", { + clientId, + sequenceNumber, + error: error.message, + }); + return; + } + + ipcRenderer.send("autofill.completePasskeyRegistration", { + clientId, + sequenceNumber, + response, + }); + }); + }, + ); + }, + + listenPasskeyAssertion: ( + fn: ( + clientId: number, + sequenceNumber: number, + request: autofill.PasskeyAssertionRequest, + completeCallback: (error: Error | null, response: autofill.PasskeyAssertionResponse) => void, + ) => void, + ) => { + ipcRenderer.on( + "autofill.passkeyAssertion", + ( + event, + data: { + clientId: number; + sequenceNumber: number; + request: autofill.PasskeyAssertionRequest; + }, + ) => { + const { clientId, sequenceNumber, request } = data; + fn(clientId, sequenceNumber, request, (error, response) => { + if (error) { + ipcRenderer.send("autofill.completeError", { + clientId, + sequenceNumber, + error: error.message, + }); + return; + } + + ipcRenderer.send("autofill.completePasskeyAssertion", { + clientId, + sequenceNumber, + response, + }); + }); + }, + ); + }, }; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 8c5dd597850..1ce58596b34 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -1,12 +1,32 @@ import { Injectable, OnDestroy } from "@angular/core"; -import { EMPTY, Subject, distinctUntilChanged, mergeMap, switchMap, takeUntil } from "rxjs"; +import { autofill } from "desktop_native/napi"; +import { + EMPTY, + Subject, + distinctUntilChanged, + firstValueFrom, + map, + mergeMap, + switchMap, + takeUntil, +} from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { + Fido2AuthenticatorGetAssertionParams, + Fido2AuthenticatorGetAssertionResult, + Fido2AuthenticatorMakeCredentialResult, + Fido2AuthenticatorMakeCredentialsParams, + Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction, +} from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils"; +import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; +import { guidToRawFormat } from "@bitwarden/common/platform/services/fido2/guid-utils"; 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"; @@ -26,6 +46,8 @@ export class DesktopAutofillService implements OnDestroy { private logService: LogService, private cipherService: CipherService, private configService: ConfigService, + private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction, + private accountService: AccountService, ) {} async init() { @@ -47,6 +69,8 @@ export class DesktopAutofillService implements OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this.listenIpc(); } /** Give metadata about all available credentials in the users vault */ @@ -114,6 +138,146 @@ export class DesktopAutofillService implements OnDestroy { }); } + listenIpc() { + ipc.autofill.listenPasskeyRegistration((clientId, sequenceNumber, request, callback) => { + this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request); + this.logService.warning( + "listenPasskeyRegistration2", + this.convertRegistrationRequest(request), + ); + + const controller = new AbortController(); + void this.fido2AuthenticatorService + .makeCredential(this.convertRegistrationRequest(request), null, controller) + .then((response) => { + callback(null, this.convertRegistrationResponse(request, response)); + }) + .catch((error) => { + this.logService.error("listenPasskeyRegistration error", error); + callback(error, null); + }); + }); + + ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => { + this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request); + + // TODO: For some reason the credentialId is passed as an empty array in the request, so we need to + // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. + if (request.recordIdentifier && request.credentialId.length === 0) { + const cipher = await this.cipherService.get(request.recordIdentifier); + if (!cipher) { + this.logService.error("listenPasskeyAssertion error", "Cipher not found"); + callback(new Error("Cipher not found"), null); + return; + } + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const decrypted = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + + const fido2Credential = decrypted.login.fido2Credentials?.[0]; + if (!fido2Credential) { + this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found"); + callback(new Error("Fido2Credential not found"), null); + return; + } + + request.credentialId = Array.from( + guidToRawFormat(decrypted.login.fido2Credentials?.[0].credentialId), + ); + } + + const controller = new AbortController(); + void this.fido2AuthenticatorService + .getAssertion(this.convertAssertionRequest(request), null, controller) + .then((response) => { + callback(null, this.convertAssertionResponse(request, response)); + }) + .catch((error) => { + this.logService.error("listenPasskeyAssertion error", error); + callback(error, null); + }); + }); + } + + private convertRegistrationRequest( + request: autofill.PasskeyRegistrationRequest, + ): Fido2AuthenticatorMakeCredentialsParams { + return { + hash: new Uint8Array(request.clientDataHash), + rpEntity: { + name: request.rpId, + id: request.rpId, + }, + userEntity: { + id: new Uint8Array(request.userHandle), + name: request.userName, + displayName: undefined, + icon: undefined, + }, + credTypesAndPubKeyAlgs: request.supportedAlgorithms.map((alg) => ({ + alg, + type: "public-key", + })), + excludeCredentialDescriptorList: [], + requireResidentKey: true, + requireUserVerification: + request.userVerification === "required" || request.userVerification === "preferred", + fallbackSupported: false, + }; + } + + private convertRegistrationResponse( + request: autofill.PasskeyRegistrationRequest, + response: Fido2AuthenticatorMakeCredentialResult, + ): autofill.PasskeyRegistrationResponse { + return { + rpId: request.rpId, + clientDataHash: request.clientDataHash, + credentialId: Array.from(Fido2Utils.bufferSourceToUint8Array(response.credentialId)), + attestationObject: Array.from( + Fido2Utils.bufferSourceToUint8Array(response.attestationObject), + ), + }; + } + + private convertAssertionRequest( + request: autofill.PasskeyAssertionRequest, + ): Fido2AuthenticatorGetAssertionParams { + return { + rpId: request.rpId, + hash: new Uint8Array(request.clientDataHash), + allowCredentialDescriptorList: [ + { + id: new Uint8Array(request.credentialId), + type: "public-key", + }, + ], + extensions: {}, + requireUserVerification: + request.userVerification === "required" || request.userVerification === "preferred", + fallbackSupported: false, + }; + } + + private convertAssertionResponse( + request: autofill.PasskeyAssertionRequest, + response: Fido2AuthenticatorGetAssertionResult, + ): autofill.PasskeyAssertionResponse { + return { + userHandle: Array.from(response.selectedCredential.userHandle), + rpId: request.rpId, + signature: Array.from(response.signature), + clientDataHash: request.clientDataHash, + authenticatorData: Array.from(response.authenticatorData), + credentialId: Array.from(response.selectedCredential.id), + }; + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index b19f25256b2..a4842249c93 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -261,7 +261,7 @@ export class Main { new EphemeralValueStorageService(); new SSOLocalhostCallbackService(this.environmentService, this.messagingService); - this.nativeAutofillMain = new NativeAutofillMain(this.logService); + this.nativeAutofillMain = new NativeAutofillMain(this.logService, this.windowMain); void this.nativeAutofillMain.init(); } diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts index b4b7895e8ac..1465831340f 100644 --- a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -3,6 +3,8 @@ import { ipcMain } from "electron"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { autofill } from "@bitwarden/desktop-napi"; +import { WindowMain } from "../../../main/window.main"; + import { CommandDefinition } from "./command"; export type RunCommandParams = { @@ -14,7 +16,12 @@ export type RunCommandParams = { export type RunCommandResult = C["output"]; export class NativeAutofillMain { - constructor(private logService: LogService) {} + private ipcServer: autofill.IpcServer | null; + + constructor( + private logService: LogService, + private windowMain: WindowMain, + ) {} async init() { ipcMain.handle( @@ -26,6 +33,52 @@ export class NativeAutofillMain { return this.runCommand(params); }, ); + + this.ipcServer = await autofill.IpcServer.listen( + "autofill", + // RegistrationCallback + (error, clientId, sequenceNumber, request) => { + if (error) { + this.logService.error("autofill.IpcServer.registration", error); + return; + } + this.windowMain.win.webContents.send("autofill.passkeyRegistration", { + clientId, + sequenceNumber, + request, + }); + }, + // AssertionCallback + (error, clientId, sequenceNumber, request) => { + if (error) { + this.logService.error("autofill.IpcServer.assertion", error); + return; + } + this.windowMain.win.webContents.send("autofill.passkeyAssertion", { + clientId, + sequenceNumber, + request, + }); + }, + ); + + ipcMain.on("autofill.completePasskeyRegistration", (event, data) => { + this.logService.warning("autofill.completePasskeyRegistration", data); + const { clientId, sequenceNumber, response } = data; + this.ipcServer.completeRegistration(clientId, sequenceNumber, response); + }); + + ipcMain.on("autofill.completePasskeyAssertion", (event, data) => { + this.logService.warning("autofill.completePasskeyAssertion", data); + const { clientId, sequenceNumber, response } = data; + this.ipcServer.completeAssertion(clientId, sequenceNumber, response); + }); + + ipcMain.on("autofill.completeError", (event, data) => { + this.logService.warning("autofill.completeError", data); + const { clientId, sequenceNumber, error } = data; + this.ipcServer.completeAssertion(clientId, sequenceNumber, error); + }); } private async runCommand( diff --git a/apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts b/apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts new file mode 100644 index 00000000000..116e8989e02 --- /dev/null +++ b/apps/desktop/src/platform/services/desktop-fido2-user-interface.service.ts @@ -0,0 +1,125 @@ +import { firstValueFrom, map } 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 { + Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction, + Fido2UserInterfaceSession, + NewCredentialParams, + PickCredentialParams, +} from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; + +// TODO: This should be moved to the directory of whatever team takes this on + +export class DesktopFido2UserInterfaceService + implements Fido2UserInterfaceServiceAbstraction +{ + constructor( + private authService: AuthService, + private cipherService: CipherService, + private accountService: AccountService, + private logService: LogService, + ) {} + + async newSession( + fallbackSupported: boolean, + _tab: void, + abortController?: AbortController, + ): Promise { + this.logService.warning("newSession", fallbackSupported, abortController); + return new DesktopFido2UserInterfaceSession( + this.authService, + this.cipherService, + this.accountService, + this.logService, + ); + } +} + +export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSession { + constructor( + private authService: AuthService, + private cipherService: CipherService, + private accountService: AccountService, + private logService: LogService, + ) {} + + async pickCredential({ + cipherIds, + userVerification, + }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { + this.logService.warning("pickCredential", cipherIds, userVerification); + + return { cipherId: cipherIds[0], userVerified: userVerification }; + } + + async confirmNewCredential({ + credentialName, + userName, + userVerification, + rpId, + }: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> { + this.logService.warning( + "confirmNewCredential", + credentialName, + userName, + userVerification, + rpId, + ); + + // Store the passkey on a new cipher to avoid replacing something important + const cipher = new CipherView(); + cipher.name = credentialName; + + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + cipher.login.username = userName; + cipher.login.uris = [new LoginUriView()]; + cipher.login.uris[0].uri = "https://" + rpId; + cipher.card = new CardView(); + cipher.identity = new IdentityView(); + cipher.secureNote = new SecureNoteView(); + cipher.secureNote.type = SecureNoteType.Generic; + cipher.reprompt = CipherRepromptType.None; + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const encCipher = await this.cipherService.encrypt(cipher, activeUserId); + const createdCipher = await this.cipherService.createWithServer(encCipher); + + return { cipherId: createdCipher.id, userVerified: userVerification }; + } + + async informExcludedCredential(existingCipherIds: string[]): Promise { + this.logService.warning("informExcludedCredential", existingCipherIds); + } + + async ensureUnlockedVault(): Promise { + this.logService.warning("ensureUnlockedVault"); + + const status = await firstValueFrom(this.authService.activeAccountStatus$); + if (status !== AuthenticationStatus.Unlocked) { + throw new Error("Vault is not unlocked"); + } + } + + async informCredentialNotFound(): Promise { + this.logService.warning("informCredentialNotFound"); + } + + async close() { + this.logService.warning("close"); + } +} diff --git a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts index bacbafcb323..e9e68ca92c3 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-authenticator.service.abstraction.ts @@ -8,7 +8,7 @@ import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential * * The authenticator provides key management and cryptographic signatures. */ -export abstract class Fido2AuthenticatorService { +export abstract class Fido2AuthenticatorService { /** * Create and save a new credential as described in: * https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred @@ -19,7 +19,7 @@ export abstract class Fido2AuthenticatorService { **/ makeCredential: ( params: Fido2AuthenticatorMakeCredentialsParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; @@ -33,7 +33,7 @@ export abstract class Fido2AuthenticatorService { */ getAssertion: ( params: Fido2AuthenticatorGetAssertionParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; diff --git a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts index d9cb20995ad..55d9cce8049 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-client.service.abstraction.ts @@ -15,7 +15,7 @@ export type UserVerification = "discouraged" | "preferred" | "required"; * It is responsible for both marshalling the inputs for the underlying authenticator operations, * and for returning the results of the latter operations to the Web Authentication API's callers. */ -export abstract class Fido2ClientService { +export abstract class Fido2ClientService { isFido2FeatureEnabled: (hostname: string, origin: string) => Promise; /** @@ -28,7 +28,7 @@ export abstract class Fido2ClientService { */ createCredential: ( params: CreateCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; @@ -43,7 +43,7 @@ export abstract class Fido2ClientService { */ assertCredential: ( params: AssertCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; } diff --git a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts index 49752138527..7beefc3b4cc 100644 --- a/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/fido2/fido2-user-interface.service.abstraction.ts @@ -61,7 +61,7 @@ export interface PickCredentialParams { * The service is session based and is intended to be used by the FIDO2 authenticator to open a window, * and then use this window to ask the user for input and/or display messages to the user. */ -export abstract class Fido2UserInterfaceService { +export abstract class Fido2UserInterfaceService { /** * Creates a new session. * Note: This will not necessarily open a window until it is needed to request something from the user. @@ -71,7 +71,7 @@ export abstract class Fido2UserInterfaceService { */ newSession: ( fallbackSupported: boolean, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ) => Promise; } diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts index e3f79ff9d58..226f4c2cfe9 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts @@ -30,6 +30,8 @@ import { parseCredentialId } from "./credential-id-utils"; import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service"; import { Fido2Utils } from "./fido2-utils"; +type ParentWindowReference = string; + const RpId = "bitwarden.com"; describe("FidoAuthenticatorService", () => { @@ -41,16 +43,16 @@ describe("FidoAuthenticatorService", () => { }); let cipherService!: MockProxy; - let userInterface!: MockProxy; + let userInterface!: MockProxy>; let userInterfaceSession!: MockProxy; let syncService!: MockProxy; let accountService!: MockProxy; - let authenticator!: Fido2AuthenticatorService; - let tab!: chrome.tabs.Tab; + let authenticator!: Fido2AuthenticatorService; + let windowReference!: ParentWindowReference; beforeEach(async () => { cipherService = mock(); - userInterface = mock(); + userInterface = mock>(); userInterfaceSession = mock(); userInterface.newSession.mockResolvedValue(userInterfaceSession); syncService = mock({ @@ -63,7 +65,7 @@ describe("FidoAuthenticatorService", () => { syncService, accountService, ); - tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; + windowReference = Utils.newGuid(); accountService.activeAccount$ = activeAccountSubject; }); @@ -78,19 +80,21 @@ describe("FidoAuthenticatorService", () => { // Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation. it("should throw error when input does not contain any supported algorithms", async () => { const result = async () => - await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, tab); + await authenticator.makeCredential(invalidParams.unsupportedAlgorithm, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotSupported); }); it("should throw error when requireResidentKey has invalid value", async () => { - const result = async () => await authenticator.makeCredential(invalidParams.invalidRk, tab); + const result = async () => + await authenticator.makeCredential(invalidParams.invalidRk, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); it("should throw error when requireUserVerification has invalid value", async () => { - const result = async () => await authenticator.makeCredential(invalidParams.invalidUv, tab); + const result = async () => + await authenticator.makeCredential(invalidParams.invalidUv, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -103,7 +107,7 @@ describe("FidoAuthenticatorService", () => { it.skip("should throw error if requireUserVerification is set to true", async () => { const params = await createParams({ requireUserVerification: true }); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint); }); @@ -117,7 +121,7 @@ describe("FidoAuthenticatorService", () => { for (const p of Object.values(invalidParams)) { try { - await authenticator.makeCredential(p, tab); + await authenticator.makeCredential(p, windowReference); // eslint-disable-next-line no-empty } catch {} } @@ -158,7 +162,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informExcludedCredential.mockResolvedValue(); try { - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -169,7 +173,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error", async () => { userInterfaceSession.informExcludedCredential.mockResolvedValue(); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -180,7 +184,7 @@ describe("FidoAuthenticatorService", () => { excludedCipher.organizationId = "someOrganizationId"; try { - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -193,7 +197,7 @@ describe("FidoAuthenticatorService", () => { for (const p of Object.values(invalidParams)) { try { - await authenticator.makeCredential(p, tab); + await authenticator.makeCredential(p, windowReference); // eslint-disable-next-line no-empty } catch {} } @@ -230,7 +234,7 @@ describe("FidoAuthenticatorService", () => { userVerified: userVerification, }); - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); expect(userInterfaceSession.confirmNewCredential).toHaveBeenCalledWith({ credentialName: params.rpEntity.name, @@ -250,7 +254,7 @@ describe("FidoAuthenticatorService", () => { }); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); - await authenticator.makeCredential(params, tab); + await authenticator.makeCredential(params, windowReference); const saved = cipherService.encrypt.mock.lastCall?.[0]; expect(saved).toEqual( @@ -288,7 +292,7 @@ describe("FidoAuthenticatorService", () => { }); const params = await createParams(); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -302,7 +306,7 @@ describe("FidoAuthenticatorService", () => { const encryptedCipher = { ...existingCipher, reprompt: CipherRepromptType.Password }; cipherService.get.mockResolvedValue(encryptedCipher as unknown as Cipher); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -317,7 +321,7 @@ describe("FidoAuthenticatorService", () => { cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); - const result = async () => await authenticator.makeCredential(params, tab); + const result = async () => await authenticator.makeCredential(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -358,7 +362,7 @@ describe("FidoAuthenticatorService", () => { }); it("should return attestation object", async () => { - const result = await authenticator.makeCredential(params, tab); + const result = await authenticator.makeCredential(params, windowReference); const attestationObject = CBOR.decode( Fido2Utils.bufferSourceToUint8Array(result.attestationObject).buffer, @@ -455,7 +459,8 @@ describe("FidoAuthenticatorService", () => { describe("invalid input parameters", () => { it("should throw error when requireUserVerification has invalid value", async () => { - const result = async () => await authenticator.getAssertion(invalidParams.invalidUv, tab); + const result = async () => + await authenticator.getAssertion(invalidParams.invalidUv, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); @@ -468,7 +473,7 @@ describe("FidoAuthenticatorService", () => { it.skip("should throw error if requireUserVerification is set to true", async () => { const params = await createParams({ requireUserVerification: true }); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Constraint); }); @@ -498,7 +503,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informCredentialNotFound.mockResolvedValue(); try { - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -513,7 +518,7 @@ describe("FidoAuthenticatorService", () => { userInterfaceSession.informCredentialNotFound.mockResolvedValue(); try { - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); // eslint-disable-next-line no-empty } catch {} @@ -534,7 +539,7 @@ describe("FidoAuthenticatorService", () => { /** Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. */ it("should throw error", async () => { - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -573,7 +578,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: ciphers.map((c) => c.id), @@ -590,7 +595,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: [discoverableCiphers[0].id], @@ -608,7 +613,7 @@ describe("FidoAuthenticatorService", () => { userVerified: userVerification, }); - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({ cipherIds: ciphers.map((c) => c.id), @@ -625,7 +630,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -637,7 +642,7 @@ describe("FidoAuthenticatorService", () => { userVerified: false, }); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.NotAllowed); }); @@ -686,7 +691,7 @@ describe("FidoAuthenticatorService", () => { cipherService.encrypt.mockResolvedValue(encrypted as any); ciphers[0].login.fido2Credentials[0].counter = 9000; - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted); expect(cipherService.encrypt).toHaveBeenCalledWith( @@ -710,13 +715,13 @@ describe("FidoAuthenticatorService", () => { cipherService.encrypt.mockResolvedValue(encrypted as any); ciphers[0].login.fido2Credentials[0].counter = 0; - await authenticator.getAssertion(params, tab); + await authenticator.getAssertion(params, windowReference); expect(cipherService.updateWithServer).not.toHaveBeenCalled(); }); it("should return an assertion result", async () => { - const result = await authenticator.getAssertion(params, tab); + const result = await authenticator.getAssertion(params, windowReference); const encAuthData = result.authenticatorData; const rpIdHash = encAuthData.slice(0, 32); @@ -757,7 +762,7 @@ describe("FidoAuthenticatorService", () => { for (let i = 0; i < 10; ++i) { await init(); // Reset inputs - const result = await authenticator.getAssertion(params, tab); + const result = await authenticator.getAssertion(params, windowReference); const counter = result.authenticatorData.slice(33, 37); expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // double check that the counter doesn't change @@ -774,7 +779,7 @@ describe("FidoAuthenticatorService", () => { it("should throw unkown error if creation fails", async () => { cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); - const result = async () => await authenticator.getAssertion(params, tab); + const result = async () => await authenticator.getAssertion(params, windowReference); await expect(result).rejects.toThrowError(Fido2AuthenticatorErrorCode.Unknown); }); diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 34117e852ea..376f4dcdced 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -43,10 +43,12 @@ const KeyUsages: KeyUsage[] = ["sign"]; * * It is highly recommended that the W3C specification is used a reference when reading this code. */ -export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstraction { +export class Fido2AuthenticatorService + implements Fido2AuthenticatorServiceAbstraction +{ constructor( private cipherService: CipherService, - private userInterface: Fido2UserInterfaceService, + private userInterface: Fido2UserInterfaceService, private syncService: SyncService, private accountService: AccountService, private logService?: LogService, @@ -54,12 +56,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr async makeCredential( params: Fido2AuthenticatorMakeCredentialsParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, - tab, + window, abortController, ); @@ -209,12 +211,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr async getAssertion( params: Fido2AuthenticatorGetAssertionParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController?: AbortController, ): Promise { const userInterfaceSession = await this.userInterface.newSession( params.fallbackSupported, - tab, + window, abortController, ); try { diff --git a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts index 7ef705b95f9..31f6ce10e01 100644 --- a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts @@ -3,6 +3,9 @@ import { CipherType } from "../../../vault/enums"; import { CipherView } from "../../../vault/models/view/cipher.view"; import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view"; +import { Utils } from "../../misc/utils"; + +import { parseCredentialId } from "./credential-id-utils"; // TODO: Move into Fido2AuthenticatorService export async function getCredentialsForAutofill( @@ -15,9 +18,14 @@ export async function getCredentialsForAutofill( ) .map((cipher) => { const credential = cipher.login.fido2Credentials[0]; + + // Credentials are stored as a GUID or b64 string with `b64.` prepended, + // but we need to return them as a URL-safe base64 string + const credId = Utils.fromBufferToUrlB64(parseCredentialId(credential.credentialId)); + return { cipherId: cipher.id, - credentialId: credential.credentialId, + credentialId: credId, rpId: credential.rpId, userHandle: credential.userHandle, userName: credential.userName, diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts index 582849ebc12..51c3d8617ab 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts @@ -32,12 +32,14 @@ import { Fido2ClientService } from "./fido2-client.service"; import { Fido2Utils } from "./fido2-utils"; import { guidToRawFormat } from "./guid-utils"; +type ParentWindowReference = string; + const RpId = "bitwarden.com"; const Origin = "https://bitwarden.com"; const VaultUrl = "https://vault.bitwarden.com"; describe("FidoAuthenticatorService", () => { - let authenticator!: MockProxy; + let authenticator!: MockProxy>; let configService!: MockProxy; let authService!: MockProxy; let vaultSettingsService: MockProxy; @@ -45,12 +47,12 @@ describe("FidoAuthenticatorService", () => { let taskSchedulerService: MockProxy; let activeRequest!: MockProxy; let requestManager!: MockProxy; - let client!: Fido2ClientService; - let tab!: chrome.tabs.Tab; + let client!: Fido2ClientService; + let windowReference!: ParentWindowReference; let isValidRpId!: jest.SpyInstance; beforeEach(async () => { - authenticator = mock(); + authenticator = mock>(); configService = mock(); authService = mock(); vaultSettingsService = mock(); @@ -82,7 +84,7 @@ describe("FidoAuthenticatorService", () => { vaultSettingsService.enablePasskeys$ = of(true); domainSettingsService.neverDomains$ = of({}); authService.activeAccountStatus$ = of(AuthenticationStatus.Unlocked); - tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; + windowReference = Utils.newGuid(); }); afterEach(() => { @@ -95,7 +97,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error if sameOriginWithAncestors is false", async () => { const params = createParams({ sameOriginWithAncestors: false }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -106,7 +108,7 @@ describe("FidoAuthenticatorService", () => { it("should throw error if user.id is too small", async () => { const params = createParams({ user: { id: "", displayName: "displayName", name: "name" } }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); await expect(result).rejects.toBeInstanceOf(TypeError); }); @@ -121,7 +123,7 @@ describe("FidoAuthenticatorService", () => { }, }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); await expect(result).rejects.toBeInstanceOf(TypeError); }); @@ -136,7 +138,7 @@ describe("FidoAuthenticatorService", () => { origin: "invalid-domain-name", }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -151,7 +153,7 @@ describe("FidoAuthenticatorService", () => { rp: { id: "bitwarden.com", name: "Bitwarden" }, }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -165,7 +167,7 @@ describe("FidoAuthenticatorService", () => { // `params` actually has a valid rp.id, but we're mocking the function to return false isValidRpId.mockReturnValue(false); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -179,7 +181,7 @@ describe("FidoAuthenticatorService", () => { }); domainSettingsService.neverDomains$ = of({ "bitwarden.com": null }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); await expect(result).rejects.toThrow(FallbackRequestedError); }); @@ -190,7 +192,7 @@ describe("FidoAuthenticatorService", () => { rp: { id: "bitwarden.com", name: "Bitwarden" }, }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -204,7 +206,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - await client.createCredential(params, tab); + await client.createCredential(params, windowReference); }); // Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm. @@ -216,7 +218,7 @@ describe("FidoAuthenticatorService", () => { ], }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotSupportedError" }); @@ -231,7 +233,8 @@ describe("FidoAuthenticatorService", () => { const abortController = new AbortController(); abortController.abort(); - const result = async () => await client.createCredential(params, tab, abortController); + const result = async () => + await client.createCredential(params, windowReference, abortController); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "AbortError" }); @@ -246,7 +249,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - await client.createCredential(params, tab); + await client.createCredential(params, windowReference); expect(authenticator.makeCredential).toHaveBeenCalledWith( expect.objectContaining({ @@ -259,7 +262,7 @@ describe("FidoAuthenticatorService", () => { displayName: params.user.displayName, }), }), - tab, + windowReference, expect.anything(), ); }); @@ -271,7 +274,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - const result = await client.createCredential(params, tab); + const result = await client.createCredential(params, windowReference); expect(result.extensions.credProps?.rk).toBe(true); }); @@ -283,7 +286,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - const result = await client.createCredential(params, tab); + const result = await client.createCredential(params, windowReference); expect(result.extensions.credProps?.rk).toBe(false); }); @@ -295,7 +298,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); - const result = await client.createCredential(params, tab); + const result = await client.createCredential(params, windowReference); expect(result.extensions.credProps).toBeUndefined(); }); @@ -307,7 +310,7 @@ describe("FidoAuthenticatorService", () => { new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState), ); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "InvalidStateError" }); @@ -319,7 +322,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authenticator.makeCredential.mockRejectedValue(new Error("unknown error")); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -330,7 +333,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); vaultSettingsService.enablePasskeys$ = of(false); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -340,7 +343,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -349,7 +352,7 @@ describe("FidoAuthenticatorService", () => { it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => { const params = createParams({ origin: VaultUrl }); - const result = async () => await client.createCredential(params, tab); + const result = async () => await client.createCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -408,7 +411,7 @@ describe("FidoAuthenticatorService", () => { origin: "invalid-domain-name", }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -423,7 +426,7 @@ describe("FidoAuthenticatorService", () => { rpId: "bitwarden.com", }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -437,7 +440,7 @@ describe("FidoAuthenticatorService", () => { // `params` actually has a valid rp.id, but we're mocking the function to return false isValidRpId.mockReturnValue(false); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -451,7 +454,7 @@ describe("FidoAuthenticatorService", () => { domainSettingsService.neverDomains$ = of({ "bitwarden.com": null }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); await expect(result).rejects.toThrow(FallbackRequestedError); }); @@ -462,7 +465,7 @@ describe("FidoAuthenticatorService", () => { rpId: "bitwarden.com", }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "SecurityError" }); @@ -477,7 +480,8 @@ describe("FidoAuthenticatorService", () => { const abortController = new AbortController(); abortController.abort(); - const result = async () => await client.assertCredential(params, tab, abortController); + const result = async () => + await client.assertCredential(params, windowReference, abortController); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "AbortError" }); @@ -493,7 +497,7 @@ describe("FidoAuthenticatorService", () => { new Fido2AuthenticatorError(Fido2AuthenticatorErrorCode.InvalidState), ); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "InvalidStateError" }); @@ -505,7 +509,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authenticator.getAssertion.mockRejectedValue(new Error("unknown error")); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toMatchObject({ name: "NotAllowedError" }); @@ -516,7 +520,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); vaultSettingsService.enablePasskeys$ = of(false); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -526,7 +530,7 @@ describe("FidoAuthenticatorService", () => { const params = createParams(); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.LoggedOut); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -535,7 +539,7 @@ describe("FidoAuthenticatorService", () => { it("should throw FallbackRequestedError if origin equals the bitwarden vault", async () => { const params = createParams({ origin: VaultUrl }); - const result = async () => await client.assertCredential(params, tab); + const result = async () => await client.assertCredential(params, windowReference); const rejects = expect(result).rejects; await rejects.toThrow(FallbackRequestedError); @@ -555,7 +559,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledWith( expect.objectContaining({ @@ -573,7 +577,7 @@ describe("FidoAuthenticatorService", () => { }), ], }), - tab, + windowReference, expect.anything(), ); }); @@ -585,7 +589,7 @@ describe("FidoAuthenticatorService", () => { params.rpId = undefined; authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); }); }); @@ -597,7 +601,7 @@ describe("FidoAuthenticatorService", () => { }); authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledWith( expect.objectContaining({ @@ -605,7 +609,7 @@ describe("FidoAuthenticatorService", () => { rpId: RpId, allowCredentialDescriptorList: [], }), - tab, + windowReference, expect.anything(), ); }); @@ -627,7 +631,7 @@ describe("FidoAuthenticatorService", () => { }); it("creates an active mediated conditional request", async () => { - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(requestManager.newActiveRequest).toHaveBeenCalled(); expect(authenticator.getAssertion).toHaveBeenCalledWith( @@ -635,14 +639,14 @@ describe("FidoAuthenticatorService", () => { assumeUserPresence: true, rpId: RpId, }), - tab, + windowReference, ); }); it("restarts the mediated conditional request if a user aborts the request", async () => { authenticator.getAssertion.mockRejectedValueOnce(new Error()); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledTimes(2); }); @@ -652,7 +656,7 @@ describe("FidoAuthenticatorService", () => { abortController.abort(); authenticator.getAssertion.mockRejectedValueOnce(new DOMException("AbortError")); - await client.assertCredential(params, tab); + await client.assertCredential(params, windowReference); expect(authenticator.getAssertion).toHaveBeenCalledTimes(2); }); diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.ts b/libs/common/src/platform/services/fido2/fido2-client.service.ts index d08d1e2a42d..4bf30ef6537 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.ts @@ -47,7 +47,9 @@ import { guidToRawFormat } from "./guid-utils"; * * It is highly recommended that the W3C specification is used a reference when reading this code. */ -export class Fido2ClientService implements Fido2ClientServiceAbstraction { +export class Fido2ClientService + implements Fido2ClientServiceAbstraction +{ private timeoutAbortController: AbortController; private readonly TIMEOUTS = { NO_VERIFICATION: { @@ -63,7 +65,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { }; constructor( - private authenticator: Fido2AuthenticatorService, + private authenticator: Fido2AuthenticatorService, private configService: ConfigService, private authService: AuthService, private vaultSettingsService: VaultSettingsService, @@ -102,7 +104,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { async createCredential( params: CreateCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController = new AbortController(), ): Promise { const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); @@ -201,7 +203,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { try { makeCredentialResult = await this.authenticator.makeCredential( makeCredentialParams, - tab, + window, abortController, ); } catch (error) { @@ -256,7 +258,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { async assertCredential( params: AssertCredentialParams, - tab: chrome.tabs.Tab, + window: ParentWindowReference, abortController = new AbortController(), ): Promise { const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); @@ -300,7 +302,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { if (params.mediation === "conditional") { return this.handleMediatedConditionalRequest( params, - tab, + window, abortController, clientDataJSONBytes, ); @@ -324,7 +326,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { try { getAssertionResult = await this.authenticator.getAssertion( getAssertionParams, - tab, + window, abortController, ); } catch (error) { @@ -363,7 +365,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { private async handleMediatedConditionalRequest( params: AssertCredentialParams, - tab: chrome.tabs.Tab, + tab: ParentWindowReference, abortController: AbortController, clientDataJSONBytes: Uint8Array, ): Promise { @@ -379,7 +381,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { `[Fido2Client] started mediated request, available credentials: ${availableCredentials.length}`, ); const requestResult = await this.requestManager.newActiveRequest( - tab.id, + // TODO: This isn't correct, but this.requestManager.newActiveRequest expects a number, + // while this class is currently generic over ParentWindowReference. + // Consider moving requestManager into browser and adding support for ParentWindowReference => tab.id + (tab as any).id, availableCredentials, abortController, ); diff --git a/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts b/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts index 440bd519002..14b4da0ef1b 100644 --- a/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts +++ b/libs/common/src/platform/services/fido2/noop-fido2-user-interface.service.ts @@ -7,7 +7,7 @@ import { * Noop implementation of the {@link Fido2UserInterfaceService}. * This implementation does not provide any user interface. */ -export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { +export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction { newSession(): Promise { throw new Error("Not implemented exception"); } From 23212fb9ae33266a7cf22e40905daac0d09fb4dd Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 19 Dec 2024 21:53:39 +1000 Subject: [PATCH 39/57] Fix: users can import in PM if they can create new collections (#12472) --- libs/importer/src/components/import.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index b50be773251..0035fbdf10d 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -290,7 +290,9 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { private async initializeOrganizations() { this.organizations$ = concat( this.organizationService.memberOrganizations$.pipe( - map((orgs) => orgs.filter((org) => org.canAccessImport)), + // Import is an alternative way to create collections during onboarding, so import from Password Manager + // is available to any user who can create collections in the organization. + map((orgs) => orgs.filter((org) => org.canAccessImport || org.canCreateNewCollections)), map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), ), ); From 1d04a0a3998b3a83258c2a2350ebdd33b1dbe424 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 19 Dec 2024 09:59:42 -0500 Subject: [PATCH 40/57] [PM-8214] New Device Verification Notice UI (#12360) * starting * setup first page for new device verification notice * update designs for first page. rename components and files * added second page for new device verification notice * update notice page one with bit radio buttons. routing logic. user email * updated routing for new device verification notice to show before vault based on flags, and can navigate back to vault after submission * fix translations. added remind me later link and nav to page 2 * sync the design for mobile and web * update routes in desktop * updated styles for desktop * moved new device verification notice guard * update types for new device notice page one * add null check to page one * types * types for page one, page two, service, and guard * types * update component and guard for null check * add navigation to two step login btn and account email btn * remove empty file * update fill of icons to support light & dark modes * add question mark to email access verification copy * remove unused map * use links for navigation elements - an empty href is needed so the links are keyboard accessible * remove clip path from exclamation svg - No noticeable difference in the end result * inline email message into markup --------- Co-authored-by: Nick Krantz --- apps/browser/src/_locales/en/messages.json | 36 +++++++ apps/browser/src/popup/app-routing.module.ts | 35 ++++++- apps/desktop/src/app/app-routing.module.ts | 35 ++++++- apps/desktop/src/locales/en/messages.json | 36 +++++++ apps/desktop/tailwind.config.js | 1 + apps/web/src/app/oss-routing.module.ts | 35 ++++++- apps/web/src/locales/en/messages.json | 36 +++++++ .../src/services/jslib-services.module.ts | 2 + libs/angular/src/vault/guards/index.ts | 1 + .../new-device-verification-notice.guard.ts | 51 ++++++++++ ...erification-notice-page-one.component.html | 30 ++++++ ...-verification-notice-page-one.component.ts | 82 ++++++++++++++++ ...erification-notice-page-two.component.html | 39 ++++++++ ...-verification-notice-page-two.component.ts | 95 +++++++++++++++++++ libs/vault/src/icons/exclamation-triangle.ts | 7 ++ libs/vault/src/icons/index.ts | 2 + libs/vault/src/icons/user-lock.ts | 17 ++++ libs/vault/src/index.ts | 2 + .../new-device-verification-notice.service.ts | 2 +- 19 files changed, 540 insertions(+), 4 deletions(-) create mode 100644 libs/angular/src/vault/guards/index.ts create mode 100644 libs/angular/src/vault/guards/new-device-verification-notice.guard.ts create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html create mode 100644 libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts create mode 100644 libs/vault/src/icons/exclamation-triangle.ts create mode 100644 libs/vault/src/icons/user-lock.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index c2e9ef60d8c..de438a09467 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4910,6 +4910,42 @@ "beta": { "message": "Beta" }, + "importantNotice": { + "message": "Important notice" + }, + "setupTwoStepLogin": { + "message": "Set up two-step login" + }, + "newDeviceVerificationNoticeContentPage1": { + "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + }, + "newDeviceVerificationNoticeContentPage2": { + "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + }, + "remindMeLater": { + "message": "Remind me later" + }, + "newDeviceVerificationNoticePageOneFormContent": { + "message": "Do you have reliable access to your email, $EMAIL$?", + "placeholders": { + "email": { + "content": "$1", + "example": "your_name@email.com" + } + } + }, + "newDeviceVerificationNoticePageOneEmailAccessNo": { + "message": "No, I do not" + }, + "newDeviceVerificationNoticePageOneEmailAccessYes": { + "message": "Yes, I can reliably access my email" + }, + "turnOnTwoStepLogin": { + "message": "Turn on two-step login" + }, + "changeAcctEmail": { + "message": "Change account email" + }, "extensionWidth": { "message": "Extension width" }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 3ec2667cd8c..ad839bbd7ce 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -19,6 +19,7 @@ import { import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; +import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -43,6 +44,11 @@ import { TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { + NewDeviceVerificationNoticePageOneComponent, + NewDeviceVerificationNoticePageTwoComponent, + VaultIcons, +} from "@bitwarden/vault"; import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; @@ -715,6 +721,33 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, + { + path: "new-device-notice", + component: ExtensionAnonLayoutWrapperComponent, + canActivate: [], + children: [ + { + path: "", + component: NewDeviceVerificationNoticePageOneComponent, + data: { + pageIcon: VaultIcons.ExclamationTriangle, + pageTitle: { + key: "importantNotice", + }, + }, + }, + { + path: "setup", + component: NewDeviceVerificationNoticePageTwoComponent, + data: { + pageIcon: VaultIcons.UserLock, + pageTitle: { + key: "setupTwoStepLogin", + }, + }, + }, + ], + }, ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", data: { elevation: 0 } satisfies RouteDataProperties, @@ -734,7 +767,7 @@ const routes: Routes = [ }, ...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, { path: "vault", - canActivate: [authGuard], + canActivate: [authGuard, NewDeviceVerificationNoticeGuard], canDeactivate: [clearVaultStateGuard], data: { elevation: 0 } satisfies RouteDataProperties, }), diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 21dced5c2aa..c7642638dc3 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -16,6 +16,7 @@ import { } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; +import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -40,6 +41,11 @@ import { TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { + NewDeviceVerificationNoticePageOneComponent, + NewDeviceVerificationNoticePageTwoComponent, + VaultIcons, +} from "@bitwarden/vault"; import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; @@ -116,10 +122,37 @@ const routes: Routes = [ } satisfies RouteDataProperties & AnonLayoutWrapperData, }, { path: "register", component: RegisterComponent }, + { + path: "new-device-notice", + component: AnonLayoutWrapperComponent, + canActivate: [], + children: [ + { + path: "", + component: NewDeviceVerificationNoticePageOneComponent, + data: { + pageIcon: VaultIcons.ExclamationTriangle, + pageTitle: { + key: "importantNotice", + }, + }, + }, + { + path: "setup", + component: NewDeviceVerificationNoticePageTwoComponent, + data: { + pageIcon: VaultIcons.UserLock, + pageTitle: { + key: "setupTwoStepLogin", + }, + }, + }, + ], + }, { path: "vault", component: VaultComponent, - canActivate: [authGuard], + canActivate: [authGuard, NewDeviceVerificationNoticeGuard], }, { path: "accessibility-cookie", component: AccessibilityCookieComponent }, { path: "set-password", component: SetPasswordComponent }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index f8f81a5ac2c..323d0cd3f7b 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3394,5 +3394,41 @@ }, "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." + }, + "importantNotice": { + "message": "Important notice" + }, + "setupTwoStepLogin": { + "message": "Set up two-step login" + }, + "newDeviceVerificationNoticeContentPage1": { + "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + }, + "newDeviceVerificationNoticeContentPage2": { + "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + }, + "remindMeLater": { + "message": "Remind me later" + }, + "newDeviceVerificationNoticePageOneFormContent": { + "message": "Do you have reliable access to your email, $EMAIL$?", + "placeholders": { + "email": { + "content": "$1", + "example": "your_name@email.com" + } + } + }, + "newDeviceVerificationNoticePageOneEmailAccessNo": { + "message": "No, I do not" + }, + "newDeviceVerificationNoticePageOneEmailAccessYes": { + "message": "Yes, I can reliably access my email" + }, + "turnOnTwoStepLogin": { + "message": "Turn on two-step login" + }, + "changeAcctEmail": { + "message": "Change account email" } } diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js index db1dd55694e..a561b93b21a 100644 --- a/apps/desktop/tailwind.config.js +++ b/apps/desktop/tailwind.config.js @@ -6,6 +6,7 @@ config.content = [ "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", + "../../libs/vault/src/**/*.{html,ts,mdx}", ]; module.exports = config; diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 649f1aba534..9f2a86c1c06 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -13,6 +13,7 @@ import { import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; +import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -40,6 +41,11 @@ import { LoginDecryptionOptionsComponent, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { + NewDeviceVerificationNoticePageOneComponent, + NewDeviceVerificationNoticePageTwoComponent, + VaultIcons, +} from "@bitwarden/vault"; import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { flagEnabled, Flags } from "../utils/flags"; @@ -695,10 +701,37 @@ const routes: Routes = [ }, ], }, + { + path: "new-device-notice", + component: AnonLayoutWrapperComponent, + canActivate: [], + children: [ + { + path: "", + component: NewDeviceVerificationNoticePageOneComponent, + data: { + pageIcon: VaultIcons.ExclamationTriangle, + pageTitle: { + key: "importantNotice", + }, + }, + }, + { + path: "setup", + component: NewDeviceVerificationNoticePageTwoComponent, + data: { + pageIcon: VaultIcons.UserLock, + pageTitle: { + key: "setupTwoStepLogin", + }, + }, + }, + ], + }, { path: "", component: UserLayoutComponent, - canActivate: [deepLinkGuard(), authGuard], + canActivate: [deepLinkGuard(), authGuard, NewDeviceVerificationNoticeGuard], children: [ { path: "vault", diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index abd5779339f..acbb348048c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9888,6 +9888,42 @@ "descriptorCode": { "message": "Descriptor code" }, + "importantNotice": { + "message": "Important notice" + }, + "setupTwoStepLogin": { + "message": "Set up two-step login" + }, + "newDeviceVerificationNoticeContentPage1": { + "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + }, + "newDeviceVerificationNoticeContentPage2": { + "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + }, + "remindMeLater": { + "message": "Remind me later" + }, + "newDeviceVerificationNoticePageOneFormContent": { + "message": "Do you have reliable access to your email, $EMAIL$?", + "placeholders": { + "email": { + "content": "$1", + "example": "your_name@email.com" + } + } + }, + "newDeviceVerificationNoticePageOneEmailAccessNo": { + "message": "No, I do not" + }, + "newDeviceVerificationNoticePageOneEmailAccessYes": { + "message": "Yes, I can reliably access my email" + }, + "turnOnTwoStepLogin": { + "message": "Turn on two-step login" + }, + "changeAcctEmail": { + "message": "Change account email" + }, "removeMembers": { "message": "Remove members" }, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 0e50cec1b64..0765fd8e4c6 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -298,6 +298,7 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; @@ -1401,6 +1402,7 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultLoginDecryptionOptionsService, deps: [MessagingServiceAbstraction], }), + safeProvider(NewDeviceVerificationNoticeService), safeProvider({ provide: UserAsymmetricKeysRegenerationApiService, useClass: DefaultUserAsymmetricKeysRegenerationApiService, diff --git a/libs/angular/src/vault/guards/index.ts b/libs/angular/src/vault/guards/index.ts new file mode 100644 index 00000000000..001a4832372 --- /dev/null +++ b/libs/angular/src/vault/guards/index.ts @@ -0,0 +1 @@ +export * from "./new-device-verification-notice.guard"; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts new file mode 100644 index 00000000000..a37097e3583 --- /dev/null +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts @@ -0,0 +1,51 @@ +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router"; +import { Observable, firstValueFrom, map } from "rxjs"; + +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; + +export const NewDeviceVerificationNoticeGuard: CanActivateFn = async ( + route: ActivatedRouteSnapshot, +) => { + const router = inject(Router); + const configService = inject(ConfigService); + const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService); + const accountService = inject(AccountService); + + if (route.queryParams["fromNewDeviceVerification"]) { + return true; + } + + const tempNoticeFlag = await configService.getFeatureFlag( + FeatureFlag.NewDeviceVerificationTemporaryDismiss, + ); + const permNoticeFlag = await configService.getFeatureFlag( + FeatureFlag.NewDeviceVerificationPermanentDismiss, + ); + + const currentAcct$: Observable = accountService.activeAccount$.pipe( + map((acct) => acct), + ); + const currentAcct = await firstValueFrom(currentAcct$); + + if (!currentAcct) { + return router.createUrlTree(["/login"]); + } + + const userItems$ = newDeviceVerificationNoticeService.noticeState$(currentAcct.id); + const userItems = await firstValueFrom(userItems$); + + if ( + userItems?.last_dismissal == null && + (userItems?.permanent_dismissal == null || !userItems?.permanent_dismissal) && + (tempNoticeFlag || permNoticeFlag) + ) { + return router.createUrlTree(["/new-device-notice"]); + } + + return true; +}; diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html new file mode 100644 index 00000000000..316df3aed17 --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html @@ -0,0 +1,30 @@ +
+

+ {{ "newDeviceVerificationNoticeContentPage1" | i18n }} +

+ + +

+ {{ "newDeviceVerificationNoticePageOneFormContent" | i18n: this.currentEmail }} +

+ + + + {{ "newDeviceVerificationNoticePageOneEmailAccessNo" | i18n }} + + + {{ "newDeviceVerificationNoticePageOneEmailAccessYes" | i18n }} + + +
+ + +
diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts new file mode 100644 index 00000000000..62ae22f5b22 --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts @@ -0,0 +1,82 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms"; +import { Router } from "@angular/router"; +import { firstValueFrom, Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClientType } from "@bitwarden/common/enums"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + AsyncActionsModule, + ButtonModule, + CardComponent, + FormFieldModule, + RadioButtonModule, + TypographyModule, +} from "@bitwarden/components"; + +import { NewDeviceVerificationNoticeService } from "./../../services/new-device-verification-notice.service"; + +@Component({ + standalone: true, + selector: "app-new-device-verification-notice-page-one", + templateUrl: "./new-device-verification-notice-page-one.component.html", + imports: [ + CardComponent, + CommonModule, + JslibModule, + TypographyModule, + ButtonModule, + RadioButtonModule, + FormFieldModule, + AsyncActionsModule, + ReactiveFormsModule, + ], +}) +export class NewDeviceVerificationNoticePageOneComponent implements OnInit { + protected formGroup = this.formBuilder.group({ + hasEmailAccess: new FormControl(0), + }); + protected isDesktop: boolean; + readonly currentAcct$: Observable = this.accountService.activeAccount$; + protected currentEmail: string = ""; + private currentUserId: UserId | null = null; + + constructor( + private formBuilder: FormBuilder, + private router: Router, + private accountService: AccountService, + private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService, + private platformUtilsService: PlatformUtilsService, + ) { + this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop; + } + + async ngOnInit() { + const currentAcct = await firstValueFrom(this.currentAcct$); + if (!currentAcct) { + return; + } + this.currentEmail = currentAcct.email; + this.currentUserId = currentAcct.id; + } + + submit = async () => { + if (this.formGroup.controls.hasEmailAccess.value === 0) { + await this.router.navigate(["new-device-notice/setup"]); + } else if (this.formGroup.controls.hasEmailAccess.value === 1) { + await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState( + this.currentUserId, + { + last_dismissal: new Date(), + permanent_dismissal: false, + }, + ); + + await this.router.navigate(["/vault"]); + } + }; +} diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html new file mode 100644 index 00000000000..270b4126252 --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html @@ -0,0 +1,39 @@ +

+ {{ "newDeviceVerificationNoticeContentPage2" | i18n }} +

+ + + {{ "turnOnTwoStepLogin" | i18n }} + + + + {{ "changeAcctEmail" | i18n }} + + + + diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts new file mode 100644 index 00000000000..630a2fd516c --- /dev/null +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts @@ -0,0 +1,95 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom, Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClientType } from "@bitwarden/common/enums"; +import { + Environment, + EnvironmentService, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components"; + +import { NewDeviceVerificationNoticeService } from "../../services/new-device-verification-notice.service"; + +@Component({ + standalone: true, + selector: "app-new-device-verification-notice-page-two", + templateUrl: "./new-device-verification-notice-page-two.component.html", + imports: [CommonModule, JslibModule, TypographyModule, ButtonModule, LinkModule], +}) +export class NewDeviceVerificationNoticePageTwoComponent implements OnInit { + protected isWeb: boolean; + protected isDesktop: boolean; + readonly currentAcct$: Observable = this.accountService.activeAccount$; + private currentUserId: UserId | null = null; + private env$: Observable = this.environmentService.environment$; + + constructor( + private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService, + private router: Router, + private accountService: AccountService, + private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService, + ) { + this.isWeb = this.platformUtilsService.getClientType() === ClientType.Web; + this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop; + } + + async ngOnInit() { + const currentAcct = await firstValueFrom(this.currentAcct$); + if (!currentAcct) { + return; + } + this.currentUserId = currentAcct.id; + } + + async navigateToTwoStepLogin(event: Event) { + event.preventDefault(); + + const env = await firstValueFrom(this.env$); + const url = env.getWebVaultUrl(); + + if (this.isWeb) { + await this.router.navigate(["/settings/security/two-factor"], { + queryParams: { fromNewDeviceVerification: true }, + }); + } else { + this.platformUtilsService.launchUri( + url + "/#/settings/security/two-factor/?fromNewDeviceVerification=true", + ); + } + } + + async navigateToChangeAcctEmail(event: Event) { + event.preventDefault(); + + const env = await firstValueFrom(this.env$); + const url = env.getWebVaultUrl(); + if (this.isWeb) { + await this.router.navigate(["/settings/account"], { + queryParams: { fromNewDeviceVerification: true }, + }); + } else { + this.platformUtilsService.launchUri( + url + "/#/settings/account/?fromNewDeviceVerification=true", + ); + } + } + + async remindMeLaterSelect() { + await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationNoticeState( + this.currentUserId, + { + last_dismissal: new Date(), + permanent_dismissal: false, + }, + ); + + await this.router.navigate(["/vault"]); + } +} diff --git a/libs/vault/src/icons/exclamation-triangle.ts b/libs/vault/src/icons/exclamation-triangle.ts new file mode 100644 index 00000000000..6340546d1e1 --- /dev/null +++ b/libs/vault/src/icons/exclamation-triangle.ts @@ -0,0 +1,7 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ExclamationTriangle = svgIcon` + + + +`; diff --git a/libs/vault/src/icons/index.ts b/libs/vault/src/icons/index.ts index c1b69a31ef5..2e106782f53 100644 --- a/libs/vault/src/icons/index.ts +++ b/libs/vault/src/icons/index.ts @@ -2,3 +2,5 @@ export * from "./deactivated-org"; export * from "./no-folders"; export * from "./vault"; export * from "./empty-trash"; +export * from "./exclamation-triangle"; +export * from "./user-lock"; diff --git a/libs/vault/src/icons/user-lock.ts b/libs/vault/src/icons/user-lock.ts new file mode 100644 index 00000000000..c1dc3efde39 --- /dev/null +++ b/libs/vault/src/icons/user-lock.ts @@ -0,0 +1,17 @@ +import { svgIcon } from "@bitwarden/components"; + +export const UserLock = svgIcon` + + + + + + + + + + + + + +`; diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index dca9b2dee79..0112de44241 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -14,5 +14,7 @@ export { export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component"; export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component"; +export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component"; +export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; export * as VaultIcons from "./icons"; diff --git a/libs/vault/src/services/new-device-verification-notice.service.ts b/libs/vault/src/services/new-device-verification-notice.service.ts index 6c7df590b50..bb096ff0c2c 100644 --- a/libs/vault/src/services/new-device-verification-notice.service.ts +++ b/libs/vault/src/services/new-device-verification-notice.service.ts @@ -57,7 +57,7 @@ export class NewDeviceVerificationNoticeService { } async updateNewDeviceVerificationNoticeState( - userId: UserId, + userId: UserId | null, newState: NewDeviceVerificationNotice, ): Promise { await this.noticeState(userId).update(() => { From 0f3803ac910b2df1e4b0a903b4838f7a5ba1f540 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:42:37 -0600 Subject: [PATCH 41/57] [PM-11442] Emergency Cipher Viewing (#12054) * force viewOnly to be true for emergency access * add input to hide password history, applicable when the view is used from emergency view * add extension refresh version of the emergency view dialog * allow emergency access to view password history - `ViewPasswordHistoryService` accepts a cipher id or CipherView. When a CipherView is included, the history component no longer has to fetch the cipher. * remove unused comments * Add fixme comment for removing non-extension refresh code * refactor password history component to accept a full cipher view - Remove the option to pass in only an id --- .../vault-password-history-v2.component.html | 4 +- ...ault-password-history-v2.component.spec.ts | 42 +++++-- .../vault-password-history-v2.component.ts | 32 +++++- ...wser-view-password-history.service.spec.ts | 6 +- .../browser-view-password-history.service.ts | 7 +- .../view/emergency-access-view.component.ts | 21 ++++ .../emergency-add-edit-cipher.component.ts | 12 +- .../view/emergency-view-dialog.component.html | 13 +++ .../emergency-view-dialog.component.spec.ts | 108 ++++++++++++++++++ .../view/emergency-view-dialog.component.ts | 90 +++++++++++++++ .../password-history.component.html | 2 +- .../password-history.component.ts | 18 +-- .../web-view-password-history.service.spec.ts | 8 +- .../web-view-password-history.service.ts | 6 +- .../view-password-history.service.ts | 4 +- .../item-history/item-history-v2.component.ts | 3 +- .../password-history-view.component.spec.ts | 5 +- .../password-history-view.component.ts | 39 +------ 18 files changed, 337 insertions(+), 83 deletions(-) create mode 100644 apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html create mode 100644 apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts create mode 100644 apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html index d4ff0662fe0..b1f01bb9cb0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.html @@ -1,8 +1,8 @@ - + - + diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts index a375aba302e..9ac17b49386 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts @@ -1,27 +1,40 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { Subject } from "rxjs"; +import { BehaviorSubject, Subject } from "rxjs"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { PasswordHistoryV2Component } from "./vault-password-history-v2.component"; describe("PasswordHistoryV2Component", () => { - let component: PasswordHistoryV2Component; let fixture: ComponentFixture; const params$ = new Subject(); + + const mockCipherView = { + id: "111-222-333", + name: "cipher one", + } as CipherView; + + const mockCipher = { + decrypt: jest.fn().mockResolvedValue(mockCipherView), + } as unknown as Cipher; + const back = jest.fn().mockResolvedValue(undefined); + const getCipher = jest.fn().mockResolvedValue(mockCipher); beforeEach(async () => { back.mockClear(); + getCipher.mockClear(); await TestBed.configureTestingModule({ imports: [PasswordHistoryV2Component], @@ -29,8 +42,13 @@ describe("PasswordHistoryV2Component", () => { { provide: WINDOW, useValue: window }, { provide: PlatformUtilsService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, - { provide: CipherService, useValue: mock() }, - { provide: AccountService, useValue: mock() }, + { provide: CipherService, useValue: mock({ get: getCipher }) }, + { + provide: AccountService, + useValue: mock({ + activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account), + }), + }, { provide: PopupRouterCacheService, useValue: { back } }, { provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: I18nService, useValue: { t: (key: string) => key } }, @@ -38,19 +56,21 @@ describe("PasswordHistoryV2Component", () => { }).compileComponents(); fixture = TestBed.createComponent(PasswordHistoryV2Component); - component = fixture.componentInstance; fixture.detectChanges(); }); - it("sets the cipherId from the params", () => { - params$.next({ cipherId: "444-33-33-1111" }); + it("loads the cipher from params the cipherId from the params", fakeAsync(() => { + params$.next({ cipherId: mockCipherView.id }); - expect(component["cipherId"]).toBe("444-33-33-1111"); - }); + tick(100); + + expect(getCipher).toHaveBeenCalledWith(mockCipherView.id); + })); it("navigates back when a cipherId is not in the params", () => { params$.next({}); expect(back).toHaveBeenCalledTimes(1); + expect(getCipher).not.toHaveBeenCalled(); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index c70c83f40fc..c8f590ced57 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -3,10 +3,14 @@ import { NgIf } from "@angular/common"; import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { first } from "rxjs/operators"; +import { firstValueFrom } from "rxjs"; +import { first, map } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component"; import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component"; @@ -28,18 +32,20 @@ import { PopupRouterCacheService } from "../../../../../platform/popup/view-cach ], }) export class PasswordHistoryV2Component implements OnInit { - protected cipherId: CipherId; + protected cipher: CipherView; constructor( private browserRouterHistory: PopupRouterCacheService, private route: ActivatedRoute, + private cipherService: CipherService, + private accountService: AccountService, ) {} ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil this.route.queryParams.pipe(first()).subscribe((params) => { if (params.cipherId) { - this.cipherId = params.cipherId; + void this.loadCipher(params.cipherId); } else { this.close(); } @@ -49,4 +55,22 @@ export class PasswordHistoryV2Component implements OnInit { close() { void this.browserRouterHistory.back(); } + + /** Load the cipher based on the given Id */ + private async loadCipher(cipherId: string) { + const cipher = await this.cipherService.get(cipherId); + + const activeAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)), + ); + + if (!activeAccount?.id) { + throw new Error("Active account is not available."); + } + + const activeUserId = activeAccount.id as UserId; + this.cipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + } } diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts index ded4686477e..5024b960d9c 100644 --- a/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.spec.ts @@ -2,6 +2,8 @@ import { TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + import { BrowserViewPasswordHistoryService } from "./browser-view-password-history.service"; describe("BrowserViewPasswordHistoryService", () => { @@ -19,9 +21,9 @@ describe("BrowserViewPasswordHistoryService", () => { describe("viewPasswordHistory", () => { it("navigates to the password history screen", async () => { - await service.viewPasswordHistory("test"); + await service.viewPasswordHistory({ id: "cipher-id" } as CipherView); expect(router.navigate).toHaveBeenCalledWith(["/cipher-password-history"], { - queryParams: { cipherId: "test" }, + queryParams: { cipherId: "cipher-id" }, }); }); }); diff --git a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts index 453fe113ebf..5e400da9de5 100644 --- a/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts +++ b/apps/browser/src/vault/popup/services/browser-view-password-history.service.ts @@ -4,6 +4,7 @@ import { inject } from "@angular/core"; import { Router } from "@angular/router"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; /** * This class handles the premium upgrade process for the browser extension. @@ -14,7 +15,9 @@ export class BrowserViewPasswordHistoryService implements ViewPasswordHistorySer /** * Navigates to the password history screen. */ - async viewPasswordHistory(cipherId: string) { - await this.router.navigate(["/cipher-password-history"], { queryParams: { cipherId } }); + async viewPasswordHistory(cipher: CipherView) { + await this.router.navigate(["/cipher-password-history"], { + queryParams: { cipherId: cipher.id }, + }); } } diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts index 6a72360cfad..7506f6c5d0b 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-access-view.component.ts @@ -4,16 +4,22 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; +import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault"; import { EmergencyAccessService } from "../../../emergency-access"; import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component"; import { EmergencyAddEditCipherComponent } from "./emergency-add-edit-cipher.component"; +import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"; @Component({ selector: "emergency-access-view", templateUrl: "emergency-access-view.component.html", + providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class EmergencyAccessViewComponent implements OnInit { @@ -31,6 +37,8 @@ export class EmergencyAccessViewComponent implements OnInit { private router: Router, private route: ActivatedRoute, private emergencyAccessService: EmergencyAccessService, + private configService: ConfigService, + private dialogService: DialogService, ) {} ngOnInit() { @@ -49,6 +57,19 @@ export class EmergencyAccessViewComponent implements OnInit { } async selectCipher(cipher: CipherView) { + const browserRefreshEnabled = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); + + if (browserRefreshEnabled) { + EmergencyViewDialogComponent.open(this.dialogService, { + cipher, + }); + return; + } + + // FIXME PM-15385: Remove below dialog service logic once extension refresh is live. + // eslint-disable-next-line const [_, childComponent] = await this.modalService.openViewRef( EmergencyAddEditCipherComponent, diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts index 2da8e06449a..59228431e65 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DatePipe } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { CollectionService } from "@bitwarden/admin-console/common"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -30,7 +30,7 @@ import { AddEditComponent as BaseAddEditComponent } from "../../../../vault/indi selector: "app-org-vault-add-edit", templateUrl: "../../../../vault/individual-vault/add-edit.component.html", }) -export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { +export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implements OnInit { originalCipher: Cipher = null; viewOnly = true; protected override componentName = "app-org-vault-add-edit"; @@ -85,6 +85,14 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { this.title = this.i18nService.t("viewItem"); } + async ngOnInit(): Promise { + await super.ngOnInit(); + // The base component `ngOnInit` calculates the `viewOnly` property based on cipher properties + // In the case of emergency access, `viewOnly` should always be true, set it manually here after + // the base `ngOnInit` is complete. + this.viewOnly = true; + } + protected async loadCipher() { return Promise.resolve(this.originalCipher); } diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html new file mode 100644 index 00000000000..be38e1d9505 --- /dev/null +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.html @@ -0,0 +1,13 @@ + + + {{ title }} + +
+ +
+ + + +
diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts new file mode 100644 index 00000000000..341e44f643b --- /dev/null +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -0,0 +1,108 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { mock } from "jest-mock-extended"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { DialogService } from "@bitwarden/components"; + +import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"; + +describe("EmergencyViewDialogComponent", () => { + let component: EmergencyViewDialogComponent; + let fixture: ComponentFixture; + + const open = jest.fn(); + const close = jest.fn(); + + const mockCipher = { + id: "cipher1", + name: "Cipher", + type: CipherType.Login, + login: { uris: [] }, + card: {}, + } as CipherView; + + beforeEach(async () => { + open.mockClear(); + close.mockClear(); + + await TestBed.configureTestingModule({ + imports: [EmergencyViewDialogComponent, NoopAnimationsModule], + providers: [ + { provide: OrganizationService, useValue: mock() }, + { provide: CollectionService, useValue: mock() }, + { provide: FolderService, useValue: mock() }, + { provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } }, + { provide: DialogService, useValue: { open } }, + { provide: DialogRef, useValue: { close } }, + { provide: DIALOG_DATA, useValue: { cipher: mockCipher } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(EmergencyViewDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("creates", () => { + expect(component).toBeTruthy(); + }); + + it("opens dialog", () => { + EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher }); + + expect(open).toHaveBeenCalled(); + }); + + it("closes the dialog", () => { + EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher }); + fixture.detectChanges(); + + const cancelButton = fixture.debugElement.queryAll(By.css("button")).pop(); + + cancelButton.nativeElement.click(); + + expect(close).toHaveBeenCalled(); + }); + + describe("updateTitle", () => { + it("sets login title", () => { + mockCipher.type = CipherType.Login; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType typelogin"); + }); + + it("sets card title", () => { + mockCipher.type = CipherType.Card; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType typecard"); + }); + + it("sets identity title", () => { + mockCipher.type = CipherType.Identity; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType typeidentity"); + }); + + it("sets note title", () => { + mockCipher.type = CipherType.SecureNote; + + component["updateTitle"](); + + expect(component["title"]).toBe("viewItemType note"); + }); + }); +}); diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts new file mode 100644 index 00000000000..7da4ce3165b --- /dev/null +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -0,0 +1,90 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; +import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { CipherViewComponent } from "@bitwarden/vault"; + +import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service"; + +export interface EmergencyViewDialogParams { + /** The cipher being viewed. */ + cipher: CipherView; +} + +/** Stubbed class, premium upgrade is not applicable for emergency viewing */ +class PremiumUpgradePromptNoop implements PremiumUpgradePromptService { + async promptForPremium() { + return Promise.resolve(); + } +} + +@Component({ + selector: "app-emergency-view-dialog", + templateUrl: "emergency-view-dialog.component.html", + standalone: true, + imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule], + providers: [ + { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, + { provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop }, + ], +}) +export class EmergencyViewDialogComponent { + /** + * The title of the dialog. Updates based on the cipher type. + * @protected + */ + protected title: string; + + constructor( + @Inject(DIALOG_DATA) protected params: EmergencyViewDialogParams, + private dialogRef: DialogRef, + private i18nService: I18nService, + ) { + this.updateTitle(); + } + + get cipher(): CipherView { + return this.params.cipher; + } + + cancel = () => { + this.dialogRef.close(); + }; + + private updateTitle() { + const partOne = "viewItemType"; + + const type = this.cipher.type; + + switch (type) { + case CipherType.Login: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase()); + break; + case CipherType.Card: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase()); + break; + case CipherType.Identity: + this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase()); + break; + case CipherType.SecureNote: + this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase()); + break; + } + } + + /** + * Opens the EmergencyViewDialog. + */ + static open(dialogService: DialogService, params: EmergencyViewDialogParams) { + return dialogService.open(EmergencyViewDialogComponent, { + data: params, + }); + } +} diff --git a/apps/web/src/app/vault/individual-vault/password-history.component.html b/apps/web/src/app/vault/individual-vault/password-history.component.html index 7127e7ca649..4eaca8f736e 100644 --- a/apps/web/src/app/vault/individual-vault/password-history.component.html +++ b/apps/web/src/app/vault/individual-vault/password-history.component.html @@ -3,7 +3,7 @@ {{ "passwordHistory" | i18n }} - + - -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • - - -
    diff --git a/apps/browser/src/popup/tabs.component.ts b/apps/browser/src/popup/tabs.component.ts deleted file mode 100644 index 7546c9ca13b..00000000000 --- a/apps/browser/src/popup/tabs.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from "@angular/core"; - -import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; - -@Component({ - selector: "app-tabs", - templateUrl: "tabs.component.html", -}) -export class TabsComponent implements OnInit { - showCurrentTab = true; - - ngOnInit() { - this.showCurrentTab = !BrowserPopupUtils.inPopout(window); - } -} From b27a1a5337f765ee3fa9c07f51083c29c69a68da Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:44:36 -0600 Subject: [PATCH 57/57] [PM-12998] View Cipher: Color Password (#12354) * show color password for visible passwords in vault view - The password input will be visually hidden - Adds tests for the login credentials component * formatting --- .../login-credentials-view.component.html | 23 +- .../login-credentials-view.component.spec.ts | 198 ++++++++++++++++++ 2 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 5b6b995d095..8503604bf7c 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -28,17 +28,34 @@

    {{ "loginCredentials" | i18n }}

    > - {{ "password" | i18n }} + + {{ "password" | i18n }} + + + + +