diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index 363fe134eee..d2c842301a3 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -15,26 +15,9 @@ defaults: shell: bash jobs: - setup: - name: Setup - runs-on: ubuntu-22.04 - steps: - - name: Checkout repo - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - - - name: Branch check - run: | - if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then - echo "===================================" - echo "[!] Can only increase rollout from the 'rc' or 'hotfix-rc-desktop' branches" - echo "===================================" - exit 1 - fi - rollout: name: Update Rollout Percentage runs-on: ubuntu-22.04 - needs: setup steps: - name: Login to Azure uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 diff --git a/apps/browser/package.json b/apps/browser/package.json index a337378e9df..b986cbd010b 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2022.12.0", + "version": "2022.12.1", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 84d6d09bc6f..d1d2f16b36f 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": "2022.12.0", + "version": "2022.12.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 5f1aadd2566..915096694c9 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": "2022.12.0", + "version": "2022.12.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 3f73b5881ed..e871b2a7874 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -64,6 +64,8 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; +import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { EnvironmentComponent } from "./accounts/environment.component"; import { HintComponent } from "./accounts/hint.component"; @@ -202,6 +204,8 @@ registerLocaleData(localeZhTw, "zh-TW"); CipherRowComponent, VaultItemsComponent, CollectionsComponent, + ColorPasswordPipe, + ColorPasswordCountPipe, CurrentTabComponent, EnvironmentComponent, ExcludedDomainsComponent, diff --git a/apps/browser/src/popup/scss/misc.scss b/apps/browser/src/popup/scss/misc.scss index 0a550915987..07f7b6d5091 100644 --- a/apps/browser/src/popup/scss/misc.scss +++ b/apps/browser/src/popup/scss/misc.scss @@ -193,7 +193,7 @@ p.lead { font-size: 8px; @include themify($themes) { - color: themed("mutedColor") !important; + color: themed("passwordCountText") !important; } } diff --git a/apps/browser/src/popup/scss/variables.scss b/apps/browser/src/popup/scss/variables.scss index 0e99f1058a6..6d2842ebb70 100644 --- a/apps/browser/src/popup/scss/variables.scss +++ b/apps/browser/src/popup/scss/variables.scss @@ -109,6 +109,7 @@ $themes: ( logoSuffix: "dark", passwordNumberColor: #007fde, passwordSpecialColor: #c40800, + passwordCountText: #212529, calloutBorderColor: $border-color-dark, calloutBackgroundColor: $box-background-color, toastTextColor: #ffffff, @@ -170,6 +171,7 @@ $themes: ( logoSuffix: "white", passwordNumberColor: #6f9df1, passwordSpecialColor: #ff8d85, + passwordCountText: #ffffff, calloutBorderColor: #4c525f, calloutBackgroundColor: #3c424e, toastTextColor: #1f242e, @@ -230,6 +232,7 @@ $themes: ( logoSuffix: "white", passwordNumberColor: $nord8, passwordSpecialColor: $nord12, + passwordCountText: $nord5, calloutBorderColor: $nord0, calloutBackgroundColor: $nord2, toastTextColor: #ffffff, @@ -290,6 +293,7 @@ $themes: ( logoSuffix: "white", passwordNumberColor: $solarizedDarkCyan, passwordSpecialColor: $solarizedDarkYellow, + passwordCountText: $solarizedDarkBase2, calloutBorderColor: $solarizedDarkBase03, calloutBackgroundColor: $solarizedDarkBase01, toastTextColor: #ffffff, diff --git a/apps/browser/src/popup/vault/password-history.component.html b/apps/browser/src/popup/vault/password-history.component.html index 73a28bb2520..6286aa1022d 100644 --- a/apps/browser/src/popup/vault/password-history.component.html +++ b/apps/browser/src/popup/vault/password-history.component.html @@ -13,9 +13,10 @@
- - {{ h.password }} - + {{ h.lastUsedDate | date: "medium" }}
diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 29d865e826d..d40c9833f65 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -58,6 +58,9 @@ import localeZhCn from "@angular/common/locales/zh-Hans"; import localeZhTw from "@angular/common/locales/zh-Hant"; import { NgModule } from "@angular/core"; +import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; +import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; + import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component"; import { DeleteAccountComponent } from "./accounts/delete-account.component"; import { EnvironmentComponent } from "./accounts/environment.component"; @@ -170,6 +173,8 @@ registerLocaleData(localeZhTw, "zh-TW"); AttachmentsComponent, VaultItemsComponent, CollectionsComponent, + ColorPasswordPipe, + ColorPasswordCountPipe, DeleteAccountComponent, EnvironmentComponent, ExportComponent, diff --git a/apps/desktop/src/app/vault/password-generator-history.component.html b/apps/desktop/src/app/vault/password-generator-history.component.html index 6a8dfd909ef..7799dcaade6 100644 --- a/apps/desktop/src/app/vault/password-generator-history.component.html +++ b/apps/desktop/src/app/vault/password-generator-history.component.html @@ -10,7 +10,7 @@
diff --git a/apps/desktop/src/app/vault/password-history.component.html b/apps/desktop/src/app/vault/password-history.component.html index 49493273c16..362061b250d 100644 --- a/apps/desktop/src/app/vault/password-history.component.html +++ b/apps/desktop/src/app/vault/password-history.component.html @@ -9,9 +9,7 @@
- - {{ h.password }} - + {{ h.lastUsedDate | date: "medium" }}
diff --git a/apps/desktop/src/app/vault/view-custom-fields.component.html b/apps/desktop/src/app/vault/view-custom-fields.component.html index 5f2f110fbaa..492c7fbb23e 100644 --- a/apps/desktop/src/app/vault/view-custom-fields.component.html +++ b/apps/desktop/src/app/vault/view-custom-fields.component.html @@ -17,12 +17,16 @@ {{ field.value || " " }}
+ {{ field.maskedValue }} - {{ field.maskedValue }} +
@@ -41,7 +45,18 @@ {{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}
-
+
+
diff --git a/apps/web/src/app/organizations/members/people.component.ts b/apps/web/src/app/organizations/members/people.component.ts index db618cefc13..7c23347eee6 100644 --- a/apps/web/src/app/organizations/members/people.component.ts +++ b/apps/web/src/app/organizations/members/people.component.ts @@ -310,7 +310,7 @@ export class PeopleComponent if ( !user && this.organization.planProductType === ProductType.Free && - this.users.length === this.organization.seats + this.allUsers.length === this.organization.seats ) { // Show org upgrade modal diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index 37c842fddc4..0f3e87fd31d 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -25,6 +25,7 @@ import { TableModule, TabsModule, ToggleGroupModule, + ColorPasswordModule, } from "@bitwarden/components"; // Register the locales for the application @@ -66,6 +67,8 @@ import "./locales"; TableModule, TabsModule, ToggleGroupModule, + LinkModule, + ColorPasswordModule, // Web specific ], @@ -97,6 +100,8 @@ import "./locales"; TableModule, TabsModule, ToggleGroupModule, + LinkModule, + ColorPasswordModule, // Web specific ], diff --git a/apps/web/src/app/tools/generator.component.html b/apps/web/src/app/tools/generator.component.html index 73d032f0761..c4cc11efdf2 100644 --- a/apps/web/src/app/tools/generator.component.html +++ b/apps/web/src/app/tools/generator.component.html @@ -6,18 +6,10 @@
-
-
+ >
diff --git a/apps/web/src/app/tools/import-export/export.component.html b/apps/web/src/app/tools/import-export/export.component.html index 909131b6c76..01cd7f10ad8 100644 --- a/apps/web/src/app/tools/import-export/export.component.html +++ b/apps/web/src/app/tools/import-export/export.component.html @@ -84,73 +84,41 @@
-
- - {{ "filePassword" | i18n }} - - -
- -
-
-
- {{ "exportPasswordDescription" | i18n }} -
-
-
- - {{ "confirmFilePassword" | i18n }} - -
- -
-
-
+ + {{ "filePassword" | i18n }} + + + {{ "exportPasswordDescription" | i18n }} + + + {{ "confirmFilePassword" | i18n }} + + +
diff --git a/apps/web/src/app/tools/import-export/export.component.ts b/apps/web/src/app/tools/import-export/export.component.ts index 6087d7a2027..135b3ace14b 100644 --- a/apps/web/src/app/tools/import-export/export.component.ts +++ b/apps/web/src/app/tools/import-export/export.component.ts @@ -23,6 +23,7 @@ import { UserVerificationPromptComponent } from "../../components/user-verificat export class ExportComponent extends BaseExportComponent { organizationId: string; encryptedExportType = EncryptedExportType; + protected showFilePassword: boolean; constructor( cryptoService: CryptoService, diff --git a/apps/web/src/app/tools/import-export/file-password-prompt.component.html b/apps/web/src/app/tools/import-export/file-password-prompt.component.html index 7250cc9e833..a42aae99a8b 100644 --- a/apps/web/src/app/tools/import-export/file-password-prompt.component.html +++ b/apps/web/src/app/tools/import-export/file-password-prompt.component.html @@ -14,32 +14,17 @@ class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-pr-3.5 tw-pt-3.5 tw-pl-3.5" > {{ "confirmVaultImportDesc" | i18n }} - + {{ "confirmFilePassword" | i18n }} - +
+ + Log in to "https://vault.passky.org" → "Import & Export" → "Export" in the Passky + section. ("Backup" is unsupported as it is encrypted). + +
+ +
+ + +
+
@@ -870,7 +891,7 @@
{{ ph.lastUsedDate | date: "short" }} - - {{ ph.password }} +
diff --git a/apps/web/src/app/vault/add-edit.component.ts b/apps/web/src/app/vault/add-edit.component.ts index a8372871f9d..c17009fab9e 100644 --- a/apps/web/src/app/vault/add-edit.component.ts +++ b/apps/web/src/app/vault/add-edit.component.ts @@ -35,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On hasPasswordHistory = false; viewingPasswordHistory = false; viewOnly = false; + showPasswordCount = false; protected totpInterval: number; protected override componentName = "app-vault-add-edit"; @@ -104,6 +105,26 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On this.cipher.favorite = !this.cipher.favorite; } + togglePassword() { + super.togglePassword(); + + // Hide password count when password is hidden to be safe + if (!this.showPassword && this.showPasswordCount) { + this.togglePasswordCount(); + } + } + + togglePasswordCount() { + this.showPasswordCount = !this.showPasswordCount; + + if (this.editMode && this.showPasswordCount) { + this.eventCollectionService.collect( + EventType.Cipher_ClientToggledPasswordVisible, + this.cipherId + ); + } + } + launch(uri: LoginUriView) { if (!uri.canLaunch) { return; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 28ef8254706..0994f32578d 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5579,6 +5579,17 @@ "multiSelectClearAll": { "message": "Clear all" }, + "toggleCharacterCount": { + "message": "Toggle character count", + "description": "'Character count' describes a feature that displays a number next to each character of the password." + }, + "passwordCharacterCount": { + "message": "Password character count", + "description": "'Character count' describes a feature that displays a number next to each character of the password." + }, + "hide": { + "message": "Hide" + }, "projects":{ "message": "Projects" }, diff --git a/apps/web/src/scss/pages.scss b/apps/web/src/scss/pages.scss index 9a7a68f0822..7b56a1f9b5b 100644 --- a/apps/web/src/scss/pages.scss +++ b/apps/web/src/scss/pages.scss @@ -1,31 +1,3 @@ -.generated-wrapper { - min-width: 0; - white-space: pre-wrap; - word-break: break-all; -} - -.password-row { - min-width: 0; -} - -.password-letter { - @include themify($themes) { - color: themed("pwLetter"); - } -} - -.password-number { - @include themify($themes) { - color: themed("pwNumber"); - } -} - -.password-special { - @include themify($themes) { - color: themed("pwSpecial"); - } -} - app-generator { #lengthRange { width: 100%; diff --git a/apps/web/src/scss/variables.scss b/apps/web/src/scss/variables.scss index 01bb9b80cc4..7bb37c5e359 100644 --- a/apps/web/src/scss/variables.scss +++ b/apps/web/src/scss/variables.scss @@ -201,9 +201,6 @@ $themes: ( navBackgroundAlt: $secondary-alt, navOrgBackgroundColor: #fbfbfb, navWeight: 600, - pwLetter: $body-color, - pwNumber: #007fde, - pwSpecial: #c40800, pwStrengthBackground: #e9ecef, separator: $secondary, separatorHr: rgb(0, 0, 0, 0.1), @@ -313,9 +310,6 @@ $themes: ( navBackgroundAlt: $darkDarkBlue1, navOrgBackgroundColor: #161c26, navWeight: 400, - pwLetter: $white, - pwNumber: #52bdfb, - pwSpecial: #ff7c70, pwStrengthBackground: $darkBlue2, separator: $darkBlue1, separatorHr: $darkBlue1, diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html index 776af86c27d..e31f6df3ac7 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html +++ b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html @@ -42,12 +42,10 @@ + > @@ -59,40 +57,33 @@ id="clientSecret" /> - - - + + + + + > + > {{ "scimApiKeyHelperText" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.html b/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.html index 01c435b722b..98b4bec957e 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.html +++ b/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.html @@ -151,28 +151,24 @@ {{ "callbackPath" | i18n }} + > {{ "signedOutCallbackPath" | i18n }} + > @@ -292,14 +288,12 @@ {{ "spEntityId" | i18n }} + > @@ -315,28 +309,24 @@ + > {{ "spAcsUrl" | i18n }} + > diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html index c68b751eb83..62bb0017b29 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html @@ -2,7 +2,7 @@ - + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index 40bfb717267..86ef7ce3b04 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -1,5 +1,7 @@ import { Component } from "@angular/core"; +import { Organization } from "@bitwarden/common/models/domain/organization"; + import { SecretsManagerLogo } from "./secrets-manager-logo"; @Component({ @@ -8,4 +10,6 @@ import { SecretsManagerLogo } from "./secrets-manager-logo"; }) export class NavigationComponent { protected readonly logo = SecretsManagerLogo; + + protected orgFilter = (org: Organization) => org.canAccessSecretsManager; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/org-switcher.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/org-switcher.component.ts index 0da0ac81c7d..e933e91485b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/org-switcher.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/org-switcher.component.ts @@ -12,13 +12,22 @@ import type { Organization } from "@bitwarden/common/models/domain/organization" export class OrgSwitcherComponent { protected organizations$: Observable = this.organizationService.organizations$.pipe( - map((orgs) => orgs.sort((a, b) => a.name.localeCompare(b.name))) + map((orgs) => orgs.filter(this.filter).sort((a, b) => a.name.localeCompare(b.name))) ); protected activeOrganization$: Observable = combineLatest([ this.route.paramMap, - this.organizationService.organizations$, + this.organizations$, ]).pipe(map(([params, orgs]) => orgs.find((org) => org.id === params.get("organizationId")))); + /** + * Filter function for displayed organizations in the `org-switcher` + * @example + * const smFilter = (org: Organization) => org.canAccessSecretsManager + * // + */ + @Input() + filter: (org: Organization) => boolean = () => true; + /** * Is `true` if the expanded content is visible */ diff --git a/libs/angular/src/components/export.component.ts b/libs/angular/src/components/export.component.ts index b38b1762714..34f196d57ce 100644 --- a/libs/angular/src/components/export.component.ts +++ b/libs/angular/src/components/export.component.ts @@ -21,8 +21,6 @@ export class ExportComponent implements OnInit, OnDestroy { formPromise: Promise; disabledByPolicy = false; - showFilePassword: boolean; - showConfirmFilePassword: boolean; exportForm = this.formBuilder.group({ format: ["json"], @@ -199,16 +197,6 @@ export class ExportComponent implements OnInit, OnDestroy { return this.exportForm.get("fileEncryptionType").value; } - toggleFilePassword() { - this.showFilePassword = !this.showFilePassword; - document.getElementById("filePassword").focus(); - } - - toggleConfirmFilePassword() { - this.showConfirmFilePassword = !this.showConfirmFilePassword; - document.getElementById("confirmFilePassword").focus(); - } - adjustValidators() { this.exportForm.get("confirmFilePassword").reset(); this.exportForm.get("filePassword").reset(); diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 92218e0cc33..42dbc76cb65 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -21,8 +21,6 @@ import { SelectCopyDirective } from "./directives/select-copy.directive"; import { StopClickDirective } from "./directives/stop-click.directive"; import { StopPropDirective } from "./directives/stop-prop.directive"; import { TrueFalseValueDirective } from "./directives/true-false-value.directive"; -import { ColorPasswordCountPipe } from "./pipes/color-password-count.pipe"; -import { ColorPasswordPipe } from "./pipes/color-password.pipe"; import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe"; import { EllipsisPipe } from "./pipes/ellipsis.pipe"; import { I18nPipe } from "./pipes/i18n.pipe"; @@ -50,8 +48,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength AutofocusDirective, BoxRowDirective, CalloutComponent, - ColorPasswordCountPipe, - ColorPasswordPipe, CreditCardNumberPipe, EllipsisPipe, ExportScopeCalloutComponent, @@ -81,8 +77,6 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength BitwardenToastModule, BoxRowDirective, CalloutComponent, - ColorPasswordCountPipe, - ColorPasswordPipe, CreditCardNumberPipe, EllipsisPipe, ExportScopeCalloutComponent, diff --git a/libs/common/spec/importers/passky-json-importer.spec.ts b/libs/common/spec/importers/passky-json-importer.spec.ts new file mode 100644 index 00000000000..a156046abf8 --- /dev/null +++ b/libs/common/spec/importers/passky-json-importer.spec.ts @@ -0,0 +1,34 @@ +import { PasskyJsonImporter as Importer } from "@bitwarden/common/importers/passky/passky-json-importer"; + +import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json"; +import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json"; + +describe("Passky Json Importer", () => { + let importer: Importer; + beforeEach(() => { + importer = new Importer(); + }); + + it("should not import encrypted backups", async () => { + const testDataJson = JSON.stringify(EncryptedData); + const result = await importer.parse(testDataJson); + expect(result != null).toBe(true); + expect(result.success).toBe(false); + expect(result.errorMessage).toBe("Unable to import an encrypted passky backup."); + }); + + it("should parse login data", async () => { + const testDataJson = JSON.stringify(UnencryptedData); + const result = await importer.parse(testDataJson); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("https://bitwarden.com/"); + expect(cipher.login.username).toEqual("testUser"); + expect(cipher.login.password).toEqual("testPassword"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://bitwarden.com/"); + expect(cipher.notes).toEqual("my notes"); + }); +}); diff --git a/libs/common/spec/importers/test-data/passky-json/passky-encrypted.json.ts b/libs/common/spec/importers/test-data/passky-json/passky-encrypted.json.ts new file mode 100644 index 00000000000..2d2ee3debd9 --- /dev/null +++ b/libs/common/spec/importers/test-data/passky-json/passky-encrypted.json.ts @@ -0,0 +1,15 @@ +import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types"; + +export const testData: PasskyJsonExport = { + encrypted: true, + passwords: [ + { + website: + "w68uw6nCjUI3w7MNYsK7w6xqwqHDlXLCpsOEw4/Dq8KbIMK3w6fCvQJFFcOECsOlwprCqUAawqnDvsKbwrLCsCXCtcOlw4dp", + username: "bMKyUC0VPTx5woHCr8K9wpvDgGrClFAKw6VfJTgob8KVwqNoN8KIEA==", + password: "XcKxO2FjwqIJPkoHwqrDvcKtXcORw6TDlMOlw7TDvMORfmlNdMKOwq7DocO+", + message: + "w5jCrWTCgAV1RcO+DsOzw5zCvD5CwqLCtcKtw6sPwpbCmcOxwrfDlcOQw4h1wqomEhNtUkRgwrzCkxrClFBSHsO5wrfCrg==", + }, + ], +}; diff --git a/libs/common/spec/importers/test-data/passky-json/passky-unencrypted.json.ts b/libs/common/spec/importers/test-data/passky-json/passky-unencrypted.json.ts new file mode 100644 index 00000000000..f77bb09e11d --- /dev/null +++ b/libs/common/spec/importers/test-data/passky-json/passky-unencrypted.json.ts @@ -0,0 +1,13 @@ +import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types"; + +export const testData: PasskyJsonExport = { + encrypted: false, + passwords: [ + { + website: "https://bitwarden.com/", + username: "testUser", + password: "testPassword", + message: "my notes", + }, + ], +}; diff --git a/libs/common/src/enums/importOptions.ts b/libs/common/src/enums/importOptions.ts index 6535b133636..e6657a92d05 100644 --- a/libs/common/src/enums/importOptions.ts +++ b/libs/common/src/enums/importOptions.ts @@ -67,6 +67,7 @@ export const regularImportOptions = [ { id: "encryptrcsv", name: "Encryptr (csv)" }, { id: "yoticsv", name: "Yoti (csv)" }, { id: "nordpasscsv", name: "Nordpass (csv)" }, + { id: "passkyjson", name: "Passky (json)" }, ] as const; export type ImportType = diff --git a/libs/common/src/importers/passky/passky-json-importer.ts b/libs/common/src/importers/passky/passky-json-importer.ts new file mode 100644 index 00000000000..01a7f0d1cd5 --- /dev/null +++ b/libs/common/src/importers/passky/passky-json-importer.ts @@ -0,0 +1,43 @@ +import { ImportResult } from "../../models/domain/import-result"; +import { BaseImporter } from "../base-importer"; +import { Importer } from "../importer"; + +import { PasskyJsonExport } from "./passky-json-types"; + +export class PasskyJsonImporter extends BaseImporter implements Importer { + parse(data: string): Promise { + const result = new ImportResult(); + const passkyExport: PasskyJsonExport = JSON.parse(data); + if ( + passkyExport == null || + passkyExport.passwords == null || + passkyExport.passwords.length === 0 + ) { + result.success = false; + return Promise.resolve(result); + } + + if (passkyExport.encrypted == true) { + result.success = false; + result.errorMessage = "Unable to import an encrypted passky backup."; + return Promise.resolve(result); + } + + passkyExport.passwords.forEach((record) => { + const cipher = this.initLoginCipher(); + cipher.name = record.website; + cipher.login.username = record.username; + cipher.login.password = record.password; + + cipher.login.uris = this.makeUriArray(record.website); + cipher.notes = record.message; + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + result.success = true; + return Promise.resolve(result); + } +} diff --git a/libs/common/src/importers/passky/passky-json-types.ts b/libs/common/src/importers/passky/passky-json-types.ts new file mode 100644 index 00000000000..fb9bf11e515 --- /dev/null +++ b/libs/common/src/importers/passky/passky-json-types.ts @@ -0,0 +1,11 @@ +export interface PasskyJsonExport { + encrypted: boolean; + passwords: LoginEntry[]; +} + +export interface LoginEntry { + website: string; + username: string; + password: string; + message: string; +} diff --git a/libs/common/src/services/import.service.ts b/libs/common/src/services/import.service.ts index 202a899794b..7543ec2bfdd 100644 --- a/libs/common/src/services/import.service.ts +++ b/libs/common/src/services/import.service.ts @@ -51,6 +51,7 @@ import { OnePasswordMacCsvImporter } from "../importers/onepassword/onepassword- import { OnePasswordWinCsvImporter } from "../importers/onepassword/onepassword-win-csv-importer"; import { PadlockCsvImporter } from "../importers/padlock-csv-importer"; import { PassKeepCsvImporter } from "../importers/passkeep-csv-importer"; +import { PasskyJsonImporter } from "../importers/passky/passky-json-importer"; import { PassmanJsonImporter } from "../importers/passman-json-importer"; import { PasspackCsvImporter } from "../importers/passpack-csv-importer"; import { PasswordAgentCsvImporter } from "../importers/passwordagent-csv-importer"; @@ -279,6 +280,8 @@ export class ImportService implements ImportServiceAbstraction { return new YotiCsvImporter(); case "nordpasscsv": return new NordPassCsvImporter(); + case "passkyjson": + return new PasskyJsonImporter(); default: return null; } diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index b492032df12..ef789dc106a 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -27,6 +27,7 @@ const template = ` Email + @@ -47,6 +48,12 @@ class PromiseExampleComponent { constructor(private formBuilder: FormBuilder) {} + refresh = async () => { + await new Promise((resolve, reject) => { + setTimeout(resolve, 2000); + }); + }; + submit = async () => { this.formObj.markAllAsTouched(); @@ -78,6 +85,10 @@ class ObservableExampleComponent { constructor(private formBuilder: FormBuilder) {} + refresh = () => { + return of("fake observable").pipe(delay(2000)); + }; + submit = () => { this.formObj.markAllAsTouched(); diff --git a/libs/components/src/button/button.component.html b/libs/components/src/button/button.component.html index 836eb0f9655..ee4d150dfcc 100644 --- a/libs/components/src/button/button.component.html +++ b/libs/components/src/button/button.component.html @@ -1,6 +1,5 @@ - { expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); + testAppComponent.buttonType = "unstyled"; + fixture.detectChanges(); + expect( + Array.from(buttonDebugElement.nativeElement.classList).some((klass: string) => + klass.startsWith("tw-bg") + ) + ).toBe(false); + expect( + Array.from(linkDebugElement.nativeElement.classList).some((klass: string) => + klass.startsWith("tw-bg") + ) + ).toBe(false); + testAppComponent.buttonType = null; fixture.detectChanges(); expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true); diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index aeec8dfa688..0f3589ebf74 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -1,10 +1,15 @@ import { Input, HostBinding, Component } from "@angular/core"; -import { ButtonLikeAbstraction } from "../shared/button-like.abstraction"; +import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; -export type ButtonTypes = "primary" | "secondary" | "danger"; +const focusRing = [ + "focus-visible:tw-ring", + "focus-visible:tw-ring-offset-2", + "focus-visible:tw-ring-primary-700", + "focus-visible:tw-z-10", +]; -const buttonStyles: Record = { +const buttonStyles: Record = { primary: [ "tw-border-primary-500", "tw-bg-primary-500", @@ -15,6 +20,7 @@ const buttonStyles: Record = { "disabled:tw-border-primary-500/60", "disabled:!tw-text-contrast/60", "disabled:tw-bg-clip-padding", + ...focusRing, ], secondary: [ "tw-bg-transparent", @@ -26,6 +32,7 @@ const buttonStyles: Record = { "disabled:tw-bg-transparent", "disabled:tw-border-text-muted/60", "disabled:!tw-text-muted/60", + ...focusRing, ], danger: [ "tw-bg-transparent", @@ -37,7 +44,9 @@ const buttonStyles: Record = { "disabled:tw-bg-transparent", "disabled:tw-border-danger-500/60", "disabled:!tw-text-danger/60", + ...focusRing, ], + unstyled: [], }; @Component({ @@ -58,10 +67,6 @@ export class ButtonComponent implements ButtonLikeAbstraction { "tw-text-center", "hover:tw-no-underline", "focus:tw-outline-none", - "focus-visible:tw-ring", - "focus-visible:tw-ring-offset-2", - "focus-visible:tw-ring-primary-700", - "focus-visible:tw-z-10", ] .concat( this.block == null || this.block === false ? ["tw-inline-block"] : ["tw-w-full", "tw-block"] @@ -75,17 +80,14 @@ export class ButtonComponent implements ButtonLikeAbstraction { return disabled || this.loading ? true : null; } - @Input() buttonType: ButtonTypes = null; - + @Input() buttonType: ButtonType; @Input() block?: boolean; @Input() loading = false; @Input() disabled = false; - @Input("bitIconButton") icon: string; - - get iconClass() { - return [this.icon, "!tw-m-0"]; + setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") { + this.buttonType = value; } } diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 881824c6ae5..6619d7a3a3f 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -101,17 +101,3 @@ export const Block = BlockTemplate.bind({}); Block.args = { block: true, }; - -const IconTemplate: Story = (args) => ({ - props: args, - template: ` - - - - `, -}); - -export const Icon = IconTemplate.bind({}); -Icon.args = { - icon: "bwi-eye", -}; diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index b55384dea1f..8ebb29a31e8 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -48,12 +48,12 @@ export class ColorPasswordComponent { if (this.showCount) { return charClass.concat([ - "tw-inline-flex", "tw-flex-col", "tw-items-center", "tw-w-7", "tw-py-1", "odd:tw-bg-secondary-100", + "even:tw-bg-background", ]); } diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index 329f71c0ff5..c376d36a340 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -11,8 +11,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { AsyncActionsModule } from "../async-actions"; import { ButtonModule } from "../button"; import { CheckboxModule } from "../checkbox"; +import { IconButtonModule } from "../icon-button"; import { InputModule } from "../input/input.module"; import { RadioButtonModule } from "../radio-button"; import { I18nMockService } from "../utils/i18n-mock.service"; @@ -31,6 +33,8 @@ export default { FormFieldModule, InputModule, ButtonModule, + IconButtonModule, + AsyncActionsModule, CheckboxModule, RadioButtonModule, ], @@ -177,10 +181,13 @@ const ButtonGroupTemplate: Story = (args: BitFormFieldCom props: args, template: ` - Label - - - + + + + + `, }); @@ -195,9 +202,13 @@ const DisabledButtonInputGroupTemplate: Story = ( template: ` Label + - - + + + `, }); diff --git a/libs/components/src/form-field/password-input-toggle.directive.ts b/libs/components/src/form-field/password-input-toggle.directive.ts index 6189de636ea..3a3e3f116f6 100644 --- a/libs/components/src/form-field/password-input-toggle.directive.ts +++ b/libs/components/src/form-field/password-input-toggle.directive.ts @@ -3,13 +3,16 @@ import { Directive, EventEmitter, Host, + HostBinding, HostListener, Input, OnChanges, Output, } from "@angular/core"; -import { ButtonComponent } from "../button"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + +import { BitIconButtonComponent } from "../icon-button/icon-button.component"; import { BitFormFieldComponent } from "./form-field.component"; @@ -17,9 +20,18 @@ import { BitFormFieldComponent } from "./form-field.component"; selector: "[bitPasswordInputToggle]", }) export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges { - @Input() toggled = false; + /** + * Whether the input is toggled to show the password. + */ + @HostBinding("attr.aria-pressed") @Input() toggled = false; @Output() toggledChange = new EventEmitter(); + @HostBinding("attr.title") title = this.i18nService.t("toggleVisibility"); + @HostBinding("attr.aria-label") label = this.i18nService.t("toggleVisibility"); + + /** + * Click handler to toggle the state of the input type. + */ @HostListener("click") onClick() { this.toggled = !this.toggled; this.toggledChange.emit(this.toggled); @@ -29,7 +41,11 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan this.formField.input?.focus(); } - constructor(@Host() private button: ButtonComponent, private formField: BitFormFieldComponent) {} + constructor( + @Host() private button: BitIconButtonComponent, + private formField: BitFormFieldComponent, + private i18nService: I18nService + ) {} get icon() { return this.toggled ? "bwi-eye-slash" : "bwi-eye"; diff --git a/libs/components/src/form-field/password-input-toggle.spec.ts b/libs/components/src/form-field/password-input-toggle.spec.ts index 5c6a9d48d00..77001281b38 100644 --- a/libs/components/src/form-field/password-input-toggle.spec.ts +++ b/libs/components/src/form-field/password-input-toggle.spec.ts @@ -2,8 +2,12 @@ import { Component, DebugElement } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; -import { ButtonComponent, ButtonModule } from "../button"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + +import { IconButtonModule } from "../icon-button"; +import { BitIconButtonComponent } from "../icon-button/icon-button.component"; import { InputModule } from "../input/input.module"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { BitFormFieldControl } from "./form-field-control"; import { BitFormFieldComponent } from "./form-field.component"; @@ -17,7 +21,7 @@ import { BitPasswordInputToggleDirective } from "./password-input-toggle.directi Password - + `, @@ -26,21 +30,22 @@ class TestFormFieldComponent {} describe("PasswordInputToggle", () => { let fixture: ComponentFixture; - let button: ButtonComponent; + let button: BitIconButtonComponent; let input: BitFormFieldControl; let toggle: DebugElement; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FormFieldModule, ButtonModule, InputModule], + imports: [FormFieldModule, IconButtonModule, InputModule], declarations: [TestFormFieldComponent], + providers: [{ provide: I18nService, useValue: new I18nMockService({}) }], }).compileComponents(); fixture = TestBed.createComponent(TestFormFieldComponent); fixture.detectChanges(); toggle = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); - const buttonEl = fixture.debugElement.query(By.directive(ButtonComponent)); + const buttonEl = fixture.debugElement.query(By.directive(BitIconButtonComponent)); button = buttonEl.componentInstance; const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent)); const formField: BitFormFieldComponent = formFieldEl.componentInstance; diff --git a/libs/components/src/form-field/password-input-toggle.stories.ts b/libs/components/src/form-field/password-input-toggle.stories.ts index ff6e14c0c91..f39974615bb 100644 --- a/libs/components/src/form-field/password-input-toggle.stories.ts +++ b/libs/components/src/form-field/password-input-toggle.stories.ts @@ -1,8 +1,11 @@ import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { Meta, moduleMetadata, Story } from "@storybook/angular"; -import { ButtonModule } from "../button"; +import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; + +import { IconButtonModule } from "../icon-button"; import { InputModule } from "../input/input.module"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { FormFieldModule } from "./form-field.module"; import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive"; @@ -12,7 +15,13 @@ export default { component: BitPasswordInputToggleDirective, decorators: [ moduleMetadata({ - imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule], + imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, IconButtonModule], + providers: [ + { + provide: I18nService, + useValue: new I18nMockService({ toggleVisibility: "Toggle visibility" }), + }, + ], }), ], parameters: { @@ -40,7 +49,7 @@ const Template: Story = ( Password - + `, @@ -60,7 +69,7 @@ const TemplateBinding: Story = ( Password - +