diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d23cfa58283..5ba84c1f195 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -119,6 +119,7 @@ apps/browser/src/autofill @bitwarden/team-autofill-dev apps/desktop/src/autofill @bitwarden/team-autofill-dev libs/common/src/autofill @bitwarden/team-autofill-dev apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev +apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-dev apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev diff --git a/.github/ISSUE_TEMPLATE/browser.yml b/.github/ISSUE_TEMPLATE/browser.yml index ec78f3ee555..23a0e4276bf 100644 --- a/.github/ISSUE_TEMPLATE/browser.yml +++ b/.github/ISSUE_TEMPLATE/browser.yml @@ -84,11 +84,11 @@ body: attributes: label: Browser Version description: What version of the browser(s) are you seeing the problem on? - - type: input + - type: textarea id: version attributes: - label: Build Version - description: What version of our software are you running? (go to "Settings" → "About" in the extension) + label: Environment Versions + description: Copy from "Settings" → "About" → "About Bitwarden" in the extension. Should include the extension version and server environment. validations: required: true - type: checkboxes diff --git a/.github/renovate.json5 b/.github/renovate.json5 index b898ffc8629..bde87563dd1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -173,7 +173,6 @@ "cross-env", "del", "lit", - "nord", "patch-package", "prettier", "prettier-plugin-tailwindcss", diff --git a/apps/browser/package.json b/apps/browser/package.json index 21784167b9e..e3bccf3f0df 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2025.2.2", + "version": "2025.3.0", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack", diff --git a/apps/browser/postcss.config.js b/apps/browser/postcss.config.js index a4dee9d4362..5657df3afcf 100644 --- a/apps/browser/postcss.config.js +++ b/apps/browser/postcss.config.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef, @typescript-eslint/no-require-imports */ +/* eslint-disable @typescript-eslint/no-require-imports */ module.exports = { plugins: [ require("postcss-import"), diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 4ab45ed9003..bd05d6eb3cd 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -901,6 +901,9 @@ "no": { "message": "لا" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "حدث خطأ غير متوقع." }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 4aa46d861d4..1036fe27422 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -650,7 +650,7 @@ "message": "Veb brauzeriniz lövhəyə kopyalamağı dəstəkləmir. Əvəzində əllə kopyalayın." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Kimliyinizi doğrulayın" }, "weDontRecognizeThisDevice": { "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." @@ -901,6 +901,9 @@ "no": { "message": "Xeyr" }, + "location": { + "message": "Yerləşmə" + }, "unexpectedError": { "message": "Gözlənilməz bir xəta baş verdi." }, @@ -1069,35 +1072,35 @@ "description": "Shown to user after login is updated." }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "Yeni giriş kimi saxla", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "Giriş məlumatlarını güncəllə", "description": "Button text for updating an existing login entry." }, "saveLoginPrompt": { - "message": "Save login?", + "message": "Giriş məlumatları saxlanılsın?", "description": "Prompt asking the user if they want to save their login details." }, "updateLoginPrompt": { - "message": "Update existing login?", + "message": "Mövcud giriş məlumatları güncəllənsin?", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "Giriş məlumatları saxlanıldı", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { - "message": "Login updated", + "message": "Giriş məlumatları güncəlləndi", "description": "Message displayed when login details are successfully updated." }, "saveFailure": { - "message": "Error saving", + "message": "Saxlama xətası", "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "Bunu saxlaya bilmədik. Məlumatları manual daxil etməyə çalışın.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -5145,6 +5148,6 @@ "message": "Biometrik kilid açmanı istifadə etmək üçün lütfən masaüstü tətbiqinizi güncəlləyin, ya da masaüstü ayarlarında barmaq izi ilə kilid açmanı sıradan çıxardın." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Riskli parolları dəyişdir" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 84e438c4feb..224e6ed9cc2 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Не" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Адбылася нечаканая памылка." }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 8d670e8666b..59da9af636c 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Не" }, + "location": { + "message": "Местоположение" + }, "unexpectedError": { "message": "Възникна неочаквана грешка." }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 1e0335b8a3a..ef841249cd0 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -901,6 +901,9 @@ "no": { "message": "না" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "একটি অপ্রত্যাশিত ত্রুটি ঘটেছে।" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 0118e5854af..e0a3d3f8458 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 09da7b34563..5a424625afe 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "S'ha produït un error inesperat." }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 42f753438dd..23030a36f8d 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Umístění" + }, "unexpectedError": { "message": "Vyskytla se neočekávaná chyba." }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 80ec208458e..24bc03729d9 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Na" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 5c0b09b80d5..e7bebee94f9 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nej" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Der opstod en uventet fejl." }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index a0dacde98b6..d9095bb616c 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -650,7 +650,7 @@ "message": "Den Browser unterstützt das einfache Kopieren nicht. Bitte kopiere es manuell." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Verifiziere deine Identität" }, "weDontRecognizeThisDevice": { "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." @@ -901,6 +901,9 @@ "no": { "message": "Nein" }, + "location": { + "message": "Standort" + }, "unexpectedError": { "message": "Ein unerwarteter Fehler ist aufgetreten." }, @@ -1069,35 +1072,35 @@ "description": "Shown to user after login is updated." }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "Als neue Zugangsdaten speichern", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "Zugangsdaten aktualisieren", "description": "Button text for updating an existing login entry." }, "saveLoginPrompt": { - "message": "Save login?", + "message": "Zugangsdaten speichern?", "description": "Prompt asking the user if they want to save their login details." }, "updateLoginPrompt": { - "message": "Update existing login?", + "message": "Bestehende Zugangsdaten aktualisieren?", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "Zugangsdaten gespeichert", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { - "message": "Login updated", + "message": "Zugangsdaten aktualisiert", "description": "Message displayed when login details are successfully updated." }, "saveFailure": { - "message": "Error saving", + "message": "Fehler beim Speichern", "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "Oh nein! Das konnten wir nicht speichern. Versuch, die Details manuell einzugeben.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -2532,18 +2535,18 @@ "message": "Überprüfung gefährdeter Passwörter" }, "reviewAtRiskLoginsSlideDesc": { - "message": "Die Passwörter deiner Organisationen sind gefährdet, weil sie schwach, wiederverwendet und/oder ungeschützt sind.", + "message": "Die Passwörter deiner Organisationen sind gefährdet, weil sie schwach, wiederverwendet und/oder kompromittiert sind.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAlt": { "message": "Illustration einer Liste gefährdeter Zugangsdaten" }, "generatePasswordSlideDesc": { - "message": "Generiere schnell ein starkes, einzigartiges Passwort mit dem Bitwarden-Autofill-Menü auf der gefährdeten Website.", + "message": "Generiere schnell ein starkes, einzigartiges Passwort mit dem Bitwarden Auto-Ausfüllen-Menü auf der gefährdeten Website.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAlt": { - "message": "Illustration des Bitwarden Autofill-Menüs, das ein generiertes Passwort anzeigt" + "message": "Illustration des Bitwarden Auto-Ausfüllen-Menüs, das ein generiertes Passwort anzeigt" }, "updateInBitwarden": { "message": "In Bitwarden aktualisieren" @@ -2553,7 +2556,7 @@ "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAlt": { - "message": "Illustration einer Bitwarden-Benachrichtigung, die den Benutzer dazu auffordert, den Login zu aktualisieren" + "message": "Illustration einer Bitwarden-Benachrichtigung, die den Benutzer dazu auffordert, die Zugangsdaten zu aktualisieren" }, "turnOnAutofill": { "message": "Auto-Ausfüllen aktivieren" @@ -4108,7 +4111,7 @@ "message": "Aktives Konto" }, "bitwardenAccount": { - "message": "Bitwarden Account" + "message": "Bitwarden-Konto" }, "availableAccounts": { "message": "Verfügbare Konten" @@ -5145,6 +5148,6 @@ "message": "Um biometrisches Entsperren zu verwenden, aktualisiere bitte deine Desktop-Anwendung oder deaktiviere die Entsperrung per Fingerabdruck in den Desktop-Einstellungen." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Gefährdetes Passwort ändern" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index a630cb5d3c5..c197d5c6528 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Όχι" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Παρουσιάστηκε ένα μη αναμενόμενο σφάλμα." }, diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 1679fcfcf3f..127e07f25e8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -385,6 +385,15 @@ "editFolder": { "message": "Edit folder" }, + "editFolderWithName": { + "message": "Edit folder: $FOLDERNAME$", + "placeholders": { + "foldername": { + "content": "$1", + "example": "Social" + } + } + }, "newFolder": { "message": "New folder" }, @@ -1166,10 +1175,6 @@ "message": "Light", "description": "Light color" }, - "solarizedDark": { - "message": "Solarized dark", - "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." - }, "exportFrom": { "message": "Export from" }, @@ -1674,6 +1679,9 @@ "dragToSort": { "message": "Drag to sort" }, + "dragToReorder": { + "message": "Drag to reorder" + }, "cfTypeText": { "message": "Text" }, @@ -4701,6 +4709,9 @@ } } }, + "reorderWebsiteUriButton": { + "message": "Reorder website URI. Use arrow key to move item up or down." + }, "reorderFieldUp": { "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", "placeholders": { @@ -5132,6 +5143,33 @@ "extraWide": { "message": "Extra wide" }, + "sshKeyWrongPassword": { + "message": "The password you entered is incorrect." + }, + "importSshKey": { + "message": "Import" + }, + "confirmSshKeyPassword": { + "message": "Confirm password" + }, + "enterSshKeyPasswordDesc": { + "message": "Enter the password for the SSH key." + }, + "enterSshKeyPassword": { + "message": "Enter password" + }, + "invalidSshKey": { + "message": "The SSH key is invalid" + }, + "sshKeyTypeUnsupported": { + "message": "The SSH key type is not supported" + }, + "importSshKeyFromClipboard": { + "message": "Import key from clipboard" + }, + "sshKeyImported": { + "message": "SSH key imported successfully" + }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", "placeholders": { diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 1db4b60c106..17b76ceaa0e 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index e47870b7ebe..022d3eb4c22 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 98efcb500ed..587afb99dcb 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Ha ocurrido un error inesperado." }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 00330b50654..d59a702463d 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Ei" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Tekkis ootamatu viga." }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 780d6f255b7..4bd642ffc35 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Ez" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Ustekabeko akatsa gertatu da." }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 9d9d377b87e..f905d649549 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -901,6 +901,9 @@ "no": { "message": "خیر" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "یک خطای غیر منتظره رخ داده است." }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index d38fdf342e2..14910878987 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -901,6 +901,9 @@ "no": { "message": "En" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Tapahtui odottamaton virhe." }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 2a82c96f2d2..c9a170b037c 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Hindi" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Namana ang isang hindi inaasahang error." }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index e49a4a3ca57..cc65400fecb 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Non" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Une erreur inattendue est survenue." }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index ebfeb890b3e..ce117de8e97 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Non" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Produciuse un erro inesperado." }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 6ea7b2f04ce..8b7a178f736 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -901,6 +901,9 @@ "no": { "message": "לא" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "אירעה שגיאה לא צפויה." }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 4e7ba5fc6ad..663b47bea43 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -901,6 +901,9 @@ "no": { "message": "नहीं" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occured." }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 0226e941b31..aaf7c5895f0 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Došlo je do neočekivane pogreške." }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 1d6ed06e719..5a4827915a3 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nem" }, + "location": { + "message": "Hely" + }, "unexpectedError": { "message": "Váratlan hiba történt." }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index ad13c5c0a47..9edf3faeaa2 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Tidak" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Terjadi kesalahan yang tak diduga." }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 3dc55022c0c..ecd045d67e2 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Luogo" + }, "unexpectedError": { "message": "Si è verificato un errore imprevisto." }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 38d2dbe00cb..da2ea1a185e 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -901,6 +901,9 @@ "no": { "message": "いいえ" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "予期せぬエラーが発生しました。" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 1b99bb7ab7a..f07609677cd 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -901,6 +901,9 @@ "no": { "message": "არა" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index bfce2ae3757..dd0bf23c799 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index d920916b09c..8ed5d9e2254 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -901,6 +901,9 @@ "no": { "message": "ಇಲ್ಲ" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "ಅನಿರೀಕ್ಷಿತ ದೋಷ ಸಂಭವಿಸಿದೆ." }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index a6c8fd6bdf5..63db503c785 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -901,6 +901,9 @@ "no": { "message": "아니오" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "예기치 못한 오류가 발생했습니다." }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 628db98e8e4..b05269e9b40 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Įvyko netikėta klaida." }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 2519cb4f696..f7fe453d227 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nē" }, + "location": { + "message": "Atrašanās vieta" + }, "unexpectedError": { "message": "Ir radusies neparedzēta kļūda." }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 47dca36f9cb..f73de774ab2 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -901,6 +901,9 @@ "no": { "message": "തെറ്റ്" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "ഒരു അപ്രതീക്ഷിത പിശക് സംഭവിച്ചു." }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 4289db69f5c..7a67bab80eb 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index bfce2ae3757..dd0bf23c799 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 5075ef83c3b..c8875e0b327 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nei" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "En uventet feil har oppstått." }, diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index bfce2ae3757..dd0bf23c799 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 3c19792e4c6..19e06e63581 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nee" }, + "location": { + "message": "Locatie" + }, "unexpectedError": { "message": "Er is een onverwachte fout opgetreden." }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index bfce2ae3757..dd0bf23c799 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index bfce2ae3757..dd0bf23c799 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 8666af5db9b..754cc510398 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nie" }, + "location": { + "message": "Lokalizacja" + }, "unexpectedError": { "message": "Wystąpił nieoczekiwany błąd." }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 03686fc0c91..17a48bc1e03 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Não" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Ocorreu um erro inesperado." }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index de97d6be63a..b0bfda0066e 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Não" }, + "location": { + "message": "Localização" + }, "unexpectedError": { "message": "Ocorreu um erro inesperado." }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 0e1b918e42a..e25e8dae1b6 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nu" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "A survenit o eroare neașteptată." }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index f6d3f1e6154..591eaabb9aa 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Нет" }, + "location": { + "message": "Местоположение" + }, "unexpectedError": { "message": "Произошла непредвиденная ошибка." }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index fb444577060..99f3747b172 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -901,6 +901,9 @@ "no": { "message": "නැත" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "අනපේක්ෂිත දෝෂයක් සිදුවී ඇත." }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 26f245df214..d8fc1d8049b 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nie" }, + "location": { + "message": "Poloha" + }, "unexpectedError": { "message": "Vyskytla sa neočakávaná chyba." }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 20982a34557..531704980e5 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Prišlo je do nepričakovane napake." }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 18147852ef1..19f841ef0e6 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -650,7 +650,7 @@ "message": "Ваш прегледач не подржава једноставно копирање у клипборду. Уместо тога копирајте га ручно." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Потврдите свој идентитет" }, "weDontRecognizeThisDevice": { "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." @@ -901,6 +901,9 @@ "no": { "message": "Не" }, + "location": { + "message": "Локација" + }, "unexpectedError": { "message": "Дошло је до неочекиване грешке." }, @@ -1051,7 +1054,7 @@ "message": "Сачувај" }, "loginSaveSuccessDetails": { - "message": "$USERNAME$ saved to Bitwarden.", + "message": "$USERNAME$ сачуван и Bitwarden.", "placeholders": { "username": { "content": "$1" @@ -1060,7 +1063,7 @@ "description": "Shown to user after login is saved." }, "loginUpdatedSuccessDetails": { - "message": "$USERNAME$ updated in Bitwarden.", + "message": "$USERNAME$ ажурирано у Bitwarden.", "placeholders": { "username": { "content": "$1" @@ -1069,35 +1072,35 @@ "description": "Shown to user after login is updated." }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "Сачувати као нову пријаву", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "Ажурирати пријаву", "description": "Button text for updating an existing login entry." }, "saveLoginPrompt": { - "message": "Save login?", + "message": "Сачувати пријаву?", "description": "Prompt asking the user if they want to save their login details." }, "updateLoginPrompt": { - "message": "Update existing login?", + "message": "Ажурирајте постојећу пријаву?", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "Пријава сачувана", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { - "message": "Login updated", + "message": "Пријава ажурирана", "description": "Message displayed when login details are successfully updated." }, "saveFailure": { - "message": "Error saving", + "message": "Грешка при снимању", "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "Ох не! Нисмо могли да то сачувамо. Покушајте да ручно унесете детаље.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -2477,7 +2480,7 @@ "message": "Лозинке под ризиком" }, "atRiskPasswordDescSingleOrg": { - "message": "$ORGANIZATION$ is requesting you change one password because it is at-risk.", + "message": "$ORGANIZATION$ тражи да промените једну лозинку јер је ризична.", "placeholders": { "organization": { "content": "$1", @@ -2486,7 +2489,7 @@ } }, "atRiskPasswordsDescSingleOrgPlural": { - "message": "$ORGANIZATION$ is requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "$ORGANIZATION$ тражи да промените $COUNT$ лозинке пошто су под ризиком.", "placeholders": { "organization": { "content": "$1", @@ -2499,7 +2502,7 @@ } }, "atRiskPasswordsDescMultiOrgPlural": { - "message": "Your organizations are requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "Ваша организација тражи да промените $COUNT$ лозинке пошто су под ризиком.", "placeholders": { "count": { "content": "$1", @@ -2526,34 +2529,34 @@ "message": "Ажурирајте поставке да бисте брзо поставили лозинке и генерисати нове" }, "reviewAtRiskLogins": { - "message": "Review at-risk logins" + "message": "Прегледајте ризичне пријаве" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords" + "message": "Прегледати ризичне лозинке" }, "reviewAtRiskLoginsSlideDesc": { - "message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.", + "message": "Ваше организационе лозинке су ризичне јер су слабе, поново употребљене и/или изложене.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAlt": { - "message": "Illustration of a list of logins that are at-risk" + "message": "Илустрација листе пријаве које су ризичне" }, "generatePasswordSlideDesc": { - "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", + "message": "Брзо генеришите снажну, јединствену лозинку са Bitwarden менијем аутопуњења за коришћење на ризичном сајту.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAlt": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password" + "message": "Илустрација Bitwarden-ског менија аутопуњења који приказују генерисану лозинку" }, "updateInBitwarden": { - "message": "Update in Bitwarden" + "message": "Ажурирања у Bitwarden" }, "updateInBitwardenSlideDesc": { - "message": "Bitwarden will then prompt you to update the password in the password manager.", + "message": "Bitwarden ће тада затражити да ажурирате лозинку у менаџеру лозинке.", "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAlt": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login" + "message": "Илустрација Bitwarden-ског обавештења за ажирриање пријаве" }, "turnOnAutofill": { "message": "Омогућите ауто-пуњење" @@ -4108,7 +4111,7 @@ "message": "Активан налог" }, "bitwardenAccount": { - "message": "Bitwarden account" + "message": "Bitwarden налог" }, "availableAccounts": { "message": "Доступни налози" @@ -5145,6 +5148,6 @@ "message": "Да би сте користили биометријско откључавање, надоградите вашу апликацију на рачунару, или онемогућите откључавање отиском прста у подешавањима на рачунару." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Променити ризичну лозинку" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 152ac67ea46..74c17f93511 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Nej" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Ett okänt fel har inträffat." }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index bfce2ae3757..dd0bf23c799 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -901,6 +901,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occurred." }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index a6eb0dde351..6657625a7b9 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -901,6 +901,9 @@ "no": { "message": "ไม่ใช่" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "An unexpected error has occured." }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 6b89fbc2eb0..784c1731a24 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Hayır" }, + "location": { + "message": "Konum" + }, "unexpectedError": { "message": "Beklenmedik bir hata oluştu." }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index d437501c444..69b6d3d5ce8 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -81,7 +81,7 @@ "message": "Підказка для головного пароля (необов'язково)" }, "passwordStrengthScore": { - "message": "Password strength score $SCORE$", + "message": "Рейтинг надійності пароля $SCORE$", "placeholders": { "score": { "content": "$1", @@ -650,7 +650,7 @@ "message": "Ваш браузер не підтримує копіювання даних в буфер обміну. Скопіюйте вручну." }, "verifyYourIdentity": { - "message": "Verify your identity" + "message": "Підтвердьте свою особу" }, "weDontRecognizeThisDevice": { "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." @@ -866,19 +866,19 @@ "message": "Увійти в Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Введіть код, надісланий вам електронною поштою" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Введіть код з програми автентифікації" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "Натисніть свій YubiKey для автентифікації" }, "duoTwoFactorRequiredPageSubtitle": { - "message": "Duo two-step login is required for your account. Follow the steps below to finish logging in." + "message": "Для вашого облікового запису необхідно пройти двоетапну перевірку з Duo. Виконайте наведені нижче кроки, щоб завершити вхід." }, "followTheStepsBelowToFinishLoggingIn": { - "message": "Follow the steps below to finish logging in." + "message": "Виконайте наведені нижче кроки, щоб завершити вхід." }, "restartRegistration": { "message": "Перезапустити реєстрацію" @@ -901,6 +901,9 @@ "no": { "message": "Ні" }, + "location": { + "message": "Розташування" + }, "unexpectedError": { "message": "Сталася неочікувана помилка." }, @@ -1034,7 +1037,7 @@ "message": "Натисніть на запис у режимі перегляду сховища для автозаповнення" }, "clickToAutofill": { - "message": "Click items in autofill suggestion to fill" + "message": "Натисніть запис у пропозиціях для автозаповнення" }, "clearClipboard": { "message": "Очистити буфер обміну", @@ -1051,7 +1054,7 @@ "message": "Зберегти" }, "loginSaveSuccessDetails": { - "message": "$USERNAME$ saved to Bitwarden.", + "message": "$USERNAME$ збережено до Bitwarden.", "placeholders": { "username": { "content": "$1" @@ -1060,7 +1063,7 @@ "description": "Shown to user after login is saved." }, "loginUpdatedSuccessDetails": { - "message": "$USERNAME$ updated in Bitwarden.", + "message": "$USERNAME$ оновлено у Bitwarden.", "placeholders": { "username": { "content": "$1" @@ -1069,35 +1072,35 @@ "description": "Shown to user after login is updated." }, "saveAsNewLoginAction": { - "message": "Save as new login", + "message": "Зберегти як новий запис", "description": "Button text for saving login details as a new entry." }, "updateLoginAction": { - "message": "Update login", + "message": "Оновити запис", "description": "Button text for updating an existing login entry." }, "saveLoginPrompt": { - "message": "Save login?", + "message": "Зберегти запис?", "description": "Prompt asking the user if they want to save their login details." }, "updateLoginPrompt": { - "message": "Update existing login?", + "message": "Оновити наявний запис?", "description": "Prompt asking the user if they want to update an existing login entry." }, "loginSaveSuccess": { - "message": "Login saved", + "message": "Запис збережено", "description": "Message displayed when login details are successfully saved." }, "loginUpdateSuccess": { - "message": "Login updated", + "message": "Запис оновлено", "description": "Message displayed when login details are successfully updated." }, "saveFailure": { - "message": "Error saving", + "message": "Помилка збереження", "description": "Error message shown when the system fails to save login details." }, "saveFailureDetails": { - "message": "Oh no! We couldn't save this. Try entering the details manually.", + "message": "На жаль, не вдається зберегти. Введіть дані вручну.", "description": "Detailed error message shown when saving login details fails." }, "enableChangedPasswordNotification": { @@ -1420,7 +1423,7 @@ "message": "Запам'ятати мене" }, "dontAskAgainOnThisDeviceFor30Days": { - "message": "Don't ask again on this device for 30 days" + "message": "Більше не запитувати на цьому пристрої протягом 30 днів" }, "sendVerificationCodeEmailAgain": { "message": "Надіслати код підтвердження ще раз" @@ -1429,11 +1432,11 @@ "message": "Інший спосіб двоетапної перевірки" }, "selectAnotherMethod": { - "message": "Select another method", + "message": "Обрати інший спосіб", "description": "Select another two-step login method" }, "useYourRecoveryCode": { - "message": "Use your recovery code" + "message": "Використати код відновлення" }, "insertYubiKey": { "message": "Вставте свій YubiKey в USB порт комп'ютера, потім торкніться цієї кнопки." @@ -1448,16 +1451,16 @@ "message": "Відкрити нову вкладку" }, "openInNewTab": { - "message": "Open in new tab" + "message": "Відкрити в новій вкладці" }, "webAuthnAuthenticate": { "message": "Автентифікація WebAuthn" }, "readSecurityKey": { - "message": "Read security key" + "message": "Зчитати ключ безпеки" }, "awaitingSecurityKeyInteraction": { - "message": "Awaiting security key interaction..." + "message": "Очікується взаємодія з ключем безпеки..." }, "loginUnavailable": { "message": "Вхід недоступний" @@ -1472,7 +1475,7 @@ "message": "Налаштування двоетапної перевірки" }, "selectTwoStepLoginMethod": { - "message": "Select two-step login method" + "message": "Виберіть спосіб двоетапної перевірки" }, "recoveryCodeDesc": { "message": "Втратили доступ до всіх провайдерів двоетапної перевірки? Скористайтеся кодом відновлення, щоб вимкнути двоетапну перевірку для свого облікового запису." @@ -2165,7 +2168,7 @@ "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'" }, "vaultCustomization": { - "message": "Vault customization" + "message": "Налаштування сховища" }, "vaultTimeoutAction": { "message": "Дія після часу очікування сховища" @@ -2174,13 +2177,13 @@ "message": "Дія після часу очікування" }, "newCustomizationOptionsCalloutTitle": { - "message": "New customization options" + "message": "Нові можливості налаштування" }, "newCustomizationOptionsCalloutContent": { - "message": "Customize your vault experience with quick copy actions, compact mode, and more!" + "message": "Налаштуйте своє сховище за допомогою швидких дій копіювання, компактного режиму та інших можливостей!" }, "newCustomizationOptionsCalloutLink": { - "message": "View all Appearance settings" + "message": "Всі налаштування подання" }, "lock": { "message": "Блокувати", @@ -2477,7 +2480,7 @@ "message": "Ризиковані паролі" }, "atRiskPasswordDescSingleOrg": { - "message": "$ORGANIZATION$ is requesting you change one password because it is at-risk.", + "message": "$ORGANIZATION$ вимагає зміни один пароль, оскільки він ризикований.", "placeholders": { "organization": { "content": "$1", @@ -2486,7 +2489,7 @@ } }, "atRiskPasswordsDescSingleOrgPlural": { - "message": "$ORGANIZATION$ is requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "$ORGANIZATION$ вимагає зміни $COUNT$ паролів, оскільки вони ризиковані.", "placeholders": { "organization": { "content": "$1", @@ -2499,7 +2502,7 @@ } }, "atRiskPasswordsDescMultiOrgPlural": { - "message": "Your organizations are requesting you change the $COUNT$ passwords because they are at-risk.", + "message": "Ваші організації вимагають зміни $COUNT$ паролів, оскільки вони ризиковані.", "placeholders": { "count": { "content": "$1", @@ -2526,34 +2529,34 @@ "message": "Оновіть налаштування, щоб швидше автоматично заповнювати й створювати паролі" }, "reviewAtRiskLogins": { - "message": "Review at-risk logins" + "message": "Переглянути записи з ризиком" }, "reviewAtRiskPasswords": { - "message": "Review at-risk passwords" + "message": "Переглянути ризиковані паролі" }, "reviewAtRiskLoginsSlideDesc": { - "message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.", + "message": "Паролі вашої організації ризиковані, оскільки вони ненадійні, повторно використовуються, та/або викриті.", "description": "Description of the review at-risk login slide on the at-risk password page carousel" }, "reviewAtRiskLoginSlideImgAlt": { - "message": "Illustration of a list of logins that are at-risk" + "message": "Ілюстрація списку ризикованих записів" }, "generatePasswordSlideDesc": { - "message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.", + "message": "Швидко згенеруйте надійний, унікальний пароль через меню автозаповнення Bitwarden на сайті з ризикованим паролем.", "description": "Description of the generate password slide on the at-risk password page carousel" }, "generatePasswordSlideImgAlt": { - "message": "Illustration of the Bitwarden autofill menu displaying a generated password" + "message": "Ілюстрація меню автозаповнення Bitwarden, що показує згенерований пароль" }, "updateInBitwarden": { - "message": "Update in Bitwarden" + "message": "Оновити в Bitwarden" }, "updateInBitwardenSlideDesc": { - "message": "Bitwarden will then prompt you to update the password in the password manager.", + "message": "Потім Bitwarden запропонує вам оновити пароль у менеджері паролів.", "description": "Description of the update in Bitwarden slide on the at-risk password page carousel" }, "updateInBitwardenSlideImgAlt": { - "message": "Illustration of a Bitwarden’s notification prompting the user to update the login" + "message": "Ілюстрація сповіщення Bitwarden, що спонукає користувача оновити пароль" }, "turnOnAutofill": { "message": "Увімкніть автозаповнення" @@ -4108,7 +4111,7 @@ "message": "Активний обліковий запис" }, "bitwardenAccount": { - "message": "Bitwarden account" + "message": "Обліковий запис Bitwarden" }, "availableAccounts": { "message": "Доступні облікові записи" @@ -4309,7 +4312,7 @@ } }, "copyFieldValue": { - "message": "Copy $FIELD$, $VALUE$", + "message": "Копіювати $FIELD$, $VALUE$", "description": "Title for a button that copies a field value to the clipboard.", "placeholders": { "field": { @@ -4347,7 +4350,7 @@ "message": "Сповіщення" }, "appearance": { - "message": "Вигляд" + "message": "Подання" }, "errorAssigningTargetCollection": { "message": "Помилка призначення цільової збірки." @@ -5145,6 +5148,6 @@ "message": "Щоб використовувати біометричне розблокування, оновіть комп'ютерну програму, або вимкніть розблокування відбитком пальця в налаштуваннях системи." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Змінити ризикований пароль" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index d3624dad652..84f918d1acb 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -901,6 +901,9 @@ "no": { "message": "Không" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "Một lỗi bất ngờ đã xảy ra." }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index d4fb1e8042f..409d94604d9 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -901,6 +901,9 @@ "no": { "message": "否" }, + "location": { + "message": "位置" + }, "unexpectedError": { "message": "发生意外错误。" }, @@ -1457,7 +1460,7 @@ "message": "读取安全密钥" }, "awaitingSecurityKeyInteraction": { - "message": "等待安全密钥交互……" + "message": "等待安全密钥交互..." }, "loginUnavailable": { "message": "登录不可用" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 1b44c355dcf..242a583d704 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -901,6 +901,9 @@ "no": { "message": "否" }, + "location": { + "message": "Location" + }, "unexpectedError": { "message": "發生了未預期的錯誤。" }, diff --git a/apps/browser/src/auth/popup/environment.component.html b/apps/browser/src/auth/popup/environment.component.html index ff19739548a..21e69fbbc39 100644 --- a/apps/browser/src/auth/popup/environment.component.html +++ b/apps/browser/src/auth/popup/environment.component.html @@ -1,7 +1,7 @@
- +

{{ "appName" | i18n }} diff --git a/apps/browser/src/auth/popup/set-password.component.html b/apps/browser/src/auth/popup/set-password.component.html index 6261608c345..71a2e3ac588 100644 --- a/apps/browser/src/auth/popup/set-password.component.html +++ b/apps/browser/src/auth/popup/set-password.component.html @@ -1,7 +1,7 @@
- +

{{ "setMasterPassword" | i18n }} diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 871d1b3014d..8cdfdab9524 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -99,7 +99,6 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; VaultTimeoutInputComponent, ], }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil export class AccountSecurityComponent implements OnInit, OnDestroy { protected readonly VaultTimeoutAction = VaultTimeoutAction; diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts index 0b47fa4287e..b2c20ba2849 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.spec.ts @@ -72,7 +72,7 @@ describe("AuthPopoutWindow", () => { it("closes any existing popup window types that are open to the login extension route", async () => { const loginTab = createChromeTabMock({ - url: chrome.runtime.getURL("popup/index.html#/home"), + url: chrome.runtime.getURL("popup/index.html#/login"), }); jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([loginTab]); jest.spyOn(BrowserApi, "removeWindow"); diff --git a/apps/browser/src/auth/popup/utils/auth-popout-window.ts b/apps/browser/src/auth/popup/utils/auth-popout-window.ts index 2f135038315..0646b684b22 100644 --- a/apps/browser/src/auth/popup/utils/auth-popout-window.ts +++ b/apps/browser/src/auth/popup/utils/auth-popout-window.ts @@ -13,7 +13,7 @@ const AuthPopoutType = { const extensionUnlockUrls = new Set([ chrome.runtime.getURL("popup/index.html#/lock"), - chrome.runtime.getURL("popup/index.html#/home"), + chrome.runtime.getURL("popup/index.html#/login"), ]); /** diff --git a/apps/browser/src/autofill/content/components/constants/styles.ts b/apps/browser/src/autofill/content/components/constants/styles.ts index cd6054e90ba..cdf8f1ead53 100644 --- a/apps/browser/src/autofill/content/components/constants/styles.ts +++ b/apps/browser/src/autofill/content/components/constants/styles.ts @@ -124,8 +124,6 @@ export const themes = { // For compatibility system: lightTheme, - nord: lightTheme, - solarizedDark: darkTheme, }; export const spacing = { diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts index 497664542ad..d5541b5da48 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -479,7 +479,6 @@ describe("OverlayBackground", () => { it("will set up onMessage and onConnect listeners", () => { overlayBackground["setupExtensionMessageListeners"](); - // eslint-disable-next-line expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); }); diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts index 67f7ed66885..e79cba71763 100644 --- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts +++ b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.spec.ts @@ -277,32 +277,6 @@ describe("AutofillOverlayIframeService", () => { borderColor: "#4c525f", }); }); - - it("updates the border to match the `nord` theme", () => { - const message = { - command: "initAutofillOverlayList", - theme: ThemeType.Nord, - }; - - sendPortMessage(portSpy, message); - - expect(updateElementStylesSpy).toBeCalledWith(autofillOverlayIframeService["iframe"], { - borderColor: "#2E3440", - }); - }); - - it("updates the border to match the `solarizedDark` theme", () => { - const message = { - command: "initAutofillOverlayList", - theme: ThemeType.SolarizedDark, - }; - - sendPortMessage(portSpy, message); - - expect(updateElementStylesSpy).toBeCalledWith(autofillOverlayIframeService["iframe"], { - borderColor: "#073642", - }); - }); }); describe("updating the iframe's position", () => { diff --git a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts index 402c384b8be..e0df9eb60b6 100644 --- a/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/iframe-content/autofill-overlay-iframe.service.deprecated.ts @@ -221,12 +221,6 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf if (verifiedTheme === ThemeTypes.Dark) { borderColor = "#4c525f"; } - if (theme === ThemeTypes.Nord) { - borderColor = "#2E3440"; - } - if (theme === ThemeTypes.SolarizedDark) { - borderColor = "#073642"; - } if (borderColor) { this.updateElementStyles(this.iframe, { borderColor }); } diff --git a/apps/browser/src/autofill/notification/bar.scss b/apps/browser/src/autofill/notification/bar.scss index cd995a115ff..2f1a7f1d318 100644 --- a/apps/browser/src/autofill/notification/bar.scss +++ b/apps/browser/src/autofill/notification/bar.scss @@ -334,19 +334,3 @@ button { } } } - -.theme_solarizedDark { - .notification-bar-redesign #content .inner-wrapper { - #select-folder { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgZmlsbD0nbm9uZSc+PHBhdGggc3Ryb2tlPScjZWVlOGQ1JyBkPSdtNSA2IDMgMyAzLTMnLz48L3N2Zz4="); - } - } -} - -.theme_nord { - .notification-bar-redesign #content .inner-wrapper { - #select-folder { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgZmlsbD0nbm9uZSc+PHBhdGggc3Ryb2tlPScjRTVFOUYwJyBkPSdtNSA2IDMgMyAzLTMnLz48L3N2Zz4="); - } - } -} diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts index 7d7b48f83cb..9f2947c2e99 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts @@ -302,38 +302,6 @@ describe("AutofillInlineMenuIframeService", () => { }, ); }); - - it("updates the border to match the `nord` theme", () => { - const message = { - command: "initAutofillInlineMenuList", - theme: ThemeType.Nord, - }; - - sendPortMessage(portSpy, message); - - expect(updateElementStylesSpy).toHaveBeenCalledWith( - autofillInlineMenuIframeService["iframe"], - { - borderColor: "#2E3440", - }, - ); - }); - - it("updates the border to match the `solarizedDark` theme", () => { - const message = { - command: "initAutofillInlineMenuList", - theme: ThemeType.SolarizedDark, - }; - - sendPortMessage(portSpy, message); - - expect(updateElementStylesSpy).toHaveBeenCalledWith( - autofillInlineMenuIframeService["iframe"], - { - borderColor: "#073642", - }, - ); - }); }); describe("updating the iframe's position", () => { diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts index 72bf631f50b..9a9821f643c 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts @@ -250,12 +250,6 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe if (verifiedTheme === ThemeTypes.Dark) { borderColor = "#4c525f"; } - if (theme === ThemeTypes.Nord) { - borderColor = "#2E3440"; - } - if (theme === ThemeTypes.SolarizedDark) { - borderColor = "#073642"; - } if (borderColor) { this.updateElementStyles(this.iframe, { borderColor }); } diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 72df679294d..e833420b859 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1412,7 +1412,6 @@ export default class AutofillService implements AutofillServiceInterface { let doesContainValue = false; CreditCardAutoFillConstants.CardAttributesExtended.forEach((attributeName) => { - // eslint-disable-next-line no-prototype-builtins if (doesContainValue || !field[attributeName]) { return; } diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts index dc59e05a18b..f3047947c82 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -21,8 +21,7 @@ describe("InlineMenuFieldQualificationService", () => { }); describe("isFieldForLoginForm", () => { - it("does not disqualify totp fields for premium users with flag set to true", () => { - inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true; + it("does not disqualify totp fields for premium users", () => { inlineMenuFieldQualificationService["premiumEnabled"] = true; const field = mock({ type: "text", @@ -37,24 +36,7 @@ describe("InlineMenuFieldQualificationService", () => { ); }); - it("disqualifies totp fields for premium users with flag set to false", () => { - inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = false; - inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true; - const field = mock({ - type: "text", - autoCompleteType: "one-time-code", - htmlName: "totp", - htmlID: "totp", - placeholder: "totp", - }); - - expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( - false, - ); - }); - - it("disqualifies totp fields for non-premium users with flag set to true", () => { - inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true; + it("disqualifies totp fields for non-premium users", () => { inlineMenuFieldQualificationService["premiumEnabled"] = false; const field = mock({ type: "text", diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index dff40a3ab93..046381d956c 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -151,17 +151,14 @@ export class InlineMenuFieldQualificationService ]); private totpFieldAutocompleteValue = "one-time-code"; private inlineMenuFieldQualificationFlagSet = false; - private inlineMenuTotpFeatureFlag = false; private premiumEnabled = false; constructor() { void Promise.all([ sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"), - sendExtensionMessage("getInlineMenuTotpFeatureFlag"), sendExtensionMessage("getUserPremiumStatus"), - ]).then(([fieldQualificationFlag, totpFeatureFlag, premiumStatus]) => { + ]).then(([fieldQualificationFlag, premiumStatus]) => { this.inlineMenuFieldQualificationFlagSet = !!fieldQualificationFlag?.result; - this.inlineMenuTotpFeatureFlag = !!totpFeatureFlag?.result; this.premiumEnabled = !!premiumStatus?.result; }); } @@ -180,7 +177,7 @@ export class InlineMenuFieldQualificationService /** * Totp inline menu is available only for premium users. */ - if (this.inlineMenuTotpFeatureFlag && this.premiumEnabled) { + if (this.premiumEnabled) { const isTotpField = this.isTotpField(field); // Autofill does not fill totp inputs with a "password" `type` attribute value const passwordType = field.type === "password"; diff --git a/apps/browser/src/autofill/shared/styles/variables.scss b/apps/browser/src/autofill/shared/styles/variables.scss index 12d55ad8be6..ae6a060798a 100644 --- a/apps/browser/src/autofill/shared/styles/variables.scss +++ b/apps/browser/src/autofill/shared/styles/variables.scss @@ -1,6 +1,4 @@ -@import "~nord/src/sass/nord.scss"; - -$dark-icon-themes: "theme_dark", "theme_solarizedDark", "theme_nord"; +$dark-icon-themes: "theme_dark"; $font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; $font-family-source-code-pro: "Source Code Pro", monospace; @@ -34,14 +32,6 @@ $border-color: #ced4dc; $border-radius: 3px; $focus-outline-color: #1252a3; -$solarizedDarkBase0: #839496; -$solarizedDarkBase03: #002b36; -$solarizedDarkBase02: #073642; -$solarizedDarkBase01: #586e75; -$solarizedDarkBase2: #eee8d5; -$solarizedDarkCyan: #2aa198; -$solarizedDarkGreen: #859900; - $themes: ( light: ( textColor: $text-color-light, @@ -79,41 +69,6 @@ $themes: ( passwordSpecialColor: $password-special-color-dark, passwordNumberColor: $password-number-color-dark, ), - nord: ( - textColor: $nord5, - mutedTextColor: $nord4, - backgroundColor: $nord1, - backgroundOffsetColor: darken($nord1, 2.75%), - buttonPrimaryColor: $nord8, - primaryColor: $nord9, - textContrast: $nord2, - inputBorderColor: $nord0, - inputBackgroundColor: $nord2, - borderColor: $nord0, - focusOutlineColor: lighten($focus-outline-color, 25%), - successColor: $success-color-dark, - passkeysAuthenticating: $nord4, - passwordSpecialColor: $nord12, - passwordNumberColor: $nord8, - ), - solarizedDark: ( - textColor: $solarizedDarkBase2, - // Muted uses main text color to avoid contrast issues - mutedTextColor: $solarizedDarkBase2, - backgroundColor: $solarizedDarkBase03, - backgroundOffsetColor: darken($solarizedDarkBase03, 2.75%), - buttonPrimaryColor: $solarizedDarkCyan, - primaryColor: $solarizedDarkGreen, - textContrast: $solarizedDarkBase02, - inputBorderColor: rgba($solarizedDarkBase2, 0.2), - inputBackgroundColor: $solarizedDarkBase01, - borderColor: $solarizedDarkBase2, - focusOutlineColor: lighten($focus-outline-color, 15%), - successColor: $success-color-dark, - passkeysAuthenticating: $solarizedDarkBase2, - passwordSpecialColor: #b58900, - passwordNumberColor: $solarizedDarkCyan, - ), ); @mixin themify($themes: $themes) { diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 8dccc933375..90276eaea0a 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -34,12 +34,8 @@ export default class IdleBackground { const idleHandler = (newState: string) => { if (newState === "active") { - // 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.notificationsService.reconnectFromActivity(); } else { - // 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.notificationsService.disconnectFromInactivity(); } }; diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 89244f52ecf..a5967b5fe76 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -834,10 +834,7 @@ export default class MainBackground { this.configService, ); - this.themeStateService = new DefaultThemeStateService( - this.globalStateProvider, - this.configService, - ); + this.themeStateService = new DefaultThemeStateService(this.globalStateProvider); this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService); @@ -1015,6 +1012,7 @@ export default class MainBackground { this.encryptService, this.pinService, this.accountService, + this.sdkService, ); this.individualVaultExportService = new IndividualVaultExportService( diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index a5fea0651fc..32e7432174a 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -78,7 +78,6 @@ export default class RuntimeBackground { BiometricsCommands.GetBiometricsStatusForUser, "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag", - "getInlineMenuTotpFeatureFlag", "getUserPremiumStatus", ]; @@ -217,9 +216,6 @@ export default class RuntimeBackground { ); return result; } - case "getInlineMenuTotpFeatureFlag": { - return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuTotp); - } } } diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 63bdcf0ccf0..5bfca440b99 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": "2025.2.2", + "version": "2025.3.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 6a537a11fc4..1e2ac1812ca 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": "2025.2.2", + "version": "2025.3.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts index 2c29f1e5763..457198eaa4e 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -64,8 +64,16 @@ export class PopupViewCacheService implements ViewCacheService { filter((e) => e instanceof NavigationEnd), /** Skip the first navigation triggered by `popupRouterCacheGuard` */ skip(1), + filter((e: NavigationEnd) => + // viewing/editing a cipher and navigating back to the vault list should not clear the cache + ["/view-cipher", "/edit-cipher", "/tabs/vault"].every( + (route) => !e.urlAfterRedirects.startsWith(route), + ), + ), ) - .subscribe(() => this.clearState()); + .subscribe((e) => { + return this.clearState(); + }); } /** diff --git a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts index d72ba942051..0c85e8a8e4b 100644 --- a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts +++ b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts @@ -85,7 +85,6 @@ describe("BrowserTaskSchedulerService", () => { callback, ); // @ts-expect-error mocking global browser object - // eslint-disable-next-line no-global-assign globalThis.browser = {}; chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(undefined)); }); @@ -95,8 +94,7 @@ describe("BrowserTaskSchedulerService", () => { jest.clearAllTimers(); jest.useRealTimers(); - // eslint-disable-next-line no-global-assign - globalThis.browser = undefined; + (globalThis.browser as any) = undefined; }); describe("setTimeout", () => { diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 8f5d754b554..b33940a68d2 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -7,7 +7,6 @@ import { EnvironmentSelectorRouteData, ExtensionDefaultOverlayPosition, } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect"; import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap"; import { activeAuthGuard, @@ -58,15 +57,9 @@ import { ExtensionAnonLayoutWrapperComponent, ExtensionAnonLayoutWrapperData, } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; -import { HintComponent } from "../auth/popup/hint.component"; -import { HomeComponent } from "../auth/popup/home.component"; -import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component"; -import { LoginComponentV1 } from "../auth/popup/login-v1.component"; -import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; -import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorOptionsComponentV1 } from "../auth/popup/two-factor-options-v1.component"; import { TwoFactorComponentV1 } from "../auth/popup/two-factor-v1.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -131,20 +124,19 @@ const routes: Routes = [ children: [], // Children lets us have an empty component. canActivate: [ popupRouterCacheGuard, - redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/home", locked: "/lock" }), + redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/login", locked: "/lock" }), ], }, + { + path: "home", + redirectTo: "login", + pathMatch: "full", + }, { path: "vault", redirectTo: "/tabs/vault", pathMatch: "full", }, - { - path: "home", - component: HomeComponent, - canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")], - data: { elevation: 1 } satisfies RouteDataProperties, - }, { path: "fido2", component: Fido2Component, @@ -206,40 +198,6 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...unauthUiRefreshSwap( - SsoComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "sso", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "sso", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - pageIcon: VaultIcon, - pageTitle: { - key: "enterpriseSingleSignOn", - }, - pageSubtitle: { - key: "singleSignOnEnterOrgIdentifierText", - }, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: SsoComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, - }, - ], - }, - ), { path: "device-verification", component: ExtensionAnonLayoutWrapperComponent, @@ -420,158 +378,7 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "login-with-device", - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "login-with-device", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "logInRequestSent", - }, - pageSubtitle: { - key: "aNotificationWasSentToYourDevice", - }, - showLogo: false, - showBackButton: true, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: LoginViaAuthRequestComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginViaAuthRequestComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "admin-approval-requested", - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "admin-approval-requested", - data: { - pageIcon: DevicesIcon, - pageTitle: { - key: "adminApprovalRequested", - }, - pageSubtitle: { - key: "adminApprovalRequestSentToAdmins", - }, - showLogo: false, - showBackButton: true, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [{ path: "", component: LoginViaAuthRequestComponent }], - }, - ), - ...unauthUiRefreshSwap( - HintComponent, - ExtensionAnonLayoutWrapperComponent, - { - path: "hint", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - elevation: 1, - } satisfies RouteDataProperties, - }, - { - path: "", - children: [ - { - path: "hint", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - pageTitle: { - key: "requestPasswordHint", - }, - pageSubtitle: { - key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", - }, - pageIcon: UserLockIcon, - showBackButton: true, - elevation: 1, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: PasswordHintComponent }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, - }, - ], - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "login", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { elevation: 1 }, - }, - { - path: "", - children: [ - { - path: "login", - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { - pageIcon: VaultIcon, - pageTitle: { - key: "logInToBitwarden", - }, - elevation: 1, - showAcctSwitcher: true, - } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, - children: [ - { path: "", component: LoginComponent }, - { path: "", component: LoginSecondaryContentComponent, outlet: "secondary" }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - data: { - overlayPosition: ExtensionDefaultOverlayPosition, - } satisfies EnvironmentSelectorRouteData, - }, - ], - }, - ], - }, - ), - ...unauthUiRefreshSwap( - LoginDecryptionOptionsComponentV1, - ExtensionAnonLayoutWrapperComponent, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - { - path: "login-initiated", - canActivate: [tdeDecryptionRequiredGuard()], - data: { - pageIcon: DevicesIcon, - }, - children: [{ path: "", component: LoginDecryptionOptionsComponent }], - }, - ), + { path: "", component: ExtensionAnonLayoutWrapperComponent, @@ -597,7 +404,7 @@ const routes: Routes = [ component: RegistrationStartSecondaryComponent, outlet: "secondary", data: { - loginRoute: "/home", + loginRoute: "/login", } satisfies RegistrationStartSecondaryComponentData, }, ], @@ -617,6 +424,127 @@ const routes: Routes = [ }, ], }, + { + path: "login", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageIcon: VaultIcon, + pageTitle: { + key: "logInToBitwarden", + }, + elevation: 1, + showAcctSwitcher: true, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: LoginComponent }, + { path: "", component: LoginSecondaryContentComponent, outlet: "secondary" }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: ExtensionDefaultOverlayPosition, + } satisfies EnvironmentSelectorRouteData, + }, + ], + }, + { + path: "sso", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageIcon: VaultIcon, + pageTitle: { + key: "enterpriseSingleSignOn", + }, + pageSubtitle: { + key: "singleSignOnEnterOrgIdentifierText", + }, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: SsoComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: ExtensionDefaultOverlayPosition, + } satisfies EnvironmentSelectorRouteData, + }, + ], + }, + { + path: "login-with-device", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "logInRequestSent", + }, + pageSubtitle: { + key: "aNotificationWasSentToYourDevice", + }, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaAuthRequestComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "hint", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageTitle: { + key: "requestPasswordHint", + }, + pageSubtitle: { + key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou", + }, + pageIcon: UserLockIcon, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: PasswordHintComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: ExtensionDefaultOverlayPosition, + } satisfies EnvironmentSelectorRouteData, + }, + ], + }, + { + path: "admin-approval-requested", + data: { + pageIcon: DevicesIcon, + pageTitle: { + key: "adminApprovalRequested", + }, + pageSubtitle: { + key: "adminApprovalRequestSentToAdmins", + }, + showLogo: false, + showBackButton: true, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [{ path: "", component: LoginViaAuthRequestComponent }], + }, + { + path: "login-initiated", + canActivate: [tdeDecryptionRequiredGuard()], + data: { + pageIcon: DevicesIcon, + }, + children: [{ path: "", component: LoginDecryptionOptionsComponent }], + }, { path: "lock", canActivate: [lockGuard()], diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 9a3b6429e61..6a08bf007bb 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,9 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; +import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -68,7 +70,10 @@ export class AppComponent implements OnInit, OnDestroy { private animationControlService: AnimationControlService, private biometricStateService: BiometricStateService, private biometricsService: BiometricsService, - ) {} + private deviceTrustToastService: DeviceTrustToastService, + ) { + this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); + } async ngOnInit() { initPopupClosedListener(); @@ -113,9 +118,7 @@ export class AppComponent implements OnInit, OnDestroy { }); this.changeDetectorRef.detectChanges(); } else if (msg.command === "authBlocked" || msg.command === "goHome") { - // 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(["home"]); + await this.router.navigate(["login"]); } else if ( msg.command === "locked" && (msg.userId == null || msg.userId == this.activeUserId) diff --git a/apps/browser/src/popup/scss/environment.scss b/apps/browser/src/popup/scss/environment.scss index 042bcd1b450..cd8f6379e2c 100644 --- a/apps/browser/src/popup/scss/environment.scss +++ b/apps/browser/src/popup/scss/environment.scss @@ -40,8 +40,4 @@ html.browser_safari { &.theme_light app-root { border-color: #777777; } - - &.theme_nord app-root { - border-color: #2e3440; - } } diff --git a/apps/browser/src/popup/scss/variables.scss b/apps/browser/src/popup/scss/variables.scss index cfd61cd6a2b..b78f06f2f3f 100644 --- a/apps/browser/src/popup/scss/variables.scss +++ b/apps/browser/src/popup/scss/variables.scss @@ -1,6 +1,4 @@ -@import "~nord/src/sass/nord.scss"; - -$dark-icon-themes: "theme_dark", "theme_solarizedDark", "theme_nord"; +$dark-icon-themes: "theme_dark"; $font-family-sans-serif: "DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; @@ -47,24 +45,6 @@ $button-color-danger: darken($brand-danger, 10%); $code-color: #c01176; $code-color-dark: #f08dc7; -$code-color-nord: #dbb1d5; - -$solarizedDarkBase03: #002b36; -$solarizedDarkBase02: #073642; -$solarizedDarkBase01: #586e75; -$solarizedDarkBase00: #657b83; -$solarizedDarkBase0: #839496; -$solarizedDarkBase1: #93a1a1; -$solarizedDarkBase2: #eee8d5; -$solarizedDarkBase3: #fdf6e3; -$solarizedDarkYellow: #b58900; -$solarizedDarkOrange: #cb4b16; -$solarizedDarkRed: #dc322f; -$solarizedDarkMagenta: #d33682; -$solarizedDarkViolet: #6c71c4; -$solarizedDarkBlue: #268bd2; -$solarizedDarkCyan: #2aa198; -$solarizedDarkGreen: #859900; $themes: ( light: ( @@ -194,131 +174,6 @@ $themes: ( saturate(0%) hue-rotate(93deg) brightness(103%) contrast(103%), codeColor: $code-color-dark, ), - nord: ( - textColor: $nord5, - hoverColorTransparent: rgba($text-color, 0.15), - borderColor: $nord0, - backgroundColor: $nord1, - borderColorAlt: $nord5, - backgroundColorAlt: $nord2, - scrollbarColor: $nord4, - scrollbarHoverColor: $nord6, - boxBackgroundColor: $nord2, - boxBackgroundHoverColor: $nord3, - boxBorderColor: $nord1, - tabBackgroundColor: $nord1, - tabBackgroundHoverColor: $nord2, - headerColor: $nord5, - headerBackgroundColor: $nord1, - headerBackgroundHoverColor: $nord2, - headerBorderColor: $nord0, - headerInputBackgroundColor: $nord6, - headerInputBackgroundFocusColor: $nord5, - headerInputColor: $nord2, - headerInputPlaceholderColor: $nord3, - listItemBackgroundHoverColor: $nord3, - disabledIconColor: $nord4, - disabledBoxOpacity: 0.5, - headingColor: $nord4, - labelColor: $nord4, - mutedColor: $nord4, - totpStrokeColor: $nord4, - boxRowButtonColor: $nord4, - boxRowButtonHoverColor: $nord6, - inputBorderColor: $nord0, - inputBackgroundColor: $nord2, - inputPlaceholderColor: lighten($nord3, 20%), - buttonBackgroundColor: $nord3, - buttonBorderColor: $nord0, - buttonColor: $nord5, - buttonPrimaryColor: $nord8, - buttonDangerColor: $nord11, - primaryColor: $nord9, - primaryAccentColor: $nord8, - dangerColor: $nord11, - successColor: $nord14, - infoColor: $nord9, - warningColor: $nord12, - logoSuffix: "white", - mfaLogoSuffix: "-w.png", - passwordNumberColor: $nord8, - passwordSpecialColor: $nord12, - passwordCountText: $nord5, - calloutBorderColor: $nord0, - calloutBackgroundColor: $nord2, - toastTextColor: #000000, - svgSuffix: "-dark.svg", - transparentColor: rgba(0, 0, 0, 0), - dateInputColorScheme: dark, - webkitCalendarPickerFilter: brightness(0) saturate(100%) invert(94%) sepia(5%) saturate(454%) - hue-rotate(185deg) brightness(93%) contrast(96%), - // has no hover so use same color - webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(5%) - saturate(454%) hue-rotate(185deg) brightness(93%) contrast(96%), - codeColor: $code-color-nord, - ), - solarizedDark: ( - textColor: $solarizedDarkBase2, - hoverColorTransparent: rgba($text-color, 0.15), - borderColor: $solarizedDarkBase03, - backgroundColor: $solarizedDarkBase03, - borderColorAlt: $solarizedDarkBase01, - backgroundColorAlt: $solarizedDarkBase02, - scrollbarColor: $solarizedDarkBase0, - scrollbarHoverColor: $solarizedDarkBase2, - boxBackgroundColor: $solarizedDarkBase02, - boxBackgroundHoverColor: lighten($solarizedDarkBase02, 5%), - boxBorderColor: $solarizedDarkBase02, - tabBackgroundColor: $solarizedDarkBase02, - tabBackgroundHoverColor: $solarizedDarkBase01, - headerColor: $solarizedDarkBase1, - headerBackgroundColor: $solarizedDarkBase02, - headerBackgroundHoverColor: $solarizedDarkBase01, - headerBorderColor: $solarizedDarkBase03, - headerInputBackgroundColor: darken($solarizedDarkBase0, 5%), - headerInputBackgroundFocusColor: $solarizedDarkBase1, - headerInputColor: $solarizedDarkBase02, - headerInputPlaceholderColor: lighten($solarizedDarkBase02, 5%), - listItemBackgroundHoverColor: lighten($solarizedDarkBase02, 5%), - disabledIconColor: $solarizedDarkBase0, - disabledBoxOpacity: 0.5, - headingColor: $solarizedDarkBase0, - labelColor: $solarizedDarkBase0, - mutedColor: $solarizedDarkBase0, - totpStrokeColor: $solarizedDarkBase0, - boxRowButtonColor: $solarizedDarkBase0, - boxRowButtonHoverColor: $solarizedDarkBase2, - inputBorderColor: $solarizedDarkBase03, - inputBackgroundColor: $solarizedDarkBase01, - inputPlaceholderColor: lighten($solarizedDarkBase00, 20%), - buttonBackgroundColor: $solarizedDarkBase00, - buttonBorderColor: $solarizedDarkBase03, - buttonColor: $solarizedDarkBase1, - buttonPrimaryColor: $solarizedDarkCyan, - buttonDangerColor: $solarizedDarkRed, - primaryColor: $solarizedDarkGreen, - primaryAccentColor: $solarizedDarkCyan, - dangerColor: $solarizedDarkRed, - successColor: $solarizedDarkGreen, - infoColor: $solarizedDarkGreen, - warningColor: $solarizedDarkYellow, - logoSuffix: "white", - mfaLogoSuffix: "-w.png", - passwordNumberColor: $solarizedDarkCyan, - passwordSpecialColor: $solarizedDarkYellow, - passwordCountText: $solarizedDarkBase2, - calloutBorderColor: $solarizedDarkBase03, - calloutBackgroundColor: $solarizedDarkBase01, - toastTextColor: #000000, - svgSuffix: "-solarized.svg", - transparentColor: rgba(0, 0, 0, 0), - dateInputColorScheme: dark, - webkitCalendarPickerFilter: brightness(0) saturate(100%) invert(61%) sepia(13%) saturate(289%) - hue-rotate(138deg) brightness(92%) contrast(90%), - webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(10%) - saturate(462%) hue-rotate(345deg) brightness(103%) contrast(87%), - codeColor: $code-color-dark, - ), ); @mixin themify($themes: $themes) { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index d49f48c0c64..5a2adfcf62b 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -130,7 +130,11 @@ import { KeyService, } from "@bitwarden/key-management"; import { LockComponentService } from "@bitwarden/key-management-ui"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { + DefaultSshImportPromptService, + PasswordRepromptService, + SshImportPromptService, +} from "@bitwarden/vault"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; @@ -653,6 +657,11 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionLoginDecryptionOptionsService, deps: [MessagingServiceAbstraction, Router], }), + safeProvider({ + provide: SshImportPromptService, + useClass: DefaultSshImportPromptService, + deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction], + }), ]; @NgModule({ diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index 071873b40c9..40f00ab4332 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -6,6 +6,7 @@ (onRefresh)="refreshCurrentTab()" [description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null" showAutofillButton + [disableDescriptionMargin]="showEmptyAutofillTip$ | async" [primaryActionAutofill]="clickItemsToAutofillVaultView" [groupByType]="groupByType()" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html index 4d617ff7786..c952260a9a9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html @@ -27,12 +27,7 @@ {{ "note" | i18n }} - + {{ "typeSshKey" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index db3fff04bbb..bb452b89c7b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -5,8 +5,6 @@ import { Component, Input, OnInit } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -40,13 +38,9 @@ export class NewItemDropdownV2Component implements OnInit { constructor( private router: Router, private dialogService: DialogService, - private configService: ConfigService, ) {} - sshKeysEnabled = false; - async ngOnInit() { - this.sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem); this.tab = await BrowserApi.getTabFromCurrentWindow(); } diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html index a6abe8ed3ac..6cc60eed6d5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html @@ -17,6 +17,7 @@ -
+
{{ description }}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index cb758e7a48d..9d70c0ba236 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -245,6 +245,12 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { @Input({ transform: booleanAttribute }) disableSectionMargin: boolean = false; + /** + * Remove the description margin + */ + @Input({ transform: booleanAttribute }) + disableDescriptionMargin: boolean = false; + /** * The tooltip text for the organization icon for ciphers that belong to an organization. * @param cipher diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts index 7d67a9458b2..30715ebaedf 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts @@ -47,7 +47,7 @@ describe("AppearanceV2Component", () => { const showFavicons$ = new BehaviorSubject(true); const enableBadgeCounter$ = new BehaviorSubject(true); - const selectedTheme$ = new BehaviorSubject(ThemeType.Nord); + const selectedTheme$ = new BehaviorSubject(ThemeType.Light); const enableRoutingAnimation$ = new BehaviorSubject(true); const enableCompactMode$ = new BehaviorSubject(false); const showQuickCopyActions$ = new BehaviorSubject(false); @@ -135,7 +135,7 @@ describe("AppearanceV2Component", () => { enableAnimations: true, enableFavicon: true, enableBadgeCounter: true, - theme: ThemeType.Nord, + theme: ThemeType.Light, enableCompactMode: false, showQuickCopyActions: false, width: "default", diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index d6fca96c08c..1462a2d7ab4 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -12,7 +12,7 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { @@ -60,7 +60,7 @@ export class AppearanceV2Component implements OnInit { appearanceForm = this.formBuilder.group({ enableFavicon: false, enableBadgeCounter: true, - theme: ThemeType.System, + theme: ThemeTypes.System as Theme, enableAnimations: true, enableCompactMode: false, showQuickCopyActions: false, @@ -72,7 +72,7 @@ export class AppearanceV2Component implements OnInit { formLoading = true; /** Available theme options */ - themeOptions: { name: string; value: ThemeType }[]; + themeOptions: { name: string; value: Theme }[]; /** Available width options */ protected readonly widthOptions: Option[] = [ @@ -93,9 +93,9 @@ export class AppearanceV2Component implements OnInit { private vaultSettingsService: VaultSettingsService, ) { this.themeOptions = [ - { name: i18nService.t("systemDefault"), value: ThemeType.System }, - { name: i18nService.t("light"), value: ThemeType.Light }, - { name: i18nService.t("dark"), value: ThemeType.Dark }, + { name: i18nService.t("systemDefault"), value: ThemeTypes.System }, + { name: i18nService.t("light"), value: ThemeTypes.Light }, + { name: i18nService.t("dark"), value: ThemeTypes.Dark }, ]; } @@ -191,7 +191,7 @@ export class AppearanceV2Component implements OnInit { this.messagingService.send("bgUpdateContextMenu"); } - async saveTheme(newTheme: ThemeType) { + async saveTheme(newTheme: Theme) { await this.themeStateService.setSelectedTheme(newTheme); } diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.html b/apps/browser/src/vault/popup/settings/folders-v2.component.html index 21e00757a29..35a0fbec0a9 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.html +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.html @@ -19,7 +19,7 @@ slot="end" type="button" (click)="openAddEditFolderDialog(folder)" - [appA11yTitle]="'editFolder' | i18n" + [appA11yTitle]="'editFolderWithName' | i18n: folder.name" bitIconButton="bwi-pencil-square" class="tw-self-end" data-testid="edit-folder-button" diff --git a/apps/browser/tailwind.config.js b/apps/browser/tailwind.config.js index f0a7db8e8bc..02e1d86f5ac 100644 --- a/apps/browser/tailwind.config.js +++ b/apps/browser/tailwind.config.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef, @typescript-eslint/no-require-imports */ +/* eslint-disable @typescript-eslint/no-require-imports */ const config = require("../../libs/components/tailwind.config.base"); config.content = [ diff --git a/apps/cli/package.json b/apps/cli/package.json index 4b650e58805..f8f1c8a02d9 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2025.2.0", + "version": "2025.3.0", "keywords": [ "bitwarden", "password", diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index eea63fdfc74..28c06505f8a 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -571,7 +571,7 @@ export class GetCommand extends DownloadCommand { const pubKey = Utils.fromB64ToArray(response.publicKey); fingerprint = await this.keyService.getFingerprint(id, pubKey); } catch { - // eslint-disable-next-line + // empty - handled by the null check below } } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 0e776375e6a..555337736c7 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -780,6 +780,7 @@ export class ServiceContainer { this.encryptService, this.pinService, this.accountService, + this.sdkService, ); this.individualExportService = new IndividualVaultExportService( diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs b/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs deleted file mode 100644 index 52464487ec5..00000000000 --- a/apps/desktop/desktop_native/core/src/ssh_agent/importer.rs +++ /dev/null @@ -1,402 +0,0 @@ -use ed25519; -use pkcs8::{ - der::Decode, EncryptedPrivateKeyInfo, ObjectIdentifier, PrivateKeyInfo, SecretDocument, -}; -use ssh_key::{ - private::{Ed25519Keypair, Ed25519PrivateKey, RsaKeypair}, - HashAlg, LineEnding, -}; - -const PKCS1_HEADER: &str = "-----BEGIN RSA PRIVATE KEY-----"; -const PKCS8_UNENCRYPTED_HEADER: &str = "-----BEGIN PRIVATE KEY-----"; -const PKCS8_ENCRYPTED_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; -const OPENSSH_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----"; - -pub const RSA_PKCS8_ALGORITHM_OID: ObjectIdentifier = - ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1"); - -#[derive(Debug)] -enum KeyType { - Ed25519, - Rsa, - Unknown, -} - -pub fn import_key( - encoded_key: String, - password: String, -) -> Result { - match encoded_key.lines().next() { - Some(PKCS1_HEADER) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::UnsupportedKeyType, - ssh_key: None, - }), - Some(PKCS8_UNENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, None) { - Ok(result) => Ok(result), - Err(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - }, - Some(PKCS8_ENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, Some(password)) { - Ok(result) => Ok(result), - Err(err) => match err { - SshKeyImportError::PasswordRequired => Ok(SshKeyImportResult { - status: SshKeyImportStatus::PasswordRequired, - ssh_key: None, - }), - SshKeyImportError::WrongPassword => Ok(SshKeyImportResult { - status: SshKeyImportStatus::WrongPassword, - ssh_key: None, - }), - SshKeyImportError::ParsingError => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - }, - }, - Some(OPENSSH_HEADER) => import_openssh_key(encoded_key, password), - Some(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - None => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - } -} - -fn import_pkcs8_key( - encoded_key: String, - password: Option, -) -> Result { - let der = match SecretDocument::from_pem(&encoded_key) { - Ok((_, doc)) => doc, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }); - } - }; - - let decrypted_der = match password.clone() { - Some(password) => { - let encrypted_private_key_info = match EncryptedPrivateKeyInfo::from_der(der.as_bytes()) - { - Ok(info) => info, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }); - } - }; - match encrypted_private_key_info.decrypt(password.as_bytes()) { - Ok(der) => der, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::WrongPassword, - ssh_key: None, - }); - } - } - } - None => der, - }; - - let key_type: KeyType = match PrivateKeyInfo::from_der(decrypted_der.as_bytes()) - .map_err(|_| SshKeyImportError::ParsingError)? - .algorithm - .oid - { - ed25519::pkcs8::ALGORITHM_OID => KeyType::Ed25519, - RSA_PKCS8_ALGORITHM_OID => KeyType::Rsa, - _ => KeyType::Unknown, - }; - - match key_type { - KeyType::Ed25519 => { - let pk: ed25519::KeypairBytes = match password { - Some(password) => { - pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) - .map_err(|err| match err { - ed25519::pkcs8::Error::EncryptedPrivateKey(_) => { - SshKeyImportError::WrongPassword - } - _ => SshKeyImportError::ParsingError, - })? - } - None => ed25519::pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) - .map_err(|_| SshKeyImportError::ParsingError)?, - }; - let pk: Ed25519Keypair = - Ed25519Keypair::from(Ed25519PrivateKey::from_bytes(&pk.secret_key)); - let private_key = ssh_key::private::PrivateKey::from(pk); - Ok(SshKeyImportResult { - status: SshKeyImportStatus::Success, - ssh_key: Some(SshKey { - private_key: private_key.to_openssh(LineEnding::LF).unwrap().to_string(), - public_key: private_key.public_key().to_string(), - key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), - }), - }) - } - KeyType::Rsa => { - let pk: rsa::RsaPrivateKey = match password { - Some(password) => { - pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password) - .map_err(|err| match err { - pkcs8::Error::EncryptedPrivateKey(_) => { - SshKeyImportError::WrongPassword - } - _ => SshKeyImportError::ParsingError, - })? - } - None => pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key) - .map_err(|_| SshKeyImportError::ParsingError)?, - }; - let rsa_keypair: Result = RsaKeypair::try_from(pk); - match rsa_keypair { - Ok(rsa_keypair) => { - let private_key = ssh_key::private::PrivateKey::from(rsa_keypair); - Ok(SshKeyImportResult { - status: SshKeyImportStatus::Success, - ssh_key: Some(SshKey { - private_key: private_key - .to_openssh(LineEnding::LF) - .unwrap() - .to_string(), - public_key: private_key.public_key().to_string(), - key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), - }), - }) - } - Err(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - } - } - _ => Ok(SshKeyImportResult { - status: SshKeyImportStatus::UnsupportedKeyType, - ssh_key: None, - }), - } -} - -fn import_openssh_key( - encoded_key: String, - password: String, -) -> Result { - let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key); - let private_key = match private_key { - Ok(k) => k, - Err(err) => { - match err { - ssh_key::Error::AlgorithmUnknown - | ssh_key::Error::AlgorithmUnsupported { algorithm: _ } => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::UnsupportedKeyType, - ssh_key: None, - }); - } - _ => {} - } - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }); - } - }; - - if private_key.is_encrypted() && password.is_empty() { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::PasswordRequired, - ssh_key: None, - }); - } - let private_key = if private_key.is_encrypted() { - match private_key.decrypt(password.as_bytes()) { - Ok(k) => k, - Err(_) => { - return Ok(SshKeyImportResult { - status: SshKeyImportStatus::WrongPassword, - ssh_key: None, - }); - } - } - } else { - private_key - }; - - match private_key.to_openssh(LineEnding::LF) { - Ok(private_key_openssh) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::Success, - ssh_key: Some(SshKey { - private_key: private_key_openssh.to_string(), - public_key: private_key.public_key().to_string(), - key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(), - }), - }), - Err(_) => Ok(SshKeyImportResult { - status: SshKeyImportStatus::ParsingError, - ssh_key: None, - }), - } -} - -#[derive(PartialEq, Debug)] -pub enum SshKeyImportStatus { - /// ssh key was parsed correctly and will be returned in the result - Success, - /// ssh key was parsed correctly but is encrypted and requires a password - PasswordRequired, - /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect - WrongPassword, - /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key - ParsingError, - /// ssh key type is not supported - UnsupportedKeyType, -} - -pub enum SshKeyImportError { - ParsingError, - PasswordRequired, - WrongPassword, -} - -pub struct SshKeyImportResult { - pub status: SshKeyImportStatus, - pub ssh_key: Option, -} - -pub struct SshKey { - pub private_key: String, - pub public_key: String, - pub key_fingerprint: String, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn import_key_ed25519_openssh_unencrypted() { - let private_key = include_str!("./test_keys/ed25519_openssh_unencrypted"); - let public_key = include_str!("./test_keys/ed25519_openssh_unencrypted.pub").trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_ed25519_openssh_encrypted() { - let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); - let public_key = include_str!("./test_keys/ed25519_openssh_encrypted.pub").trim(); - let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_openssh_unencrypted() { - let private_key = include_str!("./test_keys/rsa_openssh_unencrypted"); - let public_key = include_str!("./test_keys/rsa_openssh_unencrypted.pub").trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_openssh_encrypted() { - let private_key = include_str!("./test_keys/rsa_openssh_encrypted"); - let public_key = include_str!("./test_keys/rsa_openssh_encrypted.pub").trim(); - let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_ed25519_pkcs8_unencrypted() { - let private_key = include_str!("./test_keys/ed25519_pkcs8_unencrypted"); - let public_key = - include_str!("./test_keys/ed25519_pkcs8_unencrypted.pub").replace("testkey", ""); - let public_key = public_key.trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_pkcs8_unencrypted() { - let private_key = include_str!("./test_keys/rsa_pkcs8_unencrypted"); - // for whatever reason pkcs8 + rsa does not include the comment in the public key - let public_key = - include_str!("./test_keys/rsa_pkcs8_unencrypted.pub").replace("testkey", ""); - let public_key = public_key.trim(); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_rsa_pkcs8_encrypted() { - let private_key = include_str!("./test_keys/rsa_pkcs8_encrypted"); - let public_key = include_str!("./test_keys/rsa_pkcs8_encrypted.pub").replace("testkey", ""); - let public_key = public_key.trim(); - let result = import_key(private_key.to_string(), "password".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::Success); - assert_eq!(result.ssh_key.unwrap().public_key, public_key); - } - - #[test] - fn import_key_ed25519_openssh_encrypted_wrong_password() { - let private_key = include_str!("./test_keys/ed25519_openssh_encrypted"); - let result = import_key(private_key.to_string(), "wrongpassword".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::WrongPassword); - } - - #[test] - fn import_non_key_error() { - let result = import_key("not a key".to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::ParsingError); - } - - #[test] - fn import_ecdsa_error() { - let private_key = include_str!("./test_keys/ecdsa_openssh_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType); - } - - // Putty-exported keys should be supported, but are not due to a parser incompatibility. - // Should this test start failing, please change it to expect a correct key, and - // make sure the documentation support for putty-exported keys this is updated. - // https://bitwarden.atlassian.net/browse/PM-14989 - #[test] - fn import_key_ed25519_putty() { - let private_key = include_str!("./test_keys/ed25519_putty_openssh_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::ParsingError); - } - - // Putty-exported keys should be supported, but are not due to a parser incompatibility. - // Should this test start failing, please change it to expect a correct key, and - // make sure the documentation support for putty-exported keys this is updated. - // https://bitwarden.atlassian.net/browse/PM-14989 - #[test] - fn import_key_rsa_openssh_putty() { - let private_key = include_str!("./test_keys/rsa_putty_openssh_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::ParsingError); - } - - #[test] - fn import_key_rsa_pkcs8_putty() { - let private_key = include_str!("./test_keys/rsa_putty_pkcs1_unencrypted"); - let result = import_key(private_key.to_string(), "".to_string()).unwrap(); - assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType); - } -} diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs index 3fe327948f8..5f794b49c73 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -16,7 +16,6 @@ mod platform_ssh_agent; #[cfg(any(target_os = "linux", target_os = "macos"))] mod peercred_unix_listener_stream; -pub mod importer; pub mod peerinfo; mod request_parser; diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index c40b7aed487..92f31cf5f89 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -51,22 +51,6 @@ export declare namespace sshagent { publicKey: string keyFingerprint: string } - export const enum SshKeyImportStatus { - /** ssh key was parsed correctly and will be returned in the result */ - Success = 0, - /** ssh key was parsed correctly but is encrypted and requires a password */ - PasswordRequired = 1, - /** ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect */ - WrongPassword = 2, - /** ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key */ - ParsingError = 3, - /** ssh key type is not supported (e.g. ecdsa) */ - UnsupportedKeyType = 4 - } - export interface SshKeyImportResult { - status: SshKeyImportStatus - sshKey?: SshKey - } export interface SshUiRequest { cipherId?: string isList: boolean @@ -79,7 +63,6 @@ export declare namespace sshagent { export function isRunning(agentState: SshAgentState): boolean export function setKeys(agentState: SshAgentState, newKeys: Array): void export function lock(agentState: SshAgentState): void - export function importKey(encodedKey: string, password: string): SshKeyImportResult export function clearKeys(agentState: SshAgentState): void export class SshAgentState { } } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 7d20bd50699..d0c859d427c 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -182,67 +182,6 @@ pub mod sshagent { pub key_fingerprint: String, } - impl From for SshKey { - fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self { - SshKey { - private_key: key.private_key, - public_key: key.public_key, - key_fingerprint: key.key_fingerprint, - } - } - } - - #[napi] - pub enum SshKeyImportStatus { - /// ssh key was parsed correctly and will be returned in the result - Success, - /// ssh key was parsed correctly but is encrypted and requires a password - PasswordRequired, - /// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect - WrongPassword, - /// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key - ParsingError, - /// ssh key type is not supported (e.g. ecdsa) - UnsupportedKeyType, - } - - impl From for SshKeyImportStatus { - fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self { - match status { - desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => { - SshKeyImportStatus::Success - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => { - SshKeyImportStatus::PasswordRequired - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => { - SshKeyImportStatus::WrongPassword - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => { - SshKeyImportStatus::ParsingError - } - desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => { - SshKeyImportStatus::UnsupportedKeyType - } - } - } - } - - #[napi(object)] - pub struct SshKeyImportResult { - pub status: SshKeyImportStatus, - pub ssh_key: Option, - } - - impl From for SshKeyImportResult { - fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self { - SshKeyImportResult { - status: result.status.into(), - ssh_key: result.ssh_key.map(|k| k.into()), - } - } - } - #[napi(object)] pub struct SshUIRequest { pub cipher_id: Option, @@ -359,13 +298,6 @@ pub mod sshagent { .map_err(|e| napi::Error::from_reason(e.to_string())) } - #[napi] - pub fn import_key(encoded_key: String, password: String) -> napi::Result { - let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password) - .map_err(|e| napi::Error::from_reason(e.to_string()))?; - Ok(result.into()) - } - #[napi] pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { let bitwarden_agent_state = &mut agent_state.state; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index c0dcd9a504a..d4fe93d05b9 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": "2025.2.1", + "version": "2025.3.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js index 05e2b04124f..5657df3afcf 100644 --- a/apps/desktop/postcss.config.js +++ b/apps/desktop/postcss.config.js @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-require-imports, no-undef */ +/* eslint-disable @typescript-eslint/no-require-imports */ module.exports = { plugins: [ require("postcss-import"), diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index e76526a6618..bcbd9969f96 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -423,7 +423,7 @@ "enableHardwareAccelerationDesc" | i18n }}
-
+
@@ -559,16 +568,6 @@
-
- -
diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index ae332c9723b..2c8b5a8321a 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -3,8 +3,6 @@ import { DatePipe } from "@angular/common"; import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { NgForm } from "@angular/forms"; -import { sshagent as sshAgent } from "desktop_native/napi"; -import { lastValueFrom } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; @@ -25,8 +23,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { SshKeyPasswordPromptComponent } from "@bitwarden/importer-ui"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; const BroadcasterSubscriptionId = "AddEditComponent"; @@ -60,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On toastService: ToastService, cipherAuthorizationService: CipherAuthorizationService, sdkService: SdkService, + sshImportPromptService: SshImportPromptService, ) { super( cipherService, @@ -82,6 +80,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On cipherAuthorizationService, toastService, sdkService, + sshImportPromptService, ); } @@ -159,69 +158,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On this.cipher.revisionDate = cipher.revisionDate; } - async importSshKeyFromClipboard(password: string = "") { - const key = await this.platformUtilsService.readFromClipboard(); - const parsedKey = await ipc.platform.sshAgent.importKey(key, password); - if (parsedKey == null) { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("invalidSshKey"), - }); - return; - } - - switch (parsedKey.status) { - case sshAgent.SshKeyImportStatus.ParsingError: - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("invalidSshKey"), - }); - return; - case sshAgent.SshKeyImportStatus.UnsupportedKeyType: - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("sshKeyTypeUnsupported"), - }); - return; - case sshAgent.SshKeyImportStatus.PasswordRequired: - case sshAgent.SshKeyImportStatus.WrongPassword: - if (password !== "") { - this.toastService.showToast({ - variant: "error", - title: "", - message: this.i18nService.t("sshKeyWrongPassword"), - }); - } else { - password = await this.getSshKeyPassword(); - if (password === "") { - return; - } - await this.importSshKeyFromClipboard(password); - } - return; - default: - this.cipher.sshKey.privateKey = parsedKey.sshKey.privateKey; - this.cipher.sshKey.publicKey = parsedKey.sshKey.publicKey; - this.cipher.sshKey.keyFingerprint = parsedKey.sshKey.keyFingerprint; - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("sshKeyPasted"), - }); - } - } - - async getSshKeyPassword(): Promise { - const dialog = this.dialogService.open(SshKeyPasswordPromptComponent, { - ariaModal: true, - }); - - return await lastValueFrom(dialog.closed); - } - truncateString(value: string, length: number) { return value.length > length ? value.substring(0, length) + "..." : value; } diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html index 55e10980ad1..c3dcd191dfc 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html @@ -82,7 +82,6 @@
  • diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts index f89a72b5d2b..0f949e17146 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts @@ -31,7 +31,7 @@ export type VaultFilterSection = { }; action: (filterNode: TreeNode) => Promise; edit?: { - text: string; + filterName: string; action: (filter: VaultFilterType) => void; }; add?: { diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 000feeaf337..489f42649f9 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -9,14 +9,28 @@ import { OnInit, Output, } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; -import { Unassigned, CollectionView } from "@bitwarden/admin-console/common"; +import { + Unassigned, + CollectionView, + CollectionAdminService, +} from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +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 { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { BreadcrumbsModule, MenuModule } from "@bitwarden/components"; +import { + BreadcrumbsModule, + DialogService, + MenuModule, + SimpleDialogOptions, +} from "@bitwarden/components"; import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog"; import { HeaderModule } from "../../../layouts/header/header.module"; @@ -81,7 +95,13 @@ export class VaultHeaderComponent implements OnInit { /** Emits an event when the delete collection button is clicked in the header */ @Output() onDeleteCollection = new EventEmitter(); - constructor(private i18nService: I18nService) {} + constructor( + private i18nService: I18nService, + private collectionAdminService: CollectionAdminService, + private dialogService: DialogService, + private router: Router, + private configService: ConfigService, + ) {} async ngOnInit() {} @@ -199,6 +219,56 @@ export class VaultHeaderComponent implements OnInit { } async addCollection(): Promise { + const organization = this.organizations?.find( + (org) => org.productTierType === ProductTierType.Free, + ); + const isBreadcrumbEventLogsEnabled = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs), + ); + if ( + this.organizations.length == 1 && + organization.productTierType === ProductTierType.Free && + isBreadcrumbEventLogsEnabled + ) { + const collections = await this.collectionAdminService.getAll(organization.id); + if (collections.length === organization.maxCollections) { + await this.showFreeOrgUpgradeDialog(organization); + return; + } + } this.onAddCollection.emit(); } + + private async showFreeOrgUpgradeDialog(organization: Organization): Promise { + const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = { + title: this.i18nService.t("upgradeOrganization"), + content: this.i18nService.t( + organization.canEditSubscription + ? "freeOrgMaxCollectionReachedManageBilling" + : "freeOrgMaxCollectionReachedNoManageBilling", + organization.maxCollections, + ), + type: "primary", + }; + + if (organization.canEditSubscription) { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade"); + } else { + orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok"); + orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn + } + + const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts); + const result: boolean | undefined = await firstValueFrom(simpleDialog.closed); + + if (!result) { + return; + } + + if (organization.canEditSubscription) { + await this.router.navigate(["/organizations", organization.id, "billing", "subscription"], { + queryParams: { upgrade: true }, + }); + } + } } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 9983567a4d1..b74af0b1b9c 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -24,6 +24,7 @@ import { take, takeUntil, tap, + catchError, } from "rxjs/operators"; import { @@ -80,6 +81,7 @@ import { CollectionDialogTabType, openCollectionDialog, } from "../../admin-console/organizations/shared/components/collection-dialog"; +import { BillingNotificationService } from "../../billing/services/billing-notification.service"; import { TrialFlowService } from "../../billing/services/trial-flow.service"; import { FreeTrial } from "../../billing/types/free-trial"; import { SharedModule } from "../../shared/shared.module"; @@ -213,20 +215,25 @@ export class VaultComponent implements OnInit, OnDestroy { ownerOrgs.map((org) => combineLatest([ this.organizationApiService.getSubscription(org.id), - this.organizationBillingService.getPaymentSource(org.id), + from(this.organizationBillingService.getPaymentSource(org.id)).pipe( + catchError((error: unknown) => { + this.billingNotificationService.handleError(error); + return of(null); + }), + ), ]).pipe( - map(([subscription, paymentSource]) => { - return this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( + map(([subscription, paymentSource]) => + this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( org, subscription, paymentSource, - ); - }), + ), + ), ), ), ); }), - map((results) => results.filter((result) => result.shownBanner)), + map((results) => results.filter((result) => result !== null && result.shownBanner)), shareReplay({ refCount: false, bufferSize: 1 }), ); @@ -262,6 +269,7 @@ export class VaultComponent implements OnInit, OnDestroy { protected billingApiService: BillingApiServiceAbstraction, private trialFlowService: TrialFlowService, private organizationBillingService: OrganizationBillingServiceAbstraction, + private billingNotificationService: BillingNotificationService, ) {} async ngOnInit() { diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 8490ec6c9db..89f3b79f1fb 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -28,7 +28,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; import { AddEditComponent as BaseAddEditComponent } from "../individual-vault/add-edit.component"; @@ -64,6 +64,7 @@ export class AddEditComponent extends BaseAddEditComponent { cipherAuthorizationService: CipherAuthorizationService, toastService: ToastService, sdkService: SdkService, + sshImportPromptService: SshImportPromptService, ) { super( cipherService, @@ -88,6 +89,7 @@ export class AddEditComponent extends BaseAddEditComponent { cipherAuthorizationService, toastService, sdkService, + sshImportPromptService, ); } diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index c4e0496adbb..a33d058430c 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nee" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Teken aan of skep ’n nuwe rekening vir toegang tot u beveiligde kluis." }, diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 117080e39f9..508d9f4438a 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "لا" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "قم بتسجيل الدخول أو أنشئ حساباً جديداً لتتمكن من الوصول إلى خزانتك السرية." }, diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 14bf2a25d2b..9fb288eaac3 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -202,7 +202,7 @@ "message": "Notlar" }, "privateNote": { - "message": "Private note" + "message": "Şəxsi not" }, "note": { "message": "Not" @@ -1032,6 +1032,9 @@ "no": { "message": "Xeyr" }, + "location": { + "message": "Yerləşmə" + }, "loginOrCreateNewAccount": { "message": "Güvənli seyfinizə müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." }, @@ -1183,7 +1186,7 @@ "message": "Kimlik doğrulama seansının vaxtı bitdi. Lütfən giriş prosesini yenidən başladın." }, "verifyYourIdentity": { - "message": "Verify your Identity" + "message": "Kimliyinizi doğrulayın" }, "weDontRecognizeThisDevice": { "message": "Bu cihazı tanımırıq. Kimliyinizi doğrulamaq üçün e-poçtunuza göndərilən kodu daxil edin." @@ -5088,14 +5091,14 @@ "message": "Təşkilat sahibləri və administratorlar, bu siyasətin tətbiq edilməsindən azaddırlar." }, "limitSendViews": { - "message": "Limit views" + "message": "Baxışları limitlə" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Limitə çatdıqdan sonra bu Send-ə heç kim baxa bilməz.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ baxış qaldı", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -5105,11 +5108,11 @@ } }, "sendDetails": { - "message": "Send details", + "message": "Send detalları", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Paylaşılacaq mətn" }, "sendTypeFile": { "message": "Fayl" @@ -5118,7 +5121,7 @@ "message": "Mətn" }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Alıcıların bu \"Send\"ə müraciət etməsi üçün ixtiyari bir parol əlavə edin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -5146,14 +5149,14 @@ "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": "Bu Send-i həmişəlik silmək istədiyinizə əminsiniz?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletionDate": { "message": "Silinmə tarixi" }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "Send, bu tarixdə həmişəlik silinəcək.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -5203,7 +5206,7 @@ "message": "Silinməsi gözlənilir" }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "Mətni ilkin olaraq gizlət" }, "expired": { "message": "Müddəti bitib" @@ -5674,7 +5677,7 @@ "message": "Silinmə və son istifadə tarixlərini saxlayarkən xəta baş verdi." }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "E-poçt ünvanınız baxanlardan gizlədilsin." }, "webAuthnFallbackMsg": { "message": "2FA-nı doğrulamaq üçün lütfən aşağıdakı düyməyə klikləyin." @@ -9835,13 +9838,13 @@ "message": "Bitwarden-nin API-si haqqında daha ətraflı" }, "fileSend": { - "message": "File Send" + "message": "Fayl \"Send\"i" }, "fileSends": { "message": "Fayl \"Send\"ləri" }, "textSend": { - "message": "Text Send" + "message": "Mətn \"Send\"i" }, "textSends": { "message": "Mətn \"Send\"ləri" @@ -10476,7 +10479,7 @@ "message": "Təyin edilmiş yer sayı, boş yer sayından çoxdur." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Riskli parolları dəyişdir" }, "removeUnlockWithPinPolicyTitle": { "message": "Kilidi PIN ilə açmanı ləğv et" diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 5b82fa1b370..bf26e1e3a9e 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Не" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Увайдзіце або стварыце новы ўліковы запіс для доступу да бяспечнага сховішча." }, diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 6673bbed4d4..4be07320d38 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Не" }, + "location": { + "message": "Местоположение" + }, "loginOrCreateNewAccount": { "message": "Впишете се или създайте нов абонамент, за да достъпите защитен трезор." }, diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index db0fe70d1b3..5dfc013fa5d 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "আপনার সুরক্ষিত ভল্টে প্রবেশ করতে লগ ইন করুন অথবা একটি নতুন অ্যাকাউন্ট তৈরি করুন।" }, diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index 9450c171445..66185d736ef 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 6029c1bec33..6dc3398481b 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Inicieu sessió o creeu un compte nou per accedir a la caixa forta." }, diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 3e9b22e86c4..096579fb9ae 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Umístění" + }, "loginOrCreateNewAccount": { "message": "Pro přístup do Vašeho bezpečného trezoru se přihlaste nebo si vytvořte nový účet." }, diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index c68e659224f..bbd655a6a7c 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 7c790eae3d3..62a2ea57508 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nej" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log ind eller opret en ny konto for at tilgå din sikre boks." }, diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 89e3449d7f2..de651d54ef1 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -202,7 +202,7 @@ "message": "Notizen" }, "privateNote": { - "message": "Private note" + "message": "Private Notiz" }, "note": { "message": "Notiz" @@ -1032,6 +1032,9 @@ "no": { "message": "Nein" }, + "location": { + "message": "Standort" + }, "loginOrCreateNewAccount": { "message": "Sie müssen sich anmelden oder ein neues Konto erstellen, um auf den Tresor zugreifen zu können." }, @@ -1183,7 +1186,7 @@ "message": "Die Authentifizierungssitzung ist abgelaufen. Bitte starte den Anmeldeprozess neu." }, "verifyYourIdentity": { - "message": "Verify your Identity" + "message": "Verifiziere deine Identität" }, "weDontRecognizeThisDevice": { "message": "Wir erkennen dieses Gerät nicht. Gib den an deine E-Mail-Adresse gesendeten Code ein, um deine Identität zu verifizieren." @@ -2256,7 +2259,7 @@ "message": "Zugriff widerrufen" }, "revoke": { - "message": "Zurückziehen" + "message": "Widerrufen" }, "twoStepLoginProviderEnabled": { "message": "Dieser Zwei-Faktor-Authentifizierungsanbieter ist für dein Konto aktiviert." @@ -5088,14 +5091,14 @@ "message": "Organisationseigentümer und Administratoren sind von der Durchsetzung dieser Richtlinie ausgenommen." }, "limitSendViews": { - "message": "Limit views" + "message": "Ansichten begrenzen" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Nach Erreichen des Limits kann niemand mehr dieses Send sehen.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ Ansichten übrig", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -5105,11 +5108,11 @@ } }, "sendDetails": { - "message": "Send details", + "message": "Send-Details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Zu teilender Text" }, "sendTypeFile": { "message": "Datei" @@ -5118,7 +5121,7 @@ "message": "Text" }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Füge ein optionales Passwort hinzu, mit dem Empfänger auf dieses Send zugreifen können.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { @@ -5146,14 +5149,14 @@ "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": "Bist du sicher, dass du dieses Send dauerhaft löschen möchtest?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletionDate": { "message": "Löschdatum" }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "Das Send wird an diesem Datum dauerhaft gelöscht.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -5203,7 +5206,7 @@ "message": "Ausstehende Löschung" }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "Text standardmäßig ausblenden" }, "expired": { "message": "Abgelaufen" @@ -5674,7 +5677,7 @@ "message": "Es gab einen Fehler beim Speichern deiner Lösch- und Verfallsdaten." }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "Verberge deine E-Mail-Adresse vor Betrachtern." }, "webAuthnFallbackMsg": { "message": "Um deine 2FA zu verifizieren, klicke bitte unten auf den Button." @@ -9835,13 +9838,13 @@ "message": "Erfahre mehr über die API von Bitwarden" }, "fileSend": { - "message": "File Send" + "message": "Datei-Send" }, "fileSends": { "message": "Datei-Sends" }, "textSend": { - "message": "Text Send" + "message": "Text-Send" }, "textSends": { "message": "Text-Sends" @@ -10476,7 +10479,7 @@ "message": "Die zugewiesenen Plätze überschreiten die verfügbaren Plätze." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Gefährdetes Passwort ändern" }, "removeUnlockWithPinPolicyTitle": { "message": "Entsperren mit PIN entfernen" @@ -10485,7 +10488,7 @@ "message": "Mitgliedern nicht erlauben, ihr Konto mit einer PIN zu entsperren." }, "limitedEventLogs": { - "message": "$PRODUCT_TYPE$ Pakete haben keinen Zugriff auf echte Ereignisprotokolle", + "message": "$PRODUCT_TYPE$-Abos haben keinen Zugriff auf echte Ereignisprotokolle", "placeholders": { "product_type": { "content": "$1", @@ -10494,12 +10497,12 @@ } }, "upgradeForFullEvents": { - "message": "Erhalte vollen Zugriff auf Organisations-Event-Logs durch ein Upgrade auf einen Team- oder Enterprise-Plan." + "message": "Erhalte vollen Zugriff auf Ereignisprotokolle von Organisationen durch ein Upgrade auf ein Teams- oder Enterprise-Abo." }, "upgradeEventLogTitle": { "message": "Upgrade für echte Ereignisprotokolldaten" }, "upgradeEventLogMessage": { - "message": "Diese Ereignisse sind nur Beispiele und spiegeln keine realen Ereignisse in deinen Bitwarden-Organisation wider." + "message": "Diese Ereignisse sind nur Beispiele und spiegeln keine realen Ereignisse in deiner Bitwarden-Organisation wider." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index a6cf4915ea5..2a5d2537a56 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Όχι" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Συνδεθείτε ή δημιουργήστε νέο λογαριασμό για να αποκτήσετε πρόσβαση στο vault σας." }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 229cca65ae5..68900b2ed74 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -38,7 +38,7 @@ "restoreMembers": { "message": "Restore members" }, - "cannotRestoreAccessError":{ + "cannotRestoreAccessError": { "message": "Cannot restore organization access" }, "allApplicationsWithCount": { @@ -449,6 +449,9 @@ "dragToSort": { "message": "Drag to sort" }, + "dragToReorder": { + "message": "Drag to reorder" + }, "cfTypeText": { "message": "Text" }, @@ -491,6 +494,19 @@ "editFolder": { "message": "Edit folder" }, + "editWithName": { + "message": "Edit $ITEM$: $NAME$", + "placeholders": { + "item": { + "content": "$1", + "example": "login" + }, + "name": { + "content": "$2", + "example": "Social" + } + } + }, "newFolder": { "message": "New folder" }, @@ -1355,8 +1371,8 @@ "yourAccountIsLocked": { "message": "Your account is locked" }, - "uuid":{ - "message" : "UUID" + "uuid": { + "message": "UUID" }, "unlock": { "message": "Unlock" @@ -4551,6 +4567,40 @@ } } }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, "keyUpdateFoldersFailed": { "message": "When updating your encryption key, your folders could not be decrypted. To continue with the update, your folders must be deleted. No vault items will be deleted if you proceed." }, @@ -5904,10 +5954,10 @@ "bulkFilteredMessage": { "message": "Excluded, not applicable for this action" }, - "nonCompliantMembersTitle":{ + "nonCompliantMembersTitle": { "message": "Non-compliant members" }, - "nonCompliantMembersError":{ + "nonCompliantMembersError": { "message": "Members that are non-compliant with the Single organization or Two-step login policy cannot be restored until they adhere to the policy requirements" }, "fingerprint": { @@ -9330,7 +9380,7 @@ "message": "for Bitwarden using the implementation guide for your Identity Provider.", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure single sign-on for Bitwarden using the implementation guide for your Identity Provider." }, - "userProvisioning":{ + "userProvisioning": { "message": "User provisioning" }, "scimIntegration": { @@ -9344,31 +9394,40 @@ "message": "(System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider.", "description": "This represents the end of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, - "bwdc":{ + "bwdc": { "message": "Bitwarden Directory Connector" }, "bwdcDesc": { "message": "Configure Bitwarden Directory Connector to automatically provision users and groups using the implementation guide for your Identity Provider." }, - "eventManagement":{ + "eventManagement": { "message": "Event management" }, - "eventManagementDesc":{ + "eventManagementDesc": { "message": "Integrate Bitwarden event logs with your SIEM (system information and event management) system by using the implementation guide for your platform." }, - "deviceManagement":{ + "deviceManagement": { "message": "Device management" }, - "deviceManagementDesc":{ + "deviceManagementDesc": { "message": "Configure device management for Bitwarden using the implementation guide for your platform." }, + "deviceIdMissing": { + "message": "Device ID is missing" + }, + "deviceTypeMissing": { + "message": "Device type is missing" + }, + "deviceCreationDateMissing": { + "message": "Device creation date is missing" + }, "desktopRequired": { "message": "Desktop required" }, "reopenLinkOnDesktop": { "message": "Reopen this link from your email on a desktop." }, - "integrationCardTooltip":{ + "integrationCardTooltip": { "message": "Launch $INTEGRATION$ implementation guide.", "placeholders": { "integration": { @@ -9377,7 +9436,7 @@ } } }, - "smIntegrationTooltip":{ + "smIntegrationTooltip": { "message": "Set up $INTEGRATION$.", "placeholders": { "integration": { @@ -9386,7 +9445,7 @@ } } }, - "smSdkTooltip":{ + "smSdkTooltip": { "message": "View $SDK$ repository", "placeholders": { "sdk": { @@ -9395,7 +9454,7 @@ } } }, - "integrationCardAriaLabel":{ + "integrationCardAriaLabel": { "message": "open $INTEGRATION$ implementation guide in a new tab.", "placeholders": { "integration": { @@ -9404,7 +9463,7 @@ } } }, - "smSdkAriaLabel":{ + "smSdkAriaLabel": { "message": "view $SDK$ repository in a new tab.", "placeholders": { "sdk": { @@ -9413,7 +9472,7 @@ } } }, - "smIntegrationCardAriaLabel":{ + "smIntegrationCardAriaLabel": { "message": "set up $INTEGRATION$ implementation guide in a new tab.", "placeholders": { "integration": { @@ -9820,7 +9879,7 @@ "message": "Config" }, "learnMoreAboutEmergencyAccess": { - "message":"Learn more about emergency access" + "message": "Learn more about emergency access" }, "learnMoreAboutMatchDetection": { "message": "Learn more about match detection" @@ -10122,7 +10181,7 @@ "selfHostingTitleProper": { "message": "Self-Hosting" }, - "claim-domain-single-org-warning" : { + "claim-domain-single-org-warning": { "message": "Claiming a domain will turn on the single organization policy." }, "single-org-revoked-user-warning": { @@ -10363,6 +10422,36 @@ "organizationNameMaxLength": { "message": "Organization name cannot exceed 50 characters." }, + "sshKeyWrongPassword": { + "message": "The password you entered is incorrect." + }, + "importSshKey": { + "message": "Import" + }, + "confirmSshKeyPassword": { + "message": "Confirm password" + }, + "enterSshKeyPasswordDesc": { + "message": "Enter the password for the SSH key." + }, + "enterSshKeyPassword": { + "message": "Enter password" + }, + "invalidSshKey": { + "message": "The SSH key is invalid" + }, + "sshKeyTypeUnsupported": { + "message": "The SSH key type is not supported" + }, + "importSshKeyFromClipboard": { + "message": "Import key from clipboard" + }, + "sshKeyImported": { + "message": "SSH key imported successfully" + }, + "copySSHPrivateKey": { + "message": "Copy private key" + }, "openingExtension": { "message": "Opening the Bitwarden browser extension" }, @@ -10528,5 +10617,8 @@ }, "upgradeEventLogMessage":{ "message" : "These events are examples only and do not reflect real events within your Bitwarden organization." + }, + "cannotCreateCollection": { + "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index a3a50203d2d..b3af62f35eb 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 513cb09375b..6a84d1c9906 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 1b8ff46b0f4..04329b08550 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Ensalutu aŭ kreu novan konton por aliri vian sekuran trezorejon." }, diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 04120cb9ac3..985caba147b 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Identifícate o crea una nueva cuenta para acceder a tu caja fuerte." }, diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index b695242ce01..741af41d14c 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ei" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Logi sisse või loo uus konto." }, diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 14c9a044bf7..7455c0ca652 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ez" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Saioa hasi edo sortu kontu berri bat zure kutxa gotorrera sartzeko." }, diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 0c9784c8b7e..f8427a42c32 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "خیر" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امن‌تان دسترسی یابید." }, diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 051ce99a793..280226f0c6c 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ei" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." }, diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 120cd49045e..a07c2421d0e 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Hindi" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Mag-log in o gumawa ng bagong account para ma-access ang secure vault mo." }, diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 9e536ff5efc..fe1fd4bf238 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Non" }, + "location": { + "message": "Localisation" + }, "loginOrCreateNewAccount": { "message": "Connectez-vous ou créez un nouveau compte pour accéder à votre coffre sécurisé." }, diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 95cbb2aed46..c6aae437a90 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Non" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Rexístrate ou crea unha nova conta para acceder ó teu baúl." }, diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 38381c9de5c..8c0f3335129 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "לא" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "צור חשבון חדש או התחבר כדי לגשת לכספת המאובטחת שלך." }, diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index c67bad51baa..fd1cc2b8ad5 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "नहीं" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "सुरक्षित तिजोरी में प्रवेश करने के लिए नया खाता बनाएं या लॉग इन करें।" }, diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 3a390b0dcc3..d463efabc78 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Prijavi se ili stvori novi račun za pristup svojem sigurnom trezoru." }, diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 3afa9c7448e..31ee33ba028 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nem" }, + "location": { + "message": "Hely" + }, "loginOrCreateNewAccount": { "message": "Bejelentkezés vagy új fiók létrehozása a biztonsági széf eléréséhez." }, diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index b3b69999c9a..be55d7790c4 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Tidak" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Masuk atau buat akun baru untuk mengakses brankas Anda." }, diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 11ed74b5e9f..d1e74c5d670 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Luogo" + }, "loginOrCreateNewAccount": { "message": "Entra o crea un nuovo account per accedere alla tua cassaforte." }, diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index c515e4ad8f8..4c5747b151a 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "いいえ" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "安全なデータ保管庫へアクセスするためにログインまたはアカウントを作成してください。" }, diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index fe4eec98ace..5a7be95dfba 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "არა" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "სისტემაში შედით ან შექმენით ახალი ანგარიში თქვენს დაცულ საცავთან საწვდომად." }, diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 78005200fb9..c205ace9ac1 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index ae2a0d1d596..d817f606008 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "ಇಲ್ಲ" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "ನಿಮ್ಮ ಸುರಕ್ಷಿತ ವಾಲ್ಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಲಾಗ್ ಇನ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಿ." }, diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index b515513cba1..f389ef82312 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "아니오" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "안전 보관함에 접근하려면 로그인하거나 새 계정을 만드세요." }, diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 3f03295b90e..9dc34f9e358 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nē" }, + "location": { + "message": "Atrašanās vieta" + }, "loginOrCreateNewAccount": { "message": "Jāpiesakās vai jāizveido jauns konts, lai piekļūtu drošajai glabātavai." }, diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index e45404f5f8d..8e23fe517e2 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "അല്ല" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "നിങ്ങളുടെ സുരക്ഷിത വാൾട്ടിലേക്ക് പ്രവേശിക്കാൻ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ ഒരു പുതിയ അക്കൗണ്ട് സൃഷ്ടിക്കുക." }, diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 78005200fb9..c205ace9ac1 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 78005200fb9..c205ace9ac1 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 7f5d51d3696..2d58852e557 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nei" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Logg på eller opprett en ny konto for å få tilgang til ditt sikre hvelv." }, diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 9ae54d583db..6774111e2ea 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index ce028081569..cfcdef41085 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nee" }, + "location": { + "message": "Locatie" + }, "loginOrCreateNewAccount": { "message": "Log in of maak een nieuw account aan om toegang te krijgen tot je beveiligde kluis." }, diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 7e565bfec43..5f0b86a3192 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 78005200fb9..c205ace9ac1 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 799fa2c28a2..73802dc8531 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nie" }, + "location": { + "message": "Lokalizacja" + }, "loginOrCreateNewAccount": { "message": "Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do Twojego bezpiecznego sejfu." }, diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index e24d0dcb3b0..037a17c05ca 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Não" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Inicie a sessão ou crie uma nova conta para acessar seu cofre seguro." }, diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index e6ed260ad50..3dbabdd4e10 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Não" }, + "location": { + "message": "Localização" + }, "loginOrCreateNewAccount": { "message": "Inicie sessão ou crie uma nova conta para aceder ao seu cofre seguro." }, diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index e42e071980f..670dbf0b8db 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nu" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Autentificați-vă sau creați un cont nou pentru a accesa seiful dvs. securizat." }, diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 9add86c9bdb..1ee34c7351c 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Нет" }, + "location": { + "message": "Местоположение" + }, "loginOrCreateNewAccount": { "message": "Войдите или создайте новый аккаунт для доступа к вашему защищенному хранилищу." }, diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 6a6e8bb210a..ec20b090923 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "නැහැ" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index c8f00127233..d83f441f8e6 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nie" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Prihláste sa, alebo vytvorte nový účet pre prístup k vášmu bezpečnému trezoru." }, diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 8b3bc4b9b14..de787b66af1 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega zavarovanega trezorja." }, diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 6e3c5c6b116..eaa5cc50639 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -6,7 +6,7 @@ "message": "Критичне апликације" }, "noCriticalAppsAtRisk": { - "message": "No critical applications at risk" + "message": "Нема критичних апликација у ризику" }, "accessIntelligence": { "message": "Приступи интелигенцији" @@ -202,7 +202,7 @@ "message": "Напомене" }, "privateNote": { - "message": "Private note" + "message": "Приватна белешка" }, "note": { "message": "Белешка" @@ -1032,6 +1032,9 @@ "no": { "message": "Не" }, + "location": { + "message": "Локација" + }, "loginOrCreateNewAccount": { "message": "Пријавите се или креирајте нови налог за приступ Сефу." }, @@ -1183,7 +1186,7 @@ "message": "Истекло је време сесије за аутентификацију. Молим вас покрените процес пријаве поново." }, "verifyYourIdentity": { - "message": "Verify your Identity" + "message": "Потврдите идентитет" }, "weDontRecognizeThisDevice": { "message": "Не препознајемо овај уређај. Унесите код послат на адресу ваше електронске поште да би сте потврдили ваш идентитет." @@ -2256,7 +2259,7 @@ "message": "Опозови Приступ" }, "revoke": { - "message": "Revoke" + "message": "Опозови" }, "twoStepLoginProviderEnabled": { "message": "Овај добављач услуге пријављивања у два корака је омогућен на вашем налогу." @@ -5088,14 +5091,14 @@ "message": "Власници и администратори организација изузети су ове политике." }, "limitSendViews": { - "message": "Limit views" + "message": "Ограничити приказе" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Нико не може да види ово Send након што се достигне ограничење.", "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": { @@ -5105,11 +5108,11 @@ } }, "sendDetails": { - "message": "Send details", + "message": "Детаљи Send-а", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Текст за дељење" }, "sendTypeFile": { "message": "Датотека" @@ -5118,7 +5121,7 @@ "message": "Текст" }, "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." }, "createSend": { @@ -5146,14 +5149,14 @@ "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." }, "deletionDate": { "message": "Брисање после" }, "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": { @@ -5203,7 +5206,7 @@ "message": "Брисање на чекању" }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "Сакриј текст подразумевано" }, "expired": { "message": "Истекло" @@ -5674,7 +5677,7 @@ "message": "Појавила се грешка при снимању датума брисања и истицања." }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "Сакријте свој имејл од гледалаца." }, "webAuthnFallbackMsg": { "message": "Да би проверили Ваш 2FA Кликните на дугме испод." @@ -9835,13 +9838,13 @@ "message": "Сазнајте више о API Bitwarden-а" }, "fileSend": { - "message": "File Send" + "message": "Датотека „Send“" }, "fileSends": { "message": "Датотека „Send“" }, "textSend": { - "message": "Text Send" + "message": "Текст „Send“" }, "textSends": { "message": "Текст „Send“" @@ -10476,7 +10479,7 @@ "message": "Assigned seats exceed available seats." }, "changeAtRiskPassword": { - "message": "Change at-risk password" + "message": "Променити ризичну лозинку" }, "removeUnlockWithPinPolicyTitle": { "message": "Remove Unlock with PIN" @@ -10485,7 +10488,7 @@ "message": "Do not allow members to unlock their account with a PIN." }, "limitedEventLogs": { - "message": "$PRODUCT_TYPE$ plans do not have access to real event logs", + "message": "$PRODUCT_TYPE$ планови немају приступ стварним извештајима догађаја", "placeholders": { "product_type": { "content": "$1", @@ -10494,12 +10497,12 @@ } }, "upgradeForFullEvents": { - "message": "Get full access to organization event logs by upgrading to a Teams or Enterprise plan." + "message": "Добијте потпуни приступ извештајима о догађајима организације надоградњом на Teams или Enterprise." }, "upgradeEventLogTitle": { - "message": "Upgrade for real event log data" + "message": "Надоградите за извештај о реалним догађајима" }, "upgradeEventLogMessage": { - "message": "These events are examples only and do not reflect real events within your Bitwarden organization." + "message": "Ови догађаји су само примери и не одражавају стварне догађаје у вашем Bitwarden отганизацији." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 07a2eb9b064..8b61a2d86fa 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Ne" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Ulogujte se ili napravite novi nalog kako biste pristupili Vašem trezoru." }, diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 27409b55bd9..218954600cb 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Nej" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Logga in eller skapa ett nytt konto för att komma åt ditt valv." }, diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 78005200fb9..c205ace9ac1 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "No" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index f75146265d7..945ada11394 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "ไม่ใช่" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "ล็อกอิน หรือ สร้างบัญชีใหม่ เพื่อใช้งานตู้นิรภัยของคุณ" }, diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index bb2e86f35b0..a2af30a2adf 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Hayır" }, + "location": { + "message": "Konum" + }, "loginOrCreateNewAccount": { "message": "Güvenli kasanıza ulaşmak için giriş yapın veya yeni bir hesap oluşturun." }, @@ -1414,7 +1417,7 @@ "message": "Erişimi reddet" }, "notificationSentDeviceAnchor": { - "message": "web app" + "message": "web uygulaması" }, "notificationSentDevicePart2": { "message": "Make sure the Fingerprint phrase matches the one below before approving." @@ -1828,7 +1831,7 @@ "message": "Lütfen yeniden giriş yapın." }, "currentSession": { - "message": "Current session" + "message": "Geçerli oturum" }, "requestPending": { "message": "Request pending" @@ -2235,7 +2238,7 @@ "message": "Yönet" }, "manageCollection": { - "message": "Manage collection" + "message": "Koleksiyonu yönet" }, "viewItems": { "message": "View items" @@ -5683,7 +5686,7 @@ "message": "WebAutn ile doğrula" }, "readSecurityKey": { - "message": "Read security key" + "message": "Güvenlik anahtarını oku" }, "awaitingSecurityKeyInteraction": { "message": "Awaiting security key interaction..." @@ -8509,7 +8512,7 @@ "message": "Device removed" }, "removeDevice": { - "message": "Remove device" + "message": "Cihazı kaldır" }, "removeDeviceConfirmation": { "message": "Are you sure you want to remove this device?" @@ -9310,7 +9313,7 @@ "message": "SCIM" }, "scimIntegrationDescStart": { - "message": "Configure ", + "message": "Yapılandır ", "description": "This represents the beginning of a sentence, broken up to include links. The full sentence will be 'Configure SCIM (System for Cross-domain Identity Management) to automatically provision users and groups to Bitwarden using the implementation guide for your Identity Provider" }, "scimIntegrationDescEnd": { @@ -9599,7 +9602,7 @@ "message": "Export client report" }, "memberAccessReport": { - "message": "Member access" + "message": "Üye erişimi" }, "memberAccessReportDesc": { "message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations." @@ -9931,7 +9934,7 @@ "message": "Access to Premium features" }, "additionalStorageGbMessage": { - "message": "GB additional storage" + "message": "GB ek depolama" }, "sshKeyAlgorithm": { "message": "Key algorithm" @@ -9958,13 +9961,13 @@ "message": "ED25519" }, "sshKeyAlgorithmRSA2048": { - "message": "RSA 2048-Bit" + "message": "RSA 2048 bit" }, "sshKeyAlgorithmRSA3072": { - "message": "RSA 3072-Bit" + "message": "RSA 3072 bit" }, "sshKeyAlgorithmRSA4096": { - "message": "RSA 4096-Bit" + "message": "RSA 4096 bit" }, "premiumAccounts": { "message": "6 premium hesap" @@ -10006,7 +10009,7 @@ "message": "20 makine hesabı" }, "current": { - "message": "Current" + "message": "Geçerli" }, "secretsManagerSubscriptionInfo": { "message": "Secrets Manager aboneliğiniz seçtiğiniz plana göre yükseltilecektir" @@ -10243,7 +10246,7 @@ "message": "Remove members" }, "devices": { - "message": "Devices" + "message": "Cihazlar" }, "deviceListDescription": { "message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now." @@ -10340,19 +10343,19 @@ "message": "Opening the Bitwarden browser extension" }, "somethingWentWrong": { - "message": "Something went wrong..." + "message": "Bir şeyler ters gitti..." }, "openingExtensionError": { "message": "We had trouble opening the Bitwarden browser extension. Click the button to open it now." }, "openExtension": { - "message": "Open extension" + "message": "Uzantıyı aç" }, "doNotHaveExtension": { "message": "Don't have the Bitwarden browser extension?" }, "installExtension": { - "message": "Install extension" + "message": "Uzantıyı yükle" }, "openedExtension": { "message": "Opened the browser extension" diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 67158c997ad..86e725f32cb 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -6,7 +6,7 @@ "message": "Критичні програми" }, "noCriticalAppsAtRisk": { - "message": "No critical applications at risk" + "message": "Немає критичних програм із ризиком" }, "accessIntelligence": { "message": "Управління доступом" @@ -202,7 +202,7 @@ "message": "Нотатки" }, "privateNote": { - "message": "Private note" + "message": "Приватна нотатка" }, "note": { "message": "Нотатка" @@ -471,16 +471,16 @@ "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": "Зробіть теку вкладеною, вказавши після основної теки \"/\". Наприклад: Обговорення/Форуми" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "Ви дійсно хочете остаточно видалити цю теку?" }, "baseDomain": { "message": "Основний домен", @@ -1032,6 +1032,9 @@ "no": { "message": "Ні" }, + "location": { + "message": "Розташування" + }, "loginOrCreateNewAccount": { "message": "Для доступу до сховища увійдіть в обліковий запис, або створіть новий." }, @@ -1168,13 +1171,13 @@ "message": "Увійти в Bitwarden" }, "enterTheCodeSentToYourEmail": { - "message": "Enter the code sent to your email" + "message": "Введіть код, надісланий вам електронною поштою" }, "enterTheCodeFromYourAuthenticatorApp": { - "message": "Enter the code from your authenticator app" + "message": "Введіть код з програми автентифікації" }, "pressYourYubiKeyToAuthenticate": { - "message": "Press your YubiKey to authenticate" + "message": "Натисніть свій YubiKey для автентифікації" }, "authenticationTimeout": { "message": "Час очікування автентифікації" @@ -1183,7 +1186,7 @@ "message": "Час очікування сеансу автентифікації завершився. Перезапустіть процес входу в систему." }, "verifyYourIdentity": { - "message": "Verify your Identity" + "message": "Підтвердьте свою особу" }, "weDontRecognizeThisDevice": { "message": "Ми не розпізнаємо цей пристрій. Введіть код, надісланий на вашу електронну пошту, щоб підтвердити вашу особу." @@ -1201,7 +1204,7 @@ "message": "Ініційовано вхід" }, "logInRequestSent": { - "message": "Request sent" + "message": "Запит надіслано" }, "submit": { "message": "Відправити" @@ -1393,13 +1396,13 @@ "message": "Сповіщення було надіслано на ваш пристрій." }, "notificationSentDevicePart1": { - "message": "Unlock Bitwarden on your device or on the " + "message": "Розблокуйте Bitwarden на своєму пристрої або у " }, "areYouTryingToAccessYourAccount": { - "message": "Are you trying to access your account?" + "message": "Ви намагаєтесь отримати доступ до свого облікового запису?" }, "accessAttemptBy": { - "message": "Access attempt by $EMAIL$", + "message": "Спроба доступу з $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -1408,16 +1411,16 @@ } }, "confirmAccess": { - "message": "Confirm access" + "message": "Підтвердити доступ" }, "denyAccess": { - "message": "Deny access" + "message": "Заборонити доступ" }, "notificationSentDeviceAnchor": { - "message": "web app" + "message": "вебпрограмі" }, "notificationSentDevicePart2": { - "message": "Make sure the Fingerprint phrase matches the one below before approving." + "message": "Перш ніж підтверджувати, обов'язково перевірте відповідність зазначеної нижче фрази відбитка." }, "notificationSentDeviceComplete": { "message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving." diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index 89847958d90..6310cf5a53e 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "Không" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "Đăng nhập hoặc tạo tài khoản mới để truy cập kho mật khẩu của bạn." }, diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index b310440eda6..c4c9d626304 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "否" }, + "location": { + "message": "位置" + }, "loginOrCreateNewAccount": { "message": "登录或创建一个新账户以访问您的安全密码库。" }, @@ -6174,7 +6177,7 @@ "message": "为所有现有成员和新成员激活浏览器扩展上的页面加载时的自动填充设置。" }, "experimentalFeature": { - "message": "不完整或不信任的网站可以使用页面加载时自动填充。" + "message": "被入侵或不受信任的网站可以利用页面加载时的自动填充功能。" }, "learnMoreAboutAutofill": { "message": "进一步了解自动填充" @@ -10180,7 +10183,7 @@ "message": "删除成功" }, "freeFamiliesSponsorship": { - "message": "移除免费的 Bitwarden 家庭赞助" + "message": "禁用免费 Bitwarden 家庭赞助" }, "freeFamiliesSponsorshipPolicyDesc": { "message": "不允许成员通过此组织兑换家庭计划。" @@ -10479,7 +10482,7 @@ "message": "更改有风险的密码" }, "removeUnlockWithPinPolicyTitle": { - "message": "移除使用 PIN 码解锁" + "message": "禁用 PIN 码解锁" }, "removeUnlockWithPinPolicyDesc": { "message": "不允许成员使用 PIN 码解锁他们的账户。" diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 4de0c9c9f12..89b7b7f20e4 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1032,6 +1032,9 @@ "no": { "message": "否" }, + "location": { + "message": "Location" + }, "loginOrCreateNewAccount": { "message": "登入或建立帳戶以存取您的安全密碼庫。" }, diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 28fe5ce1f35..d172ea95c71 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -81,6 +81,7 @@ const moduleRules = [ loader: "babel-loader", options: { configFile: "../../babel.config.json", + cacheDirectory: NODE_ENV !== "production", }, }, ], @@ -349,6 +350,20 @@ const webpackConfig = { styles: ["./src/scss/styles.scss", "./src/scss/tailwind.css"], theme_head: "./src/theme.ts", }, + cache: + NODE_ENV === "production" + ? false + : { + type: "filesystem", + allowCollectingMemory: true, + cacheDirectory: path.resolve(__dirname, "../../node_modules/.cache/webpack"), + buildDependencies: { + config: [__filename], + }, + }, + snapshot: { + unmanagedPaths: [path.resolve(__dirname, "../../node_modules/@bitwarden/")], + }, optimization: { splitChunks: { cacheGroups: { @@ -361,6 +376,7 @@ const webpackConfig = { }, }, }, + minimize: NODE_ENV === "production", minimizer: [ new TerserPlugin({ terserOptions: { diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html index 7723324781b..cafd0744a8f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.html @@ -1,7 +1,7 @@ diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html index c292d51ebda..adf9fcd2dcf 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.html @@ -24,7 +24,7 @@ @@ -74,7 +74,7 @@ {{ orgDomain.lastCheckedDate | date: "medium" }} - +
  • - + { + const actual = jest.requireActual("@angular/cdk/drag-drop"); + return { + ...actual, + moveItemInArray: jest.fn(actual.moveItemInArray), + }; +}); + describe("AutofillOptionsComponent", () => { let component: AutofillOptionsComponent; let fixture: ComponentFixture; @@ -255,4 +264,111 @@ describe("AutofillOptionsComponent", () => { expect(component.autofillOptionsForm.value.uris.length).toEqual(1); }); + + describe("Drag & Drop Functionality", () => { + beforeEach(() => { + // Prevent auto‑adding an empty URI by setting a non‑null initial value. + // This overrides the call to initNewCipher. + + // Now clear any existing URIs (including the auto‑added one) + component.autofillOptionsForm.controls.uris.clear(); + + // Add exactly three URIs that we want to test reordering on. + component.addUri({ uri: "https://first.com", matchDetection: null }); + component.addUri({ uri: "https://second.com", matchDetection: null }); + component.addUri({ uri: "https://third.com", matchDetection: null }); + fixture.detectChanges(); + }); + + it("should reorder URI inputs on drop event", () => { + // Simulate a drop event that moves the first URI (index 0) to the last position (index 2). + const dropEvent: CdkDragDrop = { + previousIndex: 0, + currentIndex: 2, + container: null, + previousContainer: null, + isPointerOverContainer: true, + item: null, + distance: { x: 0, y: 0 }, + } as any; + + component.onUriItemDrop(dropEvent); + fixture.detectChanges(); + + expect(moveItemInArray).toHaveBeenCalledWith( + component.autofillOptionsForm.controls.uris.controls, + 0, + 2, + ); + }); + + it("should reorder URI input via keyboard ArrowUp", async () => { + // Clear and add exactly two URIs. + component.autofillOptionsForm.controls.uris.clear(); + component.addUri({ uri: "https://first.com", matchDetection: null }); + component.addUri({ uri: "https://second.com", matchDetection: null }); + fixture.detectChanges(); + + // Simulate pressing ArrowUp on the second URI (index 1) + const keyEvent = { + key: "ArrowUp", + preventDefault: jest.fn(), + target: document.createElement("button"), + } as unknown as KeyboardEvent; + + // Force requestAnimationFrame to run synchronously + jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb: FrameRequestCallback) => { + cb(new Date().getTime()); + return 0; + }); + (liveAnnouncer.announce as jest.Mock).mockResolvedValue(null); + + await component.onUriItemKeydown(keyEvent, 1); + fixture.detectChanges(); + + expect(moveItemInArray).toHaveBeenCalledWith( + component.autofillOptionsForm.controls.uris.controls, + 1, + 0, + ); + expect(liveAnnouncer.announce).toHaveBeenCalledWith( + "reorderFieldUp websiteUri 1 2", + "assertive", + ); + }); + + it("should reorder URI input via keyboard ArrowDown", async () => { + // Clear and add exactly three URIs. + component.autofillOptionsForm.controls.uris.clear(); + component.addUri({ uri: "https://first.com", matchDetection: null }); + component.addUri({ uri: "https://second.com", matchDetection: null }); + component.addUri({ uri: "https://third.com", matchDetection: null }); + fixture.detectChanges(); + + // Simulate pressing ArrowDown on the second URI (index 1) + const keyEvent = { + key: "ArrowDown", + preventDefault: jest.fn(), + target: document.createElement("button"), + } as unknown as KeyboardEvent; + + jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb: FrameRequestCallback) => { + cb(new Date().getTime()); + return 0; + }); + (liveAnnouncer.announce as jest.Mock).mockResolvedValue(null); + + await component.onUriItemKeydown(keyEvent, 1); + + expect(moveItemInArray).toHaveBeenCalledWith( + component.autofillOptionsForm.controls.uris.controls, + 1, + 2, + ); + expect(liveAnnouncer.announce).toHaveBeenCalledWith( + "reorderFieldDown websiteUri 3 3", + "assertive", + ); + }); + }); }); diff --git a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts index c3b2a0fb9f9..5b1e4eca103 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/autofill-options.component.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { LiveAnnouncer } from "@angular/cdk/a11y"; +import { CdkDragDrop, DragDropModule, moveItemInArray } from "@angular/cdk/drag-drop"; import { AsyncPipe, NgForOf, NgIf } from "@angular/common"; import { Component, OnInit, QueryList, ViewChildren } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -41,6 +42,7 @@ interface UriField { templateUrl: "./autofill-options.component.html", standalone: true, imports: [ + DragDropModule, SectionComponent, SectionHeaderComponent, TypographyModule, @@ -229,4 +231,58 @@ export class AutofillOptionsComponent implements OnInit { removeUri(i: number) { this.autofillOptionsForm.controls.uris.removeAt(i); } + + /** Create a new list of LoginUriViews from the form objects and update the cipher */ + private updateUriFields() { + this.cipherFormContainer.patchCipher((cipher) => { + cipher.login.uris = this.uriControls.map( + (control) => + Object.assign(new LoginUriView(), { + uri: control.value.uri, + matchDetection: control.value.matchDetection ?? null, + }) as LoginUriView, + ); + return cipher; + }); + } + + /** Reorder the controls to match the new order after a "drop" event */ + onUriItemDrop(event: CdkDragDrop) { + moveItemInArray(this.uriControls, event.previousIndex, event.currentIndex); + this.updateUriFields(); + } + + /** Handles a uri item keyboard up or down event */ + async onUriItemKeydown(event: KeyboardEvent, index: number) { + if (event.key === "ArrowUp" && index !== 0) { + await this.reorderUriItems(event, index, "Up"); + } + + if (event.key === "ArrowDown" && index !== this.uriControls.length - 1) { + await this.reorderUriItems(event, index, "Down"); + } + } + + /** Reorders the uri items from a keyboard up or down event */ + async reorderUriItems(event: KeyboardEvent, previousIndex: number, direction: "Up" | "Down") { + const currentIndex = previousIndex + (direction === "Up" ? -1 : 1); + event.preventDefault(); + await this.liveAnnouncer.announce( + this.i18nService.t( + `reorderField${direction}`, + this.i18nService.t("websiteUri"), + currentIndex + 1, + this.uriControls.length, + ), + "assertive", + ); + moveItemInArray(this.uriControls, previousIndex, currentIndex); + this.updateUriFields(); + // Refocus the button after the reorder + // Angular re-renders the list when moving an item up which causes the focus to be lost + // Wait for the next tick to ensure the button is rendered before focusing + requestAnimationFrame(() => { + (event.target as HTMLButtonElement).focus(); + }); + } } diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html index a55716083de..5301e4f32b9 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.html @@ -1,35 +1,50 @@ - - {{ uriLabel }} - - - - - - - {{ "matchDetection" | i18n }} - - - - +
    +
    + + {{ uriLabel }} + + + + +
    + +
    +
    + + {{ "matchDetection" | i18n }} + + + + +
    diff --git a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts index f712e3114e0..07bf7bef775 100644 --- a/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts +++ b/libs/vault/src/cipher-form/components/autofill-options/uri-option.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { DragDropModule } from "@angular/cdk/drag-drop"; import { NgForOf, NgIf } from "@angular/common"; import { Component, @@ -43,6 +44,7 @@ import { }, ], imports: [ + DragDropModule, FormFieldModule, ReactiveFormsModule, IconButtonModule, @@ -74,6 +76,12 @@ export class UriOptionComponent implements ControlValueAccessor { { label: this.i18nService.t("never"), value: UriMatchStrategy.Never }, ]; + /** + * Whether the option can be reordered. If false, the reorder button will be hidden. + */ + @Input({ required: true }) + canReorder: boolean; + /** * Whether the URI can be removed from the form. If false, the remove button will be hidden. */ @@ -101,6 +109,9 @@ export class UriOptionComponent implements ControlValueAccessor { */ @Input({ required: true }) index: number; + @Output() + onKeydown = new EventEmitter(); + /** * Emits when the remove button is clicked and URI should be removed from the form. */ @@ -132,6 +143,10 @@ export class UriOptionComponent implements ControlValueAccessor { private onChange: any = () => {}; private onTouched: any = () => {}; + protected handleKeydown(event: KeyboardEvent) { + this.onKeydown.emit(event); + } + constructor( private formBuilder: FormBuilder, private i18nService: I18nService, diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index fab3c8f1ab1..c7c5f4a930e 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -87,7 +87,7 @@ (click)="openAddEditCustomFieldDialog({ index: i, label: field.value.name })" [appA11yTitle]="'editFieldLabel' | i18n: field.value.name" bitIconButton="bwi-pencil-square" - class="tw-self-end" + class="tw-self-center tw-mt-2" data-testid="edit-custom-field-button" *ngIf="!isPartialEdit" > @@ -95,7 +95,7 @@ + diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index 773ddd4ad66..500bb886f7a 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -7,7 +7,8 @@ import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ClientType } from "@bitwarden/common/enums"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; @@ -22,6 +23,7 @@ import { } from "@bitwarden/components"; import { generate_ssh_key } from "@bitwarden/sdk-internal"; +import { SshImportPromptService } from "../../../services/ssh-import-prompt.service"; import { CipherFormContainer } from "../../cipher-form-container"; @Component({ @@ -60,11 +62,14 @@ export class SshKeySectionComponent implements OnInit { keyFingerprint: [""], }); + showImport = false; + constructor( private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, - private i18nService: I18nService, private sdkService: SdkService, + private sshImportPromptService: SshImportPromptService, + private platformUtilsService: PlatformUtilsService, ) { this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm); this.sshKeyForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { @@ -87,6 +92,11 @@ export class SshKeySectionComponent implements OnInit { } this.sshKeyForm.disable(); + + // Web does not support clipboard access + if (this.platformUtilsService.getClientType() !== ClientType.Web) { + this.showImport = true; + } } /** Set form initial form values from the current cipher */ @@ -100,6 +110,17 @@ export class SshKeySectionComponent implements OnInit { }); } + async importSshKeyFromClipboard() { + const key = await this.sshImportPromptService.importSshKeyFromClipboard(); + if (key != null) { + this.sshKeyForm.setValue({ + privateKey: key.privateKey, + publicKey: key.publicKey, + keyFingerprint: key.keyFingerprint, + }); + } + } + private async generateSshKey() { await firstValueFrom(this.sdkService.client$); const sshKey = generate_ssh_key("Ed25519"); diff --git a/libs/vault/src/components/dark-image-source.directive.ts b/libs/vault/src/components/dark-image-source.directive.ts index 6f3e03ef914..9867f264365 100644 --- a/libs/vault/src/components/dark-image-source.directive.ts +++ b/libs/vault/src/components/dark-image-source.directive.ts @@ -54,8 +54,7 @@ export class DarkImageSourceDirective implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(([theme, systemTheme]) => { const appliedTheme = theme === "system" ? systemTheme : theme; - const isDark = - appliedTheme === "dark" || appliedTheme === "nord" || appliedTheme === "solarizedDark"; + const isDark = appliedTheme === "dark"; this.src = isDark ? this.darkImgSrc() : this.lightImgSrc; }); } diff --git a/libs/vault/src/icons/browser-extension.ts b/libs/vault/src/icons/browser-extension.ts index f0f9b781491..ac54322292f 100644 --- a/libs/vault/src/icons/browser-extension.ts +++ b/libs/vault/src/icons/browser-extension.ts @@ -1,7 +1,7 @@ import { svgIcon } from "@bitwarden/components"; export const BrowserExtensionIcon = svgIcon` - + diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index e4857411d05..f359b7289ae 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -25,8 +25,11 @@ export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.compon export * from "./components/carousel"; export * as VaultIcons from "./icons"; - export * from "./tasks"; +export * from "./notifications"; + +export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service"; +export { SshImportPromptService } from "./services/ssh-import-prompt.service"; export * from "./abstractions/change-login-password.service"; export * from "./services/default-change-login-password.service"; diff --git a/libs/vault/src/notifications/abstractions/end-user-notification.service.ts b/libs/vault/src/notifications/abstractions/end-user-notification.service.ts new file mode 100644 index 00000000000..2ed7e1de631 --- /dev/null +++ b/libs/vault/src/notifications/abstractions/end-user-notification.service.ts @@ -0,0 +1,49 @@ +import { Observable } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +import { NotificationView } from "../models"; + +/** + * A service for retrieving and managing notifications for end users. + */ +export abstract class EndUserNotificationService { + /** + * Observable of all notifications for the given user. + * @param userId + */ + abstract notifications$(userId: UserId): Observable; + + /** + * Observable of all unread notifications for the given user. + * @param userId + */ + abstract unreadNotifications$(userId: UserId): Observable; + + /** + * Mark a notification as read. + * @param notificationId + * @param userId + */ + abstract markAsRead(notificationId: any, userId: UserId): Promise; + + /** + * Mark a notification as deleted. + * @param notificationId + * @param userId + */ + abstract markAsDeleted(notificationId: any, userId: UserId): Promise; + + /** + * Create/update a notification in the state for the user specified within the notification. + * @remarks This method should only be called when a notification payload is received from the web socket. + * @param notification + */ + abstract upsert(notification: Notification): Promise; + + /** + * Clear all notifications from state for the given user. + * @param userId + */ + abstract clearState(userId: UserId): Promise; +} diff --git a/libs/vault/src/notifications/index.ts b/libs/vault/src/notifications/index.ts new file mode 100644 index 00000000000..0c9d5c0d16b --- /dev/null +++ b/libs/vault/src/notifications/index.ts @@ -0,0 +1,2 @@ +export * from "./abstractions/end-user-notification.service"; +export * from "./services/default-end-user-notification.service"; diff --git a/libs/vault/src/notifications/models/index.ts b/libs/vault/src/notifications/models/index.ts new file mode 100644 index 00000000000..b782335caa9 --- /dev/null +++ b/libs/vault/src/notifications/models/index.ts @@ -0,0 +1,3 @@ +export * from "./notification-view"; +export * from "./notification-view.data"; +export * from "./notification-view.response"; diff --git a/libs/vault/src/notifications/models/notification-view.data.ts b/libs/vault/src/notifications/models/notification-view.data.ts new file mode 100644 index 00000000000..07c147052ad --- /dev/null +++ b/libs/vault/src/notifications/models/notification-view.data.ts @@ -0,0 +1,37 @@ +import { Jsonify } from "type-fest"; + +import { NotificationId } from "@bitwarden/common/types/guid"; + +import { NotificationViewResponse } from "./notification-view.response"; + +export class NotificationViewData { + id: NotificationId; + priority: number; + title: string; + body: string; + date: Date; + readDate: Date | null; + deletedDate: Date | null; + + constructor(response: NotificationViewResponse) { + this.id = response.id; + this.priority = response.priority; + this.title = response.title; + this.body = response.body; + this.date = response.date; + this.readDate = response.readDate; + this.deletedDate = response.deletedDate; + } + + static fromJSON(obj: Jsonify) { + return Object.assign(new NotificationViewData({} as NotificationViewResponse), obj, { + id: obj.id, + priority: obj.priority, + title: obj.title, + body: obj.body, + date: new Date(obj.date), + readDate: obj.readDate ? new Date(obj.readDate) : null, + deletedDate: obj.deletedDate ? new Date(obj.deletedDate) : null, + }); + } +} diff --git a/libs/vault/src/notifications/models/notification-view.response.ts b/libs/vault/src/notifications/models/notification-view.response.ts new file mode 100644 index 00000000000..bbebf25bd4e --- /dev/null +++ b/libs/vault/src/notifications/models/notification-view.response.ts @@ -0,0 +1,23 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { NotificationId } from "@bitwarden/common/types/guid"; + +export class NotificationViewResponse extends BaseResponse { + id: NotificationId; + priority: number; + title: string; + body: string; + date: Date; + readDate: Date; + deletedDate: Date; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.priority = this.getResponseProperty("Priority"); + this.title = this.getResponseProperty("Title"); + this.body = this.getResponseProperty("Body"); + this.date = this.getResponseProperty("Date"); + this.readDate = this.getResponseProperty("ReadDate"); + this.deletedDate = this.getResponseProperty("DeletedDate"); + } +} diff --git a/libs/vault/src/notifications/models/notification-view.ts b/libs/vault/src/notifications/models/notification-view.ts new file mode 100644 index 00000000000..b577a889d05 --- /dev/null +++ b/libs/vault/src/notifications/models/notification-view.ts @@ -0,0 +1,21 @@ +import { NotificationId } from "@bitwarden/common/types/guid"; + +export class NotificationView { + id: NotificationId; + priority: number; + title: string; + body: string; + date: Date; + readDate: Date | null; + deletedDate: Date | null; + + constructor(obj: any) { + this.id = obj.id; + this.priority = obj.priority; + this.title = obj.title; + this.body = obj.body; + this.date = obj.date; + this.readDate = obj.readDate; + this.deletedDate = obj.deletedDate; + } +} diff --git a/libs/vault/src/notifications/services/default-end-user-notification.service.spec.ts b/libs/vault/src/notifications/services/default-end-user-notification.service.spec.ts new file mode 100644 index 00000000000..ac4304998bc --- /dev/null +++ b/libs/vault/src/notifications/services/default-end-user-notification.service.spec.ts @@ -0,0 +1,193 @@ +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { NotificationId, UserId } from "@bitwarden/common/types/guid"; +import { DefaultEndUserNotificationService } from "@bitwarden/vault"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../common/spec"; +import { NotificationViewResponse } from "../models"; +import { NOTIFICATIONS } from "../state/end-user-notification.state"; + +describe("End User Notification Center Service", () => { + let fakeStateProvider: FakeStateProvider; + + const mockApiSend = jest.fn(); + + let testBed: TestBed; + + beforeEach(async () => { + mockApiSend.mockClear(); + + fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); + + testBed = TestBed.configureTestingModule({ + imports: [], + providers: [ + DefaultEndUserNotificationService, + { + provide: StateProvider, + useValue: fakeStateProvider, + }, + { + provide: ApiService, + useValue: { + send: mockApiSend, + }, + }, + ], + }); + }); + + describe("notifications$", () => { + it("should return notifications from state when not null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + } as NotificationViewResponse, + ]); + + const { notifications$ } = testBed.inject(DefaultEndUserNotificationService); + + const result = await firstValueFrom(notifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).not.toHaveBeenCalled(); + }); + + it("should return notifications API when state is null", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, null as any); + + const { notifications$ } = testBed.inject(DefaultEndUserNotificationService); + + const result = await firstValueFrom(notifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).toHaveBeenCalledWith("GET", "/notifications", null, true, true); + }); + + it("should share the same observable for the same user", async () => { + const { notifications$ } = testBed.inject(DefaultEndUserNotificationService); + + const first = notifications$("user-id" as UserId); + const second = notifications$("user-id" as UserId); + + expect(first).toBe(second); + }); + }); + + describe("unreadNotifications$", () => { + it("should return unread notifications from state when read value is null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + readDate: null as any, + } as NotificationViewResponse, + ]); + + const { unreadNotifications$ } = testBed.inject(DefaultEndUserNotificationService); + + const result = await firstValueFrom(unreadNotifications$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).not.toHaveBeenCalled(); + }); + }); + + describe("getNotifications", () => { + it("should call getNotifications returning notifications from API", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + const service = testBed.inject(DefaultEndUserNotificationService); + + await service.getNotifications("user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith("GET", "/notifications", null, true, true); + }); + }); + it("should update local state when notifications are updated", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "notification-id", + }, + ] as NotificationViewResponse[], + }); + + const mock = fakeStateProvider.singleUser.mockFor( + "user-id" as UserId, + NOTIFICATIONS, + null as any, + ); + + const service = testBed.inject(DefaultEndUserNotificationService); + + await service.getNotifications("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([ + expect.objectContaining({ + id: "notification-id" as NotificationId, + } as NotificationViewResponse), + ]); + }); + + describe("clear", () => { + it("should clear the local notification state for the user", async () => { + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, NOTIFICATIONS, [ + { + id: "notification-id" as NotificationId, + } as NotificationViewResponse, + ]); + + const service = testBed.inject(DefaultEndUserNotificationService); + + await service.clearState("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([]); + }); + }); + + describe("markAsDeleted", () => { + it("should send an API request to mark the notification as deleted", async () => { + const service = testBed.inject(DefaultEndUserNotificationService); + + await service.markAsDeleted("notification-id" as NotificationId, "user-id" as UserId); + expect(mockApiSend).toHaveBeenCalledWith( + "DELETE", + "/notifications/notification-id/delete", + null, + true, + false, + ); + }); + }); + + describe("markAsRead", () => { + it("should send an API request to mark the notification as read", async () => { + const service = testBed.inject(DefaultEndUserNotificationService); + + await service.markAsRead("notification-id" as NotificationId, "user-id" as UserId); + expect(mockApiSend).toHaveBeenCalledWith( + "PATCH", + "/notifications/notification-id/read", + null, + true, + false, + ); + }); + }); +}); diff --git a/libs/vault/src/notifications/services/default-end-user-notification.service.ts b/libs/vault/src/notifications/services/default-end-user-notification.service.ts new file mode 100644 index 00000000000..517a968f8af --- /dev/null +++ b/libs/vault/src/notifications/services/default-end-user-notification.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from "@angular/core"; +import { map, Observable, switchMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities"; +import { EndUserNotificationService } from "../abstractions/end-user-notification.service"; +import { NotificationView, NotificationViewData, NotificationViewResponse } from "../models"; +import { NOTIFICATIONS } from "../state/end-user-notification.state"; + +/** + * A service for retrieving and managing notifications for end users. + */ +@Injectable() +export class DefaultEndUserNotificationService implements EndUserNotificationService { + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + ) {} + + notifications$ = perUserCache$((userId: UserId): Observable => { + return this.notificationState(userId).state$.pipe( + switchMap(async (notifications) => { + if (notifications == null) { + await this.fetchNotificationsFromApi(userId); + } + return notifications; + }), + filterOutNullish(), + map((notifications) => + notifications.map((notification) => new NotificationView(notification)), + ), + ); + }); + + unreadNotifications$ = perUserCache$((userId: UserId): Observable => { + return this.notifications$(userId).pipe( + map((notifications) => notifications.filter((notification) => notification.readDate == null)), + ); + }); + + async markAsRead(notificationId: any, userId: UserId): Promise { + await this.apiService.send("PATCH", `/notifications/${notificationId}/read`, null, true, false); + await this.getNotifications(userId); + } + + async markAsDeleted(notificationId: any, userId: UserId): Promise { + await this.apiService.send( + "DELETE", + `/notifications/${notificationId}/delete`, + null, + true, + false, + ); + await this.getNotifications(userId); + } + + upsert(notification: Notification): any {} + + async clearState(userId: UserId): Promise { + await this.updateNotificationState(userId, []); + } + + async getNotifications(userId: UserId) { + await this.fetchNotificationsFromApi(userId); + } + + /** + * Fetches the notifications from the API and updates the local state + * @param userId + * @private + */ + private async fetchNotificationsFromApi(userId: UserId): Promise { + const res = await this.apiService.send("GET", "/notifications", null, true, true); + const response = new ListResponse(res, NotificationViewResponse); + const notificationData = response.data.map((n) => new NotificationView(n)); + await this.updateNotificationState(userId, notificationData); + } + + /** + * Updates the local state with notifications and returns the updated state + * @param userId + * @param notifications + * @private + */ + private updateNotificationState( + userId: UserId, + notifications: NotificationViewData[], + ): Promise { + return this.notificationState(userId).update(() => notifications); + } + + /** + * Returns the local state for notifications + * @param userId + * @private + */ + private notificationState(userId: UserId) { + return this.stateProvider.getUser(userId, NOTIFICATIONS); + } +} diff --git a/libs/vault/src/notifications/state/end-user-notification.state.ts b/libs/vault/src/notifications/state/end-user-notification.state.ts new file mode 100644 index 00000000000..644c8e42429 --- /dev/null +++ b/libs/vault/src/notifications/state/end-user-notification.state.ts @@ -0,0 +1,15 @@ +import { Jsonify } from "type-fest"; + +import { NOTIFICATION_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; + +import { NotificationViewData } from "../models"; + +export const NOTIFICATIONS = UserKeyDefinition.array( + NOTIFICATION_DISK, + "notifications", + { + deserializer: (notification: Jsonify) => + NotificationViewData.fromJSON(notification), + clearOn: ["logout", "lock"], + }, +); diff --git a/libs/vault/src/services/default-ssh-import-prompt.service.ts b/libs/vault/src/services/default-ssh-import-prompt.service.ts new file mode 100644 index 00000000000..c4e51dd3638 --- /dev/null +++ b/libs/vault/src/services/default-ssh-import-prompt.service.ts @@ -0,0 +1,109 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SshKeyApi } from "@bitwarden/common/vault/models/api/ssh-key.api"; +import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { SshKeyPasswordPromptComponent } from "@bitwarden/importer-ui"; +import { import_ssh_key, SshKeyImportError, SshKeyView } from "@bitwarden/sdk-internal"; + +import { SshImportPromptService } from "./ssh-import-prompt.service"; + +/** + * Used to import ssh keys and prompt for their password. + */ +@Injectable() +export class DefaultSshImportPromptService implements SshImportPromptService { + constructor( + private dialogService: DialogService, + private toastService: ToastService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + ) {} + + async importSshKeyFromClipboard(): Promise { + const key = await this.platformUtilsService.readFromClipboard(); + + let isPasswordProtectedSshKey = false; + + let parsedKey: SshKeyView | null = null; + + try { + parsedKey = import_ssh_key(key); + } catch (e) { + const error = e as SshKeyImportError; + if (error.variant === "PasswordRequired" || error.variant === "WrongPassword") { + isPasswordProtectedSshKey = true; + } else { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t(this.sshImportErrorVariantToI18nKey(error.variant)), + }); + return null; + } + } + + if (isPasswordProtectedSshKey) { + for (;;) { + const password = await this.getSshKeyPassword(); + if (password === "" || password == null) { + return null; + } + + try { + parsedKey = import_ssh_key(key, password); + break; + } catch (e) { + const error = e as SshKeyImportError; + if (error.variant !== "WrongPassword") { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t(this.sshImportErrorVariantToI18nKey(error.variant)), + }); + return null; + } + } + } + } + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyImported"), + }); + + return new SshKeyData( + new SshKeyApi({ + privateKey: parsedKey!.privateKey, + publicKey: parsedKey!.publicKey, + keyFingerprint: parsedKey!.fingerprint, + }), + ); + } + + private sshImportErrorVariantToI18nKey(variant: string): string { + switch (variant) { + case "ParsingError": + return "invalidSshKey"; + case "UnsupportedKeyType": + return "sshKeyTypeUnsupported"; + case "PasswordRequired": + case "WrongPassword": + return "sshKeyWrongPassword"; + default: + return "errorOccurred"; + } + } + + private async getSshKeyPassword(): Promise { + const dialog = this.dialogService.open(SshKeyPasswordPromptComponent, { + ariaModal: true, + }); + + return await firstValueFrom(dialog.closed); + } +} diff --git a/libs/vault/src/services/ssh-import-prompt.service.spec.ts b/libs/vault/src/services/ssh-import-prompt.service.spec.ts new file mode 100644 index 00000000000..49b2b898d7a --- /dev/null +++ b/libs/vault/src/services/ssh-import-prompt.service.spec.ts @@ -0,0 +1,111 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SshKeyApi } from "@bitwarden/common/vault/models/api/ssh-key.api"; +import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data"; +import { DialogService, ToastService } from "@bitwarden/components"; +import * as sdkInternal from "@bitwarden/sdk-internal"; + +import { DefaultSshImportPromptService } from "./default-ssh-import-prompt.service"; + +jest.mock("@bitwarden/sdk-internal"); + +const exampleSshKey = { + privateKey: "private_key", + publicKey: "public_key", + fingerprint: "key_fingerprint", +} as sdkInternal.SshKeyView; + +const exampleSshKeyData = new SshKeyData( + new SshKeyApi({ + publicKey: exampleSshKey.publicKey, + privateKey: exampleSshKey.privateKey, + keyFingerprint: exampleSshKey.fingerprint, + }), +); + +describe("SshImportPromptService", () => { + let sshImportPromptService: DefaultSshImportPromptService; + + let dialogService: MockProxy; + let toastService: MockProxy; + let platformUtilsService: MockProxy; + let i18nService: MockProxy; + + beforeEach(() => { + dialogService = mock(); + toastService = mock(); + platformUtilsService = mock(); + i18nService = mock(); + + sshImportPromptService = new DefaultSshImportPromptService( + dialogService, + toastService, + platformUtilsService, + i18nService, + ); + jest.clearAllMocks(); + }); + + describe("importSshKeyFromClipboard()", () => { + it("imports unencrypted ssh key", async () => { + jest.spyOn(sdkInternal, "import_ssh_key").mockReturnValue(exampleSshKey); + platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key"); + expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(exampleSshKeyData); + }); + + it("requests password for encrypted ssh key", async () => { + jest + .spyOn(sdkInternal, "import_ssh_key") + .mockImplementationOnce(() => { + throw { variant: "PasswordRequired" }; + }) + .mockImplementationOnce(() => exampleSshKey); + dialogService.open.mockReturnValue({ closed: new BehaviorSubject("password") } as any); + platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key"); + + expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(exampleSshKeyData); + expect(dialogService.open).toHaveBeenCalled(); + }); + + it("cancels when no password was provided", async () => { + jest.spyOn(sdkInternal, "import_ssh_key").mockImplementationOnce(() => { + throw { variant: "PasswordRequired" }; + }); + dialogService.open.mockReturnValue({ closed: new BehaviorSubject("") } as any); + platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key"); + + expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(null); + expect(dialogService.open).toHaveBeenCalled(); + }); + + it("passes through error on no password", async () => { + jest.spyOn(sdkInternal, "import_ssh_key").mockImplementationOnce(() => { + throw { variant: "UnsupportedKeyType" }; + }); + platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key"); + + expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(null); + expect(i18nService.t).toHaveBeenCalledWith("sshKeyTypeUnsupported"); + }); + + it("passes through error with password", async () => { + jest + .spyOn(sdkInternal, "import_ssh_key") + .mockClear() + .mockImplementationOnce(() => { + throw { variant: "PasswordRequired" }; + }) + .mockImplementationOnce(() => { + throw { variant: "UnsupportedKeyType" }; + }); + platformUtilsService.readFromClipboard.mockResolvedValue("ssh_key"); + dialogService.open.mockReturnValue({ closed: new BehaviorSubject("password") } as any); + + expect(await sshImportPromptService.importSshKeyFromClipboard()).toEqual(null); + expect(i18nService.t).toHaveBeenCalledWith("sshKeyTypeUnsupported"); + }); + }); +}); diff --git a/libs/vault/src/services/ssh-import-prompt.service.ts b/libs/vault/src/services/ssh-import-prompt.service.ts new file mode 100644 index 00000000000..aae5159895b --- /dev/null +++ b/libs/vault/src/services/ssh-import-prompt.service.ts @@ -0,0 +1,5 @@ +import { SshKeyData } from "@bitwarden/common/vault/models/data/ssh-key.data"; + +export abstract class SshImportPromptService { + abstract importSshKeyFromClipboard: () => Promise; +} diff --git a/libs/vault/tsconfig.json b/libs/vault/tsconfig.json index e1515183f22..6039dccd811 100644 --- a/libs/vault/tsconfig.json +++ b/libs/vault/tsconfig.json @@ -8,11 +8,13 @@ "@bitwarden/auth/common": ["../auth/src/common"], "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], + "@bitwarden/importer-ui": ["../importer/src/components"], "@bitwarden/generator-components": ["../tools/generator/components/src"], "@bitwarden/generator-core": ["../tools/generator/core/src"], "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], "@bitwarden/key-management": ["../key-management/src"], "@bitwarden/platform": ["../platform/src"], "@bitwarden/ui-common": ["../ui/common/src"], diff --git a/package-lock.json b/package-lock.json index 142a4e13c21..7cb1d50947a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,6 @@ "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", - "nord": "0.2.1", "oidc-client-ts": "2.4.1", "open": "8.4.2", "papaparse": "5.5.2", @@ -190,11 +189,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.2.2" + "version": "2025.3.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.2.0", + "version": "2025.3.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -230,7 +229,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.2.1", + "version": "2025.3.0", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -244,7 +243,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.2.2" + "version": "2025.3.0" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -28021,12 +28020,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/nord": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/nord/-/nord-0.2.1.tgz", - "integrity": "sha512-/AD7JGJbcp1pB5XwYkJyivqdeXofUP5u2lkif6vLGLc+SsV9OCC0JFNpVwM5pqHuFqbyojRt6xILuidJOwwJDQ==", - "license": "(Apache-2.0 AND CC-BY-SA-4.0)" - }, "node_modules/normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", diff --git a/package.json b/package.json index cb941238fc2..a421d87b5de 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,6 @@ "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", - "nord": "0.2.1", "oidc-client-ts": "2.4.1", "open": "8.4.2", "papaparse": "5.5.2",