From 2e6d98938a6d12fcee2d238260d7d17624b66b5e Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:57:54 +0100 Subject: [PATCH 01/53] [PM-13868]Remove Upgrade password manager flag (#11789) * Remove the feature flag * Add the feature flag reference --- .../members/members.component.ts | 35 ++++++------------- ...ganization-subscription-cloud.component.ts | 33 ++++++----------- libs/common/src/enums/feature-flag.enum.ts | 2 -- 3 files changed, 22 insertions(+), 48 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index f1cd505de0a..30c5106a4fa 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -98,10 +98,6 @@ export class MembersComponent extends BaseMembersComponent protected canUseSecretsManager$: Observable; - protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableUpgradePasswordManagerSub, - ); - protected accountDeprovisioningEnabled$: Observable = this.configService.getFeatureFlag$( FeatureFlag.AccountDeprovisioning, ); @@ -487,29 +483,20 @@ export class MembersComponent extends BaseMembersComponent this.organization.productTierType === ProductTierType.TeamsStarter || this.organization.productTierType === ProductTierType.Families) ) { - const enableUpgradePasswordManagerSub = await firstValueFrom( - this.enableUpgradePasswordManagerSub$, - ); - if (enableUpgradePasswordManagerSub && this.organization.canEditSubscription) { - const reference = openChangePlanDialog(this.dialogService, { - data: { - organizationId: this.organization.id, - subscription: null, - productTierType: this.organization.productTierType, - }, - }); + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: this.organization.id, + subscription: null, + productTierType: this.organization.productTierType, + }, + }); - const result = await lastValueFrom(reference.closed); + const result = await lastValueFrom(reference.closed); - if (result === ChangePlanDialogResultType.Submitted) { - await this.load(); - } - return; - } else { - // Show org upgrade modal - await this.showSeatLimitReachedDialog(); - return; + if (result === ChangePlanDialogResultType.Submitted) { + await this.load(); } + return; } const dialog = openUserAddEditDialog(this.dialogService, { diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index f5cc89c86b6..61c13a26e0e 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -60,10 +60,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon; protected readonly teamsStarter = ProductTierType.TeamsStarter; - protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableUpgradePasswordManagerSub, - ); - protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( FeatureFlag.AC2476_DeprecateStripeSourcesAPI, ); @@ -360,27 +356,20 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy }; async changePlan() { - const EnableUpgradePasswordManagerSub = await firstValueFrom( - this.enableUpgradePasswordManagerSub$, - ); - if (EnableUpgradePasswordManagerSub) { - const reference = openChangePlanDialog(this.dialogService, { - data: { - organizationId: this.organizationId, - subscription: this.sub, - productTierType: this.userOrg.productTierType, - }, - }); + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: this.organizationId, + subscription: this.sub, + productTierType: this.userOrg.productTierType, + }, + }); - const result = await lastValueFrom(reference.closed); + const result = await lastValueFrom(reference.closed); - if (result === ChangePlanDialogResultType.Closed) { - return; - } - await this.load(); - } else { - this.showChangePlan = !this.showChangePlan; + if (result === ChangePlanDialogResultType.Closed) { + return; } + await this.load(); } isSecretsManagerTrial(): boolean { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index a12d05e99bc..723fac226fd 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -21,7 +21,6 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", - EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub", GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", @@ -72,7 +71,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, - [FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE, [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, From a08c9776cb59a60456408c2b1f15320a7c8aa7f6 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:05:43 -0500 Subject: [PATCH 02/53] PM-14914/ssh-key-item-type-filtering-web (#11990) --- .../vault-filter/components/vault-filter.component.ts | 3 +++ .../vault-filter/services/vault-filter.service.ts | 6 ++++++ .../vault-filter/shared/models/filter-function.ts | 3 +++ .../shared/models/routed-vault-filter.model.ts | 11 ++++++++++- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 09a7356c452..2f8a429e42e 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -68,6 +68,9 @@ export class VaultFilterComponent implements OnInit, OnDestroy { if (this.activeFilter.cipherType === CipherType.SecureNote) { return "searchSecureNote"; } + if (this.activeFilter.cipherType === CipherType.SshKey) { + return "searchSshKey"; + } if (this.activeFilter.selectedFolderNode?.node) { return "searchFolder"; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index 043ba2dcd2f..7310a6aecec 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -306,6 +306,12 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { type: CipherType.SecureNote, icon: "bwi-sticky-note", }, + { + id: "sshKey", + name: this.i18nService.t("typeSshKey"), + type: CipherType.SshKey, + icon: "bwi-key", + }, ]; return this.buildTypeTree( diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts index 4b038512581..a39918df4a7 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -23,6 +23,9 @@ export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunc if (filter.type === "note" && cipher.type !== CipherType.SecureNote) { return false; } + if (filter.type === "sshKey" && cipher.type !== CipherType.SshKey) { + return false; + } if (filter.type === "trash" && !cipher.isDeleted) { return false; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts index 4f2659d6101..866ba1d9848 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts @@ -1,7 +1,16 @@ export const All = "all"; // TODO: Remove `All` when moving to vertical navigation. -const itemTypes = ["favorites", "login", "card", "identity", "note", "trash", All] as const; +const itemTypes = [ + "favorites", + "login", + "card", + "identity", + "note", + "sshKey", + "trash", + All, +] as const; export type RoutedVaultFilterItemType = (typeof itemTypes)[number]; From 642b8d2e6b774e7d2d1864e62b4d8312a2668d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 14 Nov 2024 10:09:59 -0500 Subject: [PATCH 03/53] [PM-14838] upgrade generator account storage to ObjectKey storage (#11975) --- .../generator/core/src/data/generators.ts | 143 ++++++++++++------ 1 file changed, 97 insertions(+), 46 deletions(-) diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index 6ddea595ec7..467a84dd3b4 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -24,11 +24,6 @@ import { } from "../policies"; import { CatchallConstraints } from "../policies/catchall-constraints"; import { SubaddressConstraints } from "../policies/subaddress-constraints"; -import { - EFF_USERNAME_SETTINGS, - PASSPHRASE_SETTINGS, - PASSWORD_SETTINGS, -} from "../strategies/storage"; import { CatchallGenerationOptions, CredentialGenerator, @@ -51,7 +46,10 @@ import { DefaultPasswordBoundaries } from "./default-password-boundaries"; import { DefaultPasswordGenerationOptions } from "./default-password-generation-options"; import { DefaultSubaddressOptions } from "./default-subaddress-generator-options"; -const PASSPHRASE = Object.freeze({ +const PASSPHRASE: CredentialGeneratorConfiguration< + PassphraseGenerationOptions, + PassphraseGeneratorPolicy +> = Object.freeze({ id: "passphrase", category: "password", nameKey: "passphrase", @@ -76,7 +74,23 @@ const PASSPHRASE = Object.freeze({ }, wordSeparator: { maxLength: 1 }, }, - account: PASSPHRASE_SETTINGS, + account: { + key: "passphraseGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "numWords", + "wordSeparator", + "capitalize", + "includeNumber", + ]), + state: GENERATOR_DISK, + initial: DefaultPassphraseGenerationOptions, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, }, policy: { type: PolicyType.PasswordGenerator, @@ -89,12 +103,12 @@ const PASSPHRASE = Object.freeze({ createEvaluator: (policy) => new PassphraseGeneratorOptionsEvaluator(policy), toConstraints: (policy) => new PassphrasePolicyConstraints(policy), }, -} satisfies CredentialGeneratorConfiguration< - PassphraseGenerationOptions, - PassphraseGeneratorPolicy ->); +}); -const PASSWORD = Object.freeze({ +const PASSWORD: CredentialGeneratorConfiguration< + PasswordGenerationOptions, + PasswordGeneratorPolicy +> = Object.freeze({ id: "password", category: "password", nameKey: "password", @@ -126,7 +140,29 @@ const PASSWORD = Object.freeze({ max: DefaultPasswordBoundaries.minSpecialCharacters.max, }, }, - account: PASSWORD_SETTINGS, + account: { + key: "passwordGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "length", + "ambiguous", + "uppercase", + "minUppercase", + "lowercase", + "minLowercase", + "number", + "minNumber", + "special", + "minSpecial", + ]), + state: GENERATOR_DISK, + initial: DefaultPasswordGenerationOptions, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, }, policy: { type: PolicyType.PasswordGenerator, @@ -143,43 +179,58 @@ const PASSWORD = Object.freeze({ createEvaluator: (policy) => new PasswordGeneratorOptionsEvaluator(policy), toConstraints: (policy) => new DynamicPasswordPolicyConstraints(policy), }, -} satisfies CredentialGeneratorConfiguration); +}); -const USERNAME = Object.freeze({ - id: "username", - category: "username", - nameKey: "randomWord", - generateKey: "generateUsername", - generatedValueKey: "username", - copyKey: "copyUsername", - onlyOnRequest: false, - request: [], - engine: { - create( - dependencies: GeneratorDependencyProvider, - ): CredentialGenerator { - return new UsernameRandomizer(dependencies.randomizer); +const USERNAME: CredentialGeneratorConfiguration = + Object.freeze({ + id: "username", + category: "username", + nameKey: "randomWord", + generateKey: "generateUsername", + generatedValueKey: "username", + copyKey: "copyUsername", + onlyOnRequest: false, + request: [], + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new UsernameRandomizer(dependencies.randomizer); + }, }, - }, - settings: { - initial: DefaultEffUsernameOptions, - constraints: {}, - account: EFF_USERNAME_SETTINGS, - }, - policy: { - type: PolicyType.PasswordGenerator, - disabledValue: {}, - combine(_acc: NoPolicy, _policy: Policy) { - return {}; + settings: { + initial: DefaultEffUsernameOptions, + constraints: {}, + account: { + key: "effUsernameGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "wordCapitalize", + "wordIncludeNumber", + ]), + state: GENERATOR_DISK, + initial: DefaultEffUsernameOptions, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, }, - createEvaluator(_policy: NoPolicy) { - return new DefaultPolicyEvaluator(); + policy: { + type: PolicyType.PasswordGenerator, + disabledValue: {}, + combine(_acc: NoPolicy, _policy: Policy) { + return {}; + }, + createEvaluator(_policy: NoPolicy) { + return new DefaultPolicyEvaluator(); + }, + toConstraints(_policy: NoPolicy) { + return new IdentityConstraint(); + }, }, - toConstraints(_policy: NoPolicy) { - return new IdentityConstraint(); - }, - }, -} satisfies CredentialGeneratorConfiguration); + }); const CATCHALL: CredentialGeneratorConfiguration = Object.freeze({ From e6fce421f574678a0476e58e69589dce4e4ae10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:49:16 +0000 Subject: [PATCH 04/53] [PM-10324] Add bulk delete option for organization members (#11892) * Refactor organization user API service to support bulk deletion of users * Add copy for bulk user delete dialog * Add bulk user delete dialog component * Add bulk user delete functionality to members component * Refactor members component to only display bulk user deletion option if the Account Deprovisioning flag is enabled * Patch build process * Revert "Patch build process" This reverts commit 917c969f004274d90e8c35ecf5fa83085a473f95. --------- Co-authored-by: Matt Bishop --- .../bulk/bulk-delete-dialog.component.html | 85 +++++++++++++++++++ .../bulk/bulk-delete-dialog.component.ts | 65 ++++++++++++++ .../members/members.component.html | 11 +++ .../members/members.component.ts | 16 ++++ .../organizations/members/members.module.ts | 2 + apps/web/src/locales/en/messages.json | 9 ++ .../organization-user-api.service.ts | 11 +++ .../default-organization-user-api.service.ts | 14 +++ 8 files changed, 213 insertions(+) create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html create mode 100644 apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html new file mode 100644 index 00000000000..9a4ce89671e --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html @@ -0,0 +1,85 @@ + + + + {{ "noSelectedMembersApplicable" | i18n }} + + + {{ error }} + + + +

{{ "deleteOrganizationUserWarning" | i18n }}

+
+ + + + {{ "member" | i18n }} + + + + + + + + +
+ {{ user.email }} + + {{ "invited" | i18n }} + +
+ {{ user.name }} + + +
+
+
+ + + + + {{ "member" | i18n }} + {{ "status" | i18n }} + + + + + + + + + {{ user.email }} + {{ user.name }} + + + {{ statuses.get(user.id) }} + + + {{ "bulkFilteredMessage" | i18n }} + + + + + +
+ + + + +
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts new file mode 100644 index 00000000000..1755b0b0b91 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts @@ -0,0 +1,65 @@ +import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DialogService } from "@bitwarden/components"; + +import { BulkUserDetails } from "./bulk-status.component"; + +type BulkDeleteDialogParams = { + organizationId: string; + users: BulkUserDetails[]; +}; + +@Component({ + templateUrl: "bulk-delete-dialog.component.html", +}) +export class BulkDeleteDialogComponent { + organizationId: string; + users: BulkUserDetails[]; + loading = false; + done = false; + error: string = null; + statuses = new Map(); + userStatusType = OrganizationUserStatusType; + + constructor( + @Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams, + protected i18nService: I18nService, + private organizationUserApiService: OrganizationUserApiService, + ) { + this.organizationId = dialogParams.organizationId; + this.users = dialogParams.users; + } + + async submit() { + try { + this.loading = true; + this.error = null; + + const response = await this.organizationUserApiService.deleteManyOrganizationUsers( + this.organizationId, + this.users.map((user) => user.id), + ); + + response.data.forEach((entry) => { + this.statuses.set( + entry.id, + entry.error ? entry.error : this.i18nService.t("deletedSuccessfully"), + ); + }); + + this.done = true; + } catch (e) { + this.error = e.message; + } finally { + this.loading = false; + } + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open(BulkDeleteDialogComponent, config); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index f87934dbe81..a9c5ab3e4a8 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -137,6 +137,17 @@ {{ "remove" | i18n }} + diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 30c5106a4fa..6d2d3a45128 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -61,6 +61,7 @@ import { OrganizationUserView } from "../core/views/organization-user.view"; import { openEntityEventsDialog } from "../manage/entity-events.component"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; +import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; @@ -543,6 +544,21 @@ export class MembersComponent extends BaseMembersComponent await this.load(); } + async bulkDelete() { + if (this.actionPromise != null) { + return; + } + + const dialogRef = BulkDeleteDialogComponent.open(this.dialogService, { + data: { + organizationId: this.organization.id, + users: this.dataSource.getCheckedUsers(), + }, + }); + await lastValueFrom(dialogRef.closed); + await this.load(); + } + async bulkRevoke() { await this.bulkRevokeOrRestore(true); } diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index d7c5a9bf1df..81697f8c845 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -8,6 +8,7 @@ import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; +import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.component"; import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component"; @@ -35,6 +36,7 @@ import { MembersComponent } from "./members.component"; BulkStatusComponent, MembersComponent, ResetPasswordComponent, + BulkDeleteDialogComponent, ], }) export class MembersModule {} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 00d2102c786..76cd45e1498 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -9695,5 +9695,14 @@ }, "suspendedOwnerOrgMessage": { "message": "To regain access to your organization, add a payment method." + }, + "deleteMembers": { + "message": "Delete members" + }, + "noSelectedMembersApplicable": { + "message": "This action is not applicable to any of the selected members." + }, + "deletedSuccessfully": { + "message": "Deleted successfully" } } diff --git a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts index 42cbe1438d1..3186bdaa84b 100644 --- a/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/abstractions/organization-user-api.service.ts @@ -282,4 +282,15 @@ export abstract class OrganizationUserApiService { * @param id - Organization user identifier */ abstract deleteOrganizationUser(organizationId: string, id: string): Promise; + + /** + * Delete many organization users + * @param organizationId - Identifier for the organization the users belongs to + * @param ids - List of organization user identifiers to delete + * @return List of user ids, including both those that were successfully deleted and those that had an error + */ + abstract deleteManyOrganizationUsers( + organizationId: string, + ids: string[], + ): Promise>; } diff --git a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts index d9e069dc934..7289f41d7e7 100644 --- a/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts +++ b/libs/admin-console/src/common/organization-user/services/default-organization-user-api.service.ts @@ -369,4 +369,18 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer false, ); } + + async deleteManyOrganizationUsers( + organizationId: string, + ids: string[], + ): Promise> { + const r = await this.apiService.send( + "DELETE", + "/organizations/" + organizationId + "/users/delete-account", + new OrganizationUserBulkRequest(ids), + true, + true, + ); + return new ListResponse(r, OrganizationUserBulkResponse); + } } From a1ad3383f7479f67b30be2b6415de1ec7a4b0524 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:06:57 -0500 Subject: [PATCH 05/53] [PM-13894] updating the text area for notes to have 5 rows (#11976) * updating the text area for notes to have 5 rows * Applying the row count to the edit page as well --------- Co-authored-by: --global <> --- .../additional-options-section.component.html | 2 +- .../additional-options/additional-options.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html index 0b0c729de89..c00f51c8eba 100644 --- a/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html +++ b/libs/vault/src/cipher-form/components/additional-options/additional-options-section.component.html @@ -6,7 +6,7 @@ {{ "notes" | i18n }} - + {{ "note" | i18n }} - + -

+

{{ pageTitle }}

diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index 4851541576f..bf53b23c373 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -39,7 +39,7 @@ class ExtensionContainerComponent {} @Component({ selector: "vault-placeholder", template: ` - + + > + + + + + +

+ Hover + + + + + + +

+ Focus Visible + + + + + + +

+ Disabled + + + + + + + `, + }), +}; + export const Primary: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` Span Badge containing lengthy text

Link
Badge diff --git a/libs/components/src/banner/banner.component.html b/libs/components/src/banner/banner.component.html index 865aacd410a..566494eb64a 100644 --- a/libs/components/src/banner/banner.component.html +++ b/libs/components/src/banner/banner.component.html @@ -1,18 +1,21 @@
- + + + + `, }), diff --git a/libs/components/src/button/button.component.spec.ts b/libs/components/src/button/button.component.spec.ts index a75ac400a96..f3c3aa3175c 100644 --- a/libs/components/src/button/button.component.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -27,57 +27,6 @@ describe("Button", () => { linkDebugElement = fixture.debugElement.query(By.css("a")); })); - it("should apply classes based on type", () => { - testAppComponent.buttonType = "primary"; - fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true); - - testAppComponent.buttonType = "secondary"; - fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true); - - testAppComponent.buttonType = "danger"; - fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-600")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-600")).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); - expect(linkDebugElement.nativeElement.classList.contains("tw-border-text-muted")).toBe(true); - }); - - it("should apply block when true and inline-block when false", () => { - testAppComponent.block = true; - fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(true); - expect(buttonDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(false); - expect(linkDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(false); - - testAppComponent.block = false; - fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-inline-block")).toBe(true); - expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(false); - expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(false); - }); - it("should not be disabled when loading and disabled are false", () => { testAppComponent.loading = false; testAppComponent.disabled = false; diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index d8787c994f4..115347d3163 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -4,9 +4,9 @@ import { Input, HostBinding, Component } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; const focusRing = [ - "focus-visible:tw-ring", + "focus-visible:tw-ring-2", "focus-visible:tw-ring-offset-2", - "focus-visible:tw-ring-primary-700", + "focus-visible:tw-ring-primary-600", "focus-visible:tw-z-10", ]; @@ -17,24 +17,15 @@ const buttonStyles: Record = { "!tw-text-contrast", "hover:tw-bg-primary-700", "hover:tw-border-primary-700", - "disabled:tw-bg-primary-600/60", - "disabled:tw-border-primary-600/60", - "disabled:!tw-text-contrast/60", - "disabled:tw-bg-clip-padding", - "disabled:tw-cursor-not-allowed", ...focusRing, ], secondary: [ "tw-bg-transparent", - "tw-border-text-muted", - "!tw-text-muted", - "hover:tw-bg-text-muted", - "hover:tw-border-text-muted", + "tw-border-primary-600", + "!tw-text-primary-600", + "hover:tw-bg-primary-600", + "hover:tw-border-primary-600", "hover:!tw-text-contrast", - "disabled:tw-bg-transparent", - "disabled:tw-border-text-muted/60", - "disabled:!tw-text-muted/60", - "disabled:tw-cursor-not-allowed", ...focusRing, ], danger: [ @@ -44,10 +35,6 @@ const buttonStyles: Record = { "hover:tw-bg-danger-600", "hover:tw-border-danger-600", "hover:!tw-text-contrast", - "disabled:tw-bg-transparent", - "disabled:tw-border-danger-600/60", - "disabled:!tw-text-danger/60", - "disabled:tw-cursor-not-allowed", ...focusRing, ], unstyled: [], @@ -64,14 +51,22 @@ export class ButtonComponent implements ButtonLikeAbstraction { "tw-font-semibold", "tw-py-1.5", "tw-px-3", - "tw-rounded", + "tw-rounded-full", "tw-transition", - "tw-border", + "tw-border-2", "tw-border-solid", "tw-text-center", "tw-no-underline", "hover:tw-no-underline", "focus:tw-outline-none", + "disabled:tw-bg-secondary-300", + "disabled:hover:tw-bg-secondary-300", + "disabled:tw-border-secondary-300", + "disabled:hover:tw-border-secondary-300", + "disabled:!tw-text-muted", + "disabled:hover:!tw-text-muted", + "disabled:tw-cursor-not-allowed", + "disabled:hover:tw-no-underline", ] .concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"]) .concat(buttonStyles[this.buttonType ?? "secondary"]); @@ -99,8 +94,4 @@ export class ButtonComponent implements ButtonLikeAbstraction { @Input() loading = false; @Input() disabled = false; - - setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") { - this.buttonType = value; - } } diff --git a/libs/components/src/button/button.mdx b/libs/components/src/button/button.mdx index 5ee709093e3..33e4aed19f7 100644 --- a/libs/components/src/button/button.mdx +++ b/libs/components/src/button/button.mdx @@ -64,9 +64,6 @@ Use the danger styling only in settings when the user may preform a permanent ac ## Disabled UI -Both the disabled and loading states use the default state’s color with a 60% opacity or -`tw-opacity-60`. - ## Block diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 6923878b353..260d5565c0c 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -23,9 +23,21 @@ type Story = StoryObj; export const Primary: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` +
- Link + + + + +
+ `, }), args: { diff --git a/libs/components/src/callout/callout.component.html b/libs/components/src/callout/callout.component.html index 57da850d8bb..f64e197b9ef 100644 --- a/libs/components/src/callout/callout.component.html +++ b/libs/components/src/callout/callout.component.html @@ -1,16 +1,13 @@ diff --git a/libs/components/src/callout/callout.component.spec.ts b/libs/components/src/callout/callout.component.spec.ts index 36abae437d5..b7388da3492 100644 --- a/libs/components/src/callout/callout.component.spec.ts +++ b/libs/components/src/callout/callout.component.spec.ts @@ -12,7 +12,7 @@ describe("Callout", () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [CalloutComponent], + imports: [CalloutComponent], providers: [ { provide: I18nService, diff --git a/libs/components/src/callout/callout.component.ts b/libs/components/src/callout/callout.component.ts index bfeae17b359..0fa1c130504 100644 --- a/libs/components/src/callout/callout.component.ts +++ b/libs/components/src/callout/callout.component.ts @@ -2,6 +2,9 @@ import { Component, Input, OnInit } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SharedModule } from "../shared"; +import { TypographyModule } from "../typography"; + export type CalloutTypes = "success" | "info" | "warning" | "danger"; const defaultIcon: Record = { @@ -22,6 +25,8 @@ let nextId = 0; @Component({ selector: "bit-callout", templateUrl: "callout.component.html", + standalone: true, + imports: [SharedModule, TypographyModule], }) export class CalloutComponent implements OnInit { @Input() type: CalloutTypes = "info"; @@ -42,13 +47,13 @@ export class CalloutComponent implements OnInit { get calloutClass() { switch (this.type) { case "danger": - return "tw-border-l-danger-600"; + return "tw-border-danger-600"; case "info": - return "tw-border-l-info-600"; + return "tw-border-info-600"; case "success": - return "tw-border-l-success-600"; + return "tw-border-success-600"; case "warning": - return "tw-border-l-warning-600"; + return "tw-border-warning-600"; } } diff --git a/libs/components/src/callout/callout.mdx b/libs/components/src/callout/callout.mdx index a40a970f895..3fdb53943b4 100644 --- a/libs/components/src/callout/callout.mdx +++ b/libs/components/src/callout/callout.mdx @@ -2,6 +2,10 @@ import { Meta, Story, Primary, Controls } from "@storybook/addon-docs"; import * as stories from "./callout.stories"; +```ts +import { CalloutModule } from "@bitwarden/components"; +``` + # Callouts diff --git a/libs/components/src/callout/callout.module.ts b/libs/components/src/callout/callout.module.ts index fac89dfbcf9..ad7e083fec7 100644 --- a/libs/components/src/callout/callout.module.ts +++ b/libs/components/src/callout/callout.module.ts @@ -1,11 +1,9 @@ -import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { CalloutComponent } from "./callout.component"; @NgModule({ - imports: [CommonModule], + imports: [CalloutComponent], exports: [CalloutComponent], - declarations: [CalloutComponent], }) export class CalloutModule {} diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts index 3aaed26d8d0..9a6520829b3 100644 --- a/libs/components/src/card/card.component.ts +++ b/libs/components/src/card/card.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component } from "@angular/core"; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: - "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg tw-py-4 tw-px-3", + "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 [&:not(bit-layout_*)]:tw-rounded-lg [&:not(bit-layout_*)]:tw-border-b-shadow tw-py-4 tw-px-3", }, }) export class CardComponent {} diff --git a/libs/components/src/checkbox/checkbox.component.ts b/libs/components/src/checkbox/checkbox.component.ts index d8fd3f76eaa..ef57d3d5cd3 100644 --- a/libs/components/src/checkbox/checkbox.component.ts +++ b/libs/components/src/checkbox/checkbox.component.ts @@ -17,12 +17,13 @@ export class CheckboxComponent implements BitFormControlAbstraction { "tw-transition", "tw-cursor-pointer", "tw-inline-block", + "tw-align-sub", "tw-rounded", "tw-border", "tw-border-solid", - "tw-border-secondary-600", - "tw-h-3.5", - "tw-w-3.5", + "tw-border-secondary-500", + "tw-h-5", + "tw-w-5", "tw-mr-1.5", "tw-bottom-[-1px]", // Fix checkbox looking off-center "tw-flex-none", // Flexbox fix for bit-form-control @@ -35,13 +36,16 @@ export class CheckboxComponent implements BitFormControlAbstraction { "hover:tw-border-2", "[&>label]:tw-border-2", - "focus-visible:tw-ring-2", - "focus-visible:tw-ring-offset-2", - "focus-visible:tw-ring-primary-700", + // if it exists, the parent form control handles focus + "[&:not(bit-form-control_*)]:focus-visible:tw-ring-2", + "[&:not(bit-form-control_*)]:focus-visible:tw-ring-offset-2", + "[&:not(bit-form-control_*)]:focus-visible:tw-ring-primary-600", "disabled:tw-cursor-auto", "disabled:tw-border", + "disabled:hover:tw-border", "disabled:tw-bg-secondary-100", + "disabled:hover:tw-bg-secondary-100", "checked:tw-bg-primary-600", "checked:tw-border-primary-600", @@ -53,6 +57,7 @@ export class CheckboxComponent implements BitFormControlAbstraction { "checked:before:tw-mask-position-[center]", "checked:before:tw-mask-repeat-[no-repeat]", "checked:disabled:tw-border-secondary-100", + "checked:disabled:hover:tw-border-secondary-100", "checked:disabled:tw-bg-secondary-100", "checked:disabled:before:tw-bg-text-muted", @@ -78,11 +83,11 @@ export class CheckboxComponent implements BitFormControlAbstraction { @HostBinding("style.--mask-image") protected maskImage = - `url('data:image/svg+xml,%3Csvg class="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="8" viewBox="0 0 10 10"%3E%3Cpath d="M0.5 6.2L2.9 8.6L9.5 1.4" fill="none" stroke="white" stroke-width="2"%3E%3C/path%3E%3C/svg%3E')`; + `url('data:image/svg+xml,%3Csvg class="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 10 10"%3E%3Cpath d="M0.5 6.2L2.9 8.6L9.5 1.4" fill="none" stroke="white" stroke-width="2"%3E%3C/path%3E%3C/svg%3E')`; @HostBinding("style.--indeterminate-mask-image") protected indeterminateImage = - `url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="none" viewBox="0 0 13 13"%3E%3Cpath stroke="%23fff" stroke-width="2" d="M2.5 6.5h8"/%3E%3C/svg%3E%0A')`; + `url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 13 13"%3E%3Cpath stroke="%23fff" stroke-width="2" d="M2.5 6.5h8"/%3E%3C/svg%3E%0A')`; @HostBinding() @Input() diff --git a/libs/components/src/checkbox/checkbox.stories.ts b/libs/components/src/checkbox/checkbox.stories.ts index 0d649eb433d..c322f29b957 100644 --- a/libs/components/src/checkbox/checkbox.stories.ts +++ b/libs/components/src/checkbox/checkbox.stories.ts @@ -11,12 +11,14 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; +import { BadgeModule } from "../badge"; import { FormControlModule } from "../form-control"; +import { TableModule } from "../table"; import { I18nMockService } from "../utils/i18n-mock.service"; import { CheckboxModule } from "./checkbox.module"; -const template = ` +const template = /*html*/ `
@@ -54,7 +56,14 @@ export default { decorators: [ moduleMetadata({ declarations: [ExampleComponent], - imports: [FormsModule, ReactiveFormsModule, FormControlModule, CheckboxModule], + imports: [ + FormsModule, + ReactiveFormsModule, + FormControlModule, + CheckboxModule, + TableModule, + BadgeModule, + ], providers: [ { provide: I18nService, @@ -82,7 +91,10 @@ type Story = StoryObj; export const Default: Story = { render: (args) => ({ props: args, - template: ``, + template: /*html*/ ` + + + `, }), parameters: { docs: { @@ -91,9 +103,39 @@ export const Default: Story = { }, }, }, - args: { - checked: false, - disabled: false, +}; + +export const LongLabel: Story = { + render: () => ({ + props: { + formObj: new FormGroup({ + checkbox: new FormControl(false), + }), + }, + template: /*html*/ ` + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur iaculis consequat enim vitae elementum. + Ut non odio est. + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur iaculis consequat enim vitae elementum. + Ut non odio est. + Premium + + + + `, + }), + parameters: { + docs: { + source: { + code: template, + }, + }, }, }; @@ -104,7 +146,7 @@ export const Hint: Story = { checkbox: new FormControl(false), }), }, - template: ` + template: /*html*/ `
@@ -131,20 +173,37 @@ export const Hint: Story = { }, }; +export const Disabled: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + `, + }), + parameters: { + docs: { + source: { + code: template, + }, + }, + }, +}; + export const Custom: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
-