From a9548f519e094086d3418acf24f7d0da1a6a797f Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 16 Jun 2025 16:33:07 -0500 Subject: [PATCH 01/31] [PM-20112] Update Member Access report to use new server model (#15155) --- .../member-access-report.component.html | 2 +- .../response/member-access-report.response.ts | 49 +- .../services/member-access-report.mock.ts | 514 ++++++++++-------- .../member-access-report.service.spec.ts | 34 +- .../services/member-access-report.service.ts | 136 ++--- 5 files changed, 396 insertions(+), 339 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html index 31eb54d6110..0200e206327 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/member-access-report.component.html @@ -36,7 +36,7 @@ - {{ "members" | i18n }} + {{ "members" | i18n }} {{ "groups" | i18n }} {{ "collections" | i18n }} {{ "items" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/response/member-access-report.response.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/response/member-access-report.response.ts index 959b70b9729..c500c6c0aec 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/response/member-access-report.response.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/response/member-access-report.response.ts @@ -2,7 +2,15 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { Guid } from "@bitwarden/common/types/guid"; -export class MemberAccessDetails extends BaseResponse { +export class MemberAccessResponse extends BaseResponse { + userName: string; + email: string; + twoFactorEnabled: boolean; + accountRecoveryEnabled: boolean; + userGuid: Guid; + usesKeyConnector: boolean; + + cipherIds: Guid[] = []; collectionId: string; groupId: string; groupName: string; @@ -14,6 +22,14 @@ export class MemberAccessDetails extends BaseResponse { constructor(response: any) { super(response); + this.userName = this.getResponseProperty("UserName"); + this.email = this.getResponseProperty("Email"); + this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled"); + this.accountRecoveryEnabled = this.getResponseProperty("AccountRecoveryEnabled"); + this.userGuid = this.getResponseProperty("UserGuid"); + this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector"); + + this.cipherIds = this.getResponseProperty("CipherIds") || []; this.groupId = this.getResponseProperty("GroupId"); this.collectionId = this.getResponseProperty("CollectionId"); this.groupName = this.getResponseProperty("GroupName"); @@ -24,34 +40,3 @@ export class MemberAccessDetails extends BaseResponse { this.manage = this.getResponseProperty("Manage"); } } - -export class MemberAccessResponse extends BaseResponse { - userName: string; - email: string; - twoFactorEnabled: boolean; - accountRecoveryEnabled: boolean; - collectionsCount: number; - groupsCount: number; - totalItemCount: number; - accessDetails: MemberAccessDetails[] = []; - userGuid: Guid; - usesKeyConnector: boolean; - - constructor(response: any) { - super(response); - this.userName = this.getResponseProperty("UserName"); - this.email = this.getResponseProperty("Email"); - this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled"); - this.accountRecoveryEnabled = this.getResponseProperty("AccountRecoveryEnabled"); - this.collectionsCount = this.getResponseProperty("CollectionsCount"); - this.groupsCount = this.getResponseProperty("GroupsCount"); - this.totalItemCount = this.getResponseProperty("TotalItemCount"); - this.userGuid = this.getResponseProperty("UserGuid"); - this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector"); - - const details = this.getResponseProperty("AccessDetails"); - if (details != null) { - this.accessDetails = details.map((o: any) => new MemberAccessDetails(o)); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.mock.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.mock.ts index b07e4946ca7..ebf2b9abfc8 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.mock.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.mock.ts @@ -1,9 +1,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { Guid } from "@bitwarden/common/types/guid"; -import { - MemberAccessDetails, - MemberAccessResponse, -} from "../response/member-access-report.response"; +import { MemberAccessResponse } from "../response/member-access-report.response"; export const memberAccessReportsMock: MemberAccessResponse[] = [ { @@ -11,223 +9,290 @@ export const memberAccessReportsMock: MemberAccessResponse[] = [ email: "sjohnson@email.com", twoFactorEnabled: true, accountRecoveryEnabled: true, - groupsCount: 2, - collectionsCount: 4, - totalItemCount: 20, - userGuid: "1234", + userGuid: "1001" as Guid, usesKeyConnector: false, - accessDetails: [ - { - groupId: "", - collectionId: "c1", - collectionName: new EncString("Collection 1"), - groupName: "", - itemCount: 10, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "", - collectionId: "c2", - collectionName: new EncString("Collection 2"), - groupName: "", - itemCount: 20, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "", - collectionId: "c3", - collectionName: new EncString("Collection 3"), - groupName: "", - itemCount: 30, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "g1", - collectionId: "c1", - collectionName: new EncString("Collection 1"), - groupName: "Group 1", - itemCount: 30, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "g1", - collectionId: "c2", - collectionName: new EncString("Collection 2"), - groupName: "Group 1", - itemCount: 20, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - ], - } as MemberAccessResponse, + groupId: "", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "", + itemCount: 10, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Sarah Johnson", + email: "sjohnson@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + userGuid: "1001" as Guid, + usesKeyConnector: false, + groupId: "", + collectionId: "c2", + collectionName: new EncString("Collection 2"), + groupName: "", + itemCount: 20, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Sarah Johnson", + email: "sjohnson@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + userGuid: "1001" as Guid, + usesKeyConnector: false, + groupId: "", + collectionId: "c3", + collectionName: new EncString("Collection 3"), + groupName: "", + itemCount: 30, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Sarah Johnson", + email: "sjohnson@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + userGuid: "1001", + usesKeyConnector: false, + groupId: "g1", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "Group 1", + itemCount: 30, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Sarah Johnson", + email: "sjohnson@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + userGuid: "1001", + usesKeyConnector: false, + groupId: "g1", + collectionId: "c2", + collectionName: new EncString("Collection 2"), + groupName: "Group 1", + itemCount: 20, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, { userName: "James Lull", email: "jlull@email.com", twoFactorEnabled: false, accountRecoveryEnabled: false, - groupsCount: 2, - collectionsCount: 4, - totalItemCount: 20, - userGuid: "1234", + userGuid: "2001", usesKeyConnector: false, - accessDetails: [ - { - groupId: "g4", - collectionId: "c4", - groupName: "Group 4", - collectionName: new EncString("Collection 4"), - itemCount: 5, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "g4", - collectionId: "c5", - groupName: "Group 4", - collectionName: new EncString("Collection 5"), - itemCount: 15, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "", - collectionId: "c4", - groupName: "", - collectionName: new EncString("Collection 4"), - itemCount: 5, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "", - collectionId: "c5", - groupName: "", - collectionName: new EncString("Collection 5"), - itemCount: 15, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - ], - } as MemberAccessResponse, + groupId: "g4", + collectionId: "c4", + groupName: "Group 4", + collectionName: new EncString("Collection 4"), + itemCount: 5, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "James Lull", + email: "jlull@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "2001", + usesKeyConnector: false, + groupId: "g4", + collectionId: "c5", + groupName: "Group 4", + collectionName: new EncString("Collection 5"), + itemCount: 15, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "James Lull", + email: "jlull@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "2001", + usesKeyConnector: false, + groupId: "", + collectionId: "c4", + groupName: "", + collectionName: new EncString("Collection 4"), + itemCount: 5, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "James Lull", + email: "jlull@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "2001", + usesKeyConnector: false, + groupId: "", + collectionId: "c5", + groupName: "", + collectionName: new EncString("Collection 5"), + itemCount: 15, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, { userName: "Beth Williams", email: "bwilliams@email.com", twoFactorEnabled: true, accountRecoveryEnabled: true, - groupsCount: 2, - collectionsCount: 4, - totalItemCount: 20, - userGuid: "1234", + userGuid: "3001", usesKeyConnector: false, - accessDetails: [ - { - groupId: "", - collectionId: "c6", - groupName: "", - collectionName: new EncString("Collection 6"), - itemCount: 25, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "g6", - collectionId: "c4", - groupName: "Group 6", - collectionName: new EncString("Collection 4"), - itemCount: 35, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - ], - } as MemberAccessResponse, + groupId: "", + collectionId: "c6", + groupName: "", + collectionName: new EncString("Collection 6"), + itemCount: 25, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Beth Williams", + email: "bwilliams@email.com", + twoFactorEnabled: true, + accountRecoveryEnabled: true, + userGuid: "3001", + usesKeyConnector: false, + groupId: "g6", + collectionId: "c4", + groupName: "Group 6", + collectionName: new EncString("Collection 4"), + itemCount: 35, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, { userName: "Ray Williams", email: "rwilliams@email.com", twoFactorEnabled: false, accountRecoveryEnabled: false, - groupsCount: 2, - collectionsCount: 4, - totalItemCount: 20, - userGuid: "1234", + userGuid: "4000", usesKeyConnector: false, - accessDetails: [ - { - groupId: "", - collectionId: "c7", - groupName: "", - collectionName: new EncString("Collection 7"), - itemCount: 8, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "", - collectionId: "c8", - groupName: "", - collectionName: new EncString("Collection 8"), - itemCount: 12, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "", - collectionId: "c9", - groupName: "", - collectionName: new EncString("Collection 9"), - itemCount: 16, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "g9", - collectionId: "c7", - groupName: "Group 9", - collectionName: new EncString("Collection 7"), - itemCount: 8, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "g10", - collectionId: "c8", - groupName: "Group 10", - collectionName: new EncString("Collection 8"), - itemCount: 12, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - { - groupId: "g11", - collectionId: "c9", - groupName: "Group 11", - collectionName: new EncString("Collection 9"), - itemCount: 16, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - ], - } as MemberAccessResponse, + groupId: "", + collectionId: "c7", + groupName: "", + collectionName: new EncString("Collection 7"), + itemCount: 8, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Ray Williams", + email: "rwilliams@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "4000", + usesKeyConnector: false, + groupId: "", + collectionId: "c8", + groupName: "", + collectionName: new EncString("Collection 8"), + itemCount: 12, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Ray Williams", + email: "rwilliams@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "4000", + usesKeyConnector: false, + groupId: "", + collectionId: "c9", + groupName: "", + collectionName: new EncString("Collection 9"), + itemCount: 16, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Ray Williams", + email: "rwilliams@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "4000", + usesKeyConnector: false, + groupId: "g9", + collectionId: "c7", + groupName: "Group 9", + collectionName: new EncString("Collection 7"), + itemCount: 8, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Ray Williams", + email: "rwilliams@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "4000", + usesKeyConnector: false, + groupId: "g10", + collectionId: "c8", + groupName: "Group 10", + collectionName: new EncString("Collection 8"), + itemCount: 12, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, + { + userName: "Ray Williams", + email: "rwilliams@email.com", + twoFactorEnabled: false, + accountRecoveryEnabled: false, + userGuid: "4000", + usesKeyConnector: false, + groupId: "g11", + collectionId: "c9", + groupName: "Group 11", + collectionName: new EncString("Collection 9"), + itemCount: 16, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, ]; export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] = [ @@ -236,34 +301,33 @@ export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] email: "asmith@email.com", twoFactorEnabled: true, accountRecoveryEnabled: true, - groupsCount: 2, - collectionsCount: 4, - totalItemCount: 20, - userGuid: "1234", + userGuid: "1234" as Guid, usesKeyConnector: false, - accessDetails: [ - { - groupId: "", - collectionId: "c1", - collectionName: new EncString("Collection 1"), - groupName: "Alice Group 1", - itemCount: 10, - readOnly: false, - hidePasswords: false, - manage: false, - } as MemberAccessDetails, - ], - } as MemberAccessResponse, + groupId: "", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "Alice Group 1", + itemCount: 10, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, { userName: "Robert Brown", email: "rbrown@email.com", twoFactorEnabled: false, accountRecoveryEnabled: false, - groupsCount: 2, - collectionsCount: 4, - totalItemCount: 20, - userGuid: "5678", + userGuid: "5678" as Guid, usesKeyConnector: false, - accessDetails: [] as MemberAccessDetails[], - } as MemberAccessResponse, + groupId: "", + collectionId: "c1", + collectionName: new EncString("Collection 1"), + groupName: "", + itemCount: 10, + readOnly: false, + hidePasswords: false, + manage: false, + cipherIds: [], + } as unknown as MemberAccessResponse, ]; diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts index e6efac83616..ad388cfed04 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.spec.ts @@ -35,36 +35,36 @@ describe("ImportService", () => { { name: "Sarah Johnson", email: "sjohnson@email.com", - collectionsCount: 4, - groupsCount: 2, - itemsCount: 20, + collectionsCount: 3, + groupsCount: 1, + itemsCount: 0, userGuid: expect.any(String), usesKeyConnector: expect.any(Boolean), }, { name: "James Lull", email: "jlull@email.com", - collectionsCount: 4, - groupsCount: 2, - itemsCount: 20, + collectionsCount: 2, + groupsCount: 1, + itemsCount: 0, userGuid: expect.any(String), usesKeyConnector: expect.any(Boolean), }, { name: "Beth Williams", email: "bwilliams@email.com", - collectionsCount: 4, - groupsCount: 2, - itemsCount: 20, + collectionsCount: 2, + groupsCount: 1, + itemsCount: 0, userGuid: expect.any(String), usesKeyConnector: expect.any(Boolean), }, { name: "Ray Williams", email: "rwilliams@email.com", - collectionsCount: 4, - groupsCount: 2, - itemsCount: 20, + collectionsCount: 3, + groupsCount: 3, + itemsCount: 0, userGuid: expect.any(String), usesKeyConnector: expect.any(Boolean), }, @@ -82,8 +82,8 @@ describe("ImportService", () => { (item) => (item.name === "Sarah Johnson" && item.group === "Group 1" && - item.totalItems === "20") || - (item.name === "James Lull" && item.group === "Group 4" && item.totalItems === "5"), + item.totalItems === "0") || + (item.name === "James Lull" && item.group === "Group 4" && item.totalItems === "0"), ) .map((item) => ({ name: item.name, @@ -102,7 +102,7 @@ describe("ImportService", () => { twoStepLogin: "memberAccessReportTwoFactorEnabledTrue", accountRecovery: "memberAccessReportAuthenticationEnabledTrue", group: "Group 1", - totalItems: "20", + totalItems: "0", }), expect.objectContaining({ email: "jlull@email.com", @@ -110,7 +110,7 @@ describe("ImportService", () => { twoStepLogin: "memberAccessReportTwoFactorEnabledFalse", accountRecovery: "memberAccessReportAuthenticationEnabledFalse", group: "Group 4", - totalItems: "5", + totalItems: "0", }), ]), ); @@ -131,7 +131,7 @@ describe("ImportService", () => { twoStepLogin: "memberAccessReportTwoFactorEnabledTrue", accountRecovery: "memberAccessReportAuthenticationEnabledTrue", group: "Alice Group 1", - totalItems: "10", + totalItems: "0", }), expect.objectContaining({ email: "rbrown@email.com", diff --git a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts index 029dce8a404..0039788709e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/dirt/reports/member-access-report/services/member-access-report.service.ts @@ -5,13 +5,13 @@ import { Injectable } from "@angular/core"; import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { Guid, OrganizationId } from "@bitwarden/common/types/guid"; import { getPermissionList, convertToPermission, } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector"; -import { MemberAccessDetails } from "../response/member-access-report.response"; +import { MemberAccessResponse } from "../response/member-access-report.response"; import { MemberAccessExportItem } from "../view/member-access-export.view"; import { MemberAccessReportView } from "../view/member-access-report.view"; @@ -34,15 +34,44 @@ export class MemberAccessReportService { organizationId: OrganizationId, ): Promise { const memberAccessData = await this.reportApiService.getMemberAccessData(organizationId); - const memberAccessReportViewCollection = memberAccessData.map((userData) => ({ - name: userData.userName, - email: userData.email, - collectionsCount: userData.collectionsCount, - groupsCount: userData.groupsCount, - itemsCount: userData.totalItemCount, - userGuid: userData.userGuid, - usesKeyConnector: userData.usesKeyConnector, - })); + + // group member access data by userGuid + const userMap = new Map(); + memberAccessData.forEach((userData) => { + const userGuid = userData.userGuid; + if (!userMap.has(userGuid)) { + userMap.set(userGuid, []); + } + userMap.get(userGuid)?.push(userData); + }); + + // aggregate user data + const memberAccessReportViewCollection: MemberAccessReportView[] = []; + userMap.forEach((userDataArray, userGuid) => { + const collectionCount = this.getDistinctCount( + userDataArray.map((data) => data.collectionId).filter((id) => !!id), + ); + const groupCount = this.getDistinctCount( + userDataArray.map((data) => data.groupId).filter((id) => !!id), + ); + const itemsCount = this.getDistinctCount( + userDataArray + .flatMap((data) => data.cipherIds) + .filter((id) => id !== "00000000-0000-0000-0000-000000000000"), + ); + const aggregatedData = { + userGuid: userGuid, + name: userDataArray[0].userName, + email: userDataArray[0].email, + collectionsCount: collectionCount, + groupsCount: groupCount, + itemsCount: itemsCount, + usesKeyConnector: userDataArray.some((data) => data.usesKeyConnector), + }; + + memberAccessReportViewCollection.push(aggregatedData); + }); + return memberAccessReportViewCollection; } @@ -50,13 +79,8 @@ export class MemberAccessReportService { organizationId: OrganizationId, ): Promise { const memberAccessReports = await this.reportApiService.getMemberAccessData(organizationId); - const collectionNames = memberAccessReports.flatMap((item) => - item.accessDetails.map((dtl) => { - if (dtl.collectionName) { - return dtl.collectionName.encryptedString; - } - }), - ); + const collectionNames = memberAccessReports.map((item) => item.collectionName.encryptedString); + const collectionNameMap = new Map(collectionNames.map((col) => [col, ""])); for await (const key of collectionNameMap.keys()) { const decrypted = new EncString(key); @@ -64,56 +88,35 @@ export class MemberAccessReportService { collectionNameMap.set(key, decrypted.decryptedValue); } - const exportItems = memberAccessReports.flatMap((report) => { - // to include users without access details - // which means a user has no groups, collections or items - if (report.accessDetails.length === 0) { - return [ - { - email: report.email, - name: report.userName, - twoStepLogin: report.twoFactorEnabled - ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue") - : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"), - accountRecovery: report.accountRecoveryEnabled - ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue") - : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"), - group: this.i18nService.t("memberAccessReportNoGroup"), - collection: this.i18nService.t("memberAccessReportNoCollection"), - collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"), - totalItems: "0", - }, - ]; - } - const userDetails = report.accessDetails.map((detail) => { - const collectionName = collectionNameMap.get(detail.collectionName.encryptedString); - return { - email: report.email, - name: report.userName, - twoStepLogin: report.twoFactorEnabled - ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue") - : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"), - accountRecovery: report.accountRecoveryEnabled - ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue") - : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"), - group: detail.groupName - ? detail.groupName - : this.i18nService.t("memberAccessReportNoGroup"), - collection: collectionName - ? collectionName - : this.i18nService.t("memberAccessReportNoCollection"), - collectionPermission: detail.collectionId - ? this.getPermissionText(detail) - : this.i18nService.t("memberAccessReportNoCollectionPermission"), - totalItems: detail.itemCount.toString(), - }; - }); - return userDetails; + const exportItems = memberAccessReports.map((report) => { + const collectionName = collectionNameMap.get(report.collectionName.encryptedString); + return { + email: report.email, + name: report.userName, + twoStepLogin: report.twoFactorEnabled + ? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue") + : this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"), + accountRecovery: report.accountRecoveryEnabled + ? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue") + : this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"), + group: report.groupName + ? report.groupName + : this.i18nService.t("memberAccessReportNoGroup"), + collection: collectionName + ? collectionName + : this.i18nService.t("memberAccessReportNoCollection"), + collectionPermission: report.collectionId + ? this.getPermissionText(report) + : this.i18nService.t("memberAccessReportNoCollectionPermission"), + totalItems: report.cipherIds + .filter((_) => _ != "00000000-0000-0000-0000-000000000000") + .length.toString(), + }; }); return exportItems.flat(); } - private getPermissionText(accessDetails: MemberAccessDetails): string { + private getPermissionText(accessDetails: MemberAccessResponse): string { const permissionList = getPermissionList(); const collectionSelectionView = new CollectionAccessSelectionView({ id: accessDetails.groupId ?? accessDetails.collectionId, @@ -125,4 +128,9 @@ export class MemberAccessReportService { permissionList.find((p) => p.perm === convertToPermission(collectionSelectionView))?.labelId, ); } + + private getDistinctCount(items: T[]): number { + const uniqueItems = new Set(items); + return uniqueItems.size; + } } From 1dd7eae466b745dbf34e69b1ef8172f12927dfc1 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 17 Jun 2025 12:59:35 +0200 Subject: [PATCH 02/31] Update sdk for breaking init change (#15212) --- .../src/platform/services/sdk/default-sdk.service.ts | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index d9f7ba19a6f..e874dae3461 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -212,6 +212,7 @@ export class DefaultSdkService implements SdkService { }, }, privateKey, + signingKey: undefined, }); // We initialize the org crypto even if the org_keys are diff --git a/package-lock.json b/package-lock.json index e0d40c8cc99..0b91c139a55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.198", + "@bitwarden/sdk-internal": "0.2.0-main.203", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0", @@ -4378,9 +4378,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.198", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.198.tgz", - "integrity": "sha512-/MRdlcBqGxFEK/p6bU4hu5ZRoa+PqU88S+xnQaFrCXsWCTXrC8Nvm46iiz6gAqdbfFQWFNLCtmoNx6LFUdRuNg==", + "version": "0.2.0-main.203", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.203.tgz", + "integrity": "sha512-AcRX2odnabnx16VF+K7naEZ3R4Tv/o8mVsVhrvwOTG+TEBUxR1BzCoE2r+l0+iz1zV32UV2YHeLZvyCB2/KftA==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 3304d168259..d2e480f6762 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.198", + "@bitwarden/sdk-internal": "0.2.0-main.203", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.1.0", From 0ce4a2ce392c27afe3e06cfcf8aebba4334b1400 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 17 Jun 2025 13:12:15 +0200 Subject: [PATCH 03/31] [PM-22745] Move clientkeyhalf to os impl (#15140) * Move clientkeyhalf to main * Move clientkeyhalf to os platform implementation * Cleanup * Fix tests * Tests * Add tests * Add tests * Fix types * Undo linux debugging changes * Fix typo * Update apps/desktop/src/key-management/biometrics/os-biometrics.service.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update apps/desktop/src/key-management/biometrics/os-biometrics.service.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update apps/desktop/src/key-management/biometrics/os-biometrics.service.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Update apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> * Fix build --------- Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> --- .../biometrics/desktop.biometrics.service.ts | 9 +- .../main-biometrics-ipc.listener.ts | 8 +- .../main-biometrics.service.spec.ts | 151 +++++++----------- .../biometrics/main-biometrics.service.ts | 76 +++------ .../biometrics/os-biometrics-linux.service.ts | 105 ++++++++---- .../biometrics/os-biometrics-mac.service.ts | 44 +++-- .../os-biometrics-windows.service.spec.ts | 143 +++++++++++++++++ .../os-biometrics-windows.service.ts | 139 ++++++++++++---- .../biometrics/os-biometrics.service.ts | 28 ++-- .../biometrics/renderer-biometrics.service.ts | 14 +- .../electron-key.service.spec.ts | 72 +-------- .../key-management/electron-key.service.ts | 37 +---- apps/desktop/src/key-management/preload.ts | 15 +- apps/desktop/src/main.ts | 10 +- apps/desktop/src/types/biometric-message.ts | 12 +- 15 files changed, 481 insertions(+), 382 deletions(-) create mode 100644 apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts diff --git a/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts b/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts index 6415443bfbc..97e1d322a0e 100644 --- a/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/desktop.biometrics.service.ts @@ -1,3 +1,4 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { BiometricsService } from "@bitwarden/key-management"; @@ -6,10 +7,10 @@ import { BiometricsService } from "@bitwarden/key-management"; * specifically for the main process. */ export abstract class DesktopBiometricsService extends BiometricsService { - abstract setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise; + abstract setBiometricProtectedUnlockKeyForUser( + userId: UserId, + value: SymmetricCryptoKey, + ): Promise; abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise; - abstract setupBiometrics(): Promise; - - abstract setClientKeyHalfForUser(userId: UserId, value: string | null): Promise; } diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts index fe40aad54d9..e270c4cc50f 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics-ipc.listener.ts @@ -1,5 +1,6 @@ import { ipcMain } from "electron"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -37,17 +38,12 @@ export class MainBiometricsIPCListener { } return await this.biometricService.setBiometricProtectedUnlockKeyForUser( message.userId as UserId, - message.key, + SymmetricCryptoKey.fromString(message.key), ); case BiometricAction.RemoveKeyForUser: return await this.biometricService.deleteBiometricUnlockKeyForUser( message.userId as UserId, ); - case BiometricAction.SetClientKeyHalf: - return await this.biometricService.setClientKeyHalfForUser( - message.userId as UserId, - message.key, - ); case BiometricAction.Setup: return await this.biometricService.setupBiometrics(); diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts index 09a4dcef4b3..213f3d48a98 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.spec.ts @@ -1,10 +1,10 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { BiometricsService, @@ -13,6 +13,7 @@ import { } from "@bitwarden/key-management"; import { WindowMain } from "../../main/window.main"; +import { MainCryptoFunctionService } from "../../platform/main/main-crypto-function.service"; import { MainBiometricsService } from "./main-biometrics.service"; import OsBiometricsServiceLinux from "./os-biometrics-linux.service"; @@ -27,21 +28,25 @@ jest.mock("@bitwarden/desktop-napi", () => { }; }); +const unlockKey = new SymmetricCryptoKey(new Uint8Array(64)); + describe("MainBiometricsService", function () { const i18nService = mock(); const windowMain = mock(); const logService = mock(); - const messagingService = mock(); const biometricStateService = mock(); + const cryptoFunctionService = mock(); + const encryptService = mock(); it("Should call the platformspecific methods", async () => { const sut = new MainBiometricsService( i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const mockService = mock(); @@ -57,9 +62,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, "win32", biometricStateService, + encryptService, + cryptoFunctionService, ); const internalService = (sut as any).osBiometricsService; @@ -72,9 +78,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, "darwin", biometricStateService, + encryptService, + cryptoFunctionService, ); const internalService = (sut as any).osBiometricsService; expect(internalService).not.toBeNull(); @@ -86,9 +93,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, "linux", biometricStateService, + encryptService, + cryptoFunctionService, ); const internalService = (sut as any).osBiometricsService; @@ -106,9 +114,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); innerService = mock(); @@ -131,9 +140,9 @@ describe("MainBiometricsService", function () { ]; for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) { - innerService.osSupportsBiometric.mockResolvedValue(supportsBiometric as boolean); - innerService.osBiometricsNeedsSetup.mockResolvedValue(needsSetup as boolean); - innerService.osBiometricsCanAutoSetup.mockResolvedValue(canAutoSetup as boolean); + innerService.supportsBiometrics.mockResolvedValue(supportsBiometric as boolean); + innerService.needsSetup.mockResolvedValue(needsSetup as boolean); + innerService.canAutoSetup.mockResolvedValue(canAutoSetup as boolean); const actual = await sut.getBiometricsStatus(); expect(actual).toBe(expected); @@ -175,12 +184,23 @@ describe("MainBiometricsService", function () { biometricStateService.getRequirePasswordOnStart.mockResolvedValue( requirePasswordOnStart as boolean, ); - (sut as any).clientKeyHalves = new Map(); - const userId = "test" as UserId; - if (hasKeyHalf) { - (sut as any).clientKeyHalves.set(userId, "test"); + if (!requirePasswordOnStart) { + (sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest + .fn() + .mockResolvedValue(BiometricsStatus.Available); + } else { + if (hasKeyHalf) { + (sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest + .fn() + .mockResolvedValue(BiometricsStatus.Available); + } else { + (sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest + .fn() + .mockResolvedValue(BiometricsStatus.UnlockNeeded); + } } + const userId = "test" as UserId; const actual = await sut.getBiometricsStatusForUser(userId); expect(actual).toBe(expected); } @@ -193,50 +213,17 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; await sut.setupBiometrics(); - expect(osBiometricsService.osBiometricsSetup).toHaveBeenCalled(); - }); - }); - - describe("setClientKeyHalfForUser", () => { - let sut: MainBiometricsService; - - beforeEach(() => { - sut = new MainBiometricsService( - i18nService, - windowMain, - logService, - messagingService, - process.platform, - biometricStateService, - ); - }); - - it("should set the client key half for the user", async () => { - const userId = "test" as UserId; - const keyHalf = "testKeyHalf"; - - await sut.setClientKeyHalfForUser(userId, keyHalf); - - expect((sut as any).clientKeyHalves.has(userId)).toBe(true); - expect((sut as any).clientKeyHalves.get(userId)).toBe(keyHalf); - }); - - it("should reset the client key half for the user", async () => { - const userId = "test" as UserId; - - await sut.setClientKeyHalfForUser(userId, null); - - expect((sut as any).clientKeyHalves.has(userId)).toBe(true); - expect((sut as any).clientKeyHalves.get(userId)).toBe(null); + expect(osBiometricsService.runSetup).toHaveBeenCalled(); }); }); @@ -246,9 +233,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; @@ -268,9 +256,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; @@ -278,34 +267,24 @@ describe("MainBiometricsService", function () { it("should return null if no biometric key is returned ", async () => { const userId = "test" as UserId; - (sut as any).clientKeyHalves.set(userId, "testKeyHalf"); - + osBiometricsService.getBiometricKey.mockResolvedValue(null); const userKey = await sut.unlockWithBiometricsForUser(userId); expect(userKey).toBeNull(); - expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - "testKeyHalf", - ); + expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId); }); it("should return the biometric key if a valid key is returned", async () => { const userId = "test" as UserId; - (sut as any).clientKeyHalves.set(userId, "testKeyHalf"); - const biometricKey = Utils.fromBufferToB64(new Uint8Array(64)); + const biometricKey = new SymmetricCryptoKey(new Uint8Array(64)); osBiometricsService.getBiometricKey.mockResolvedValue(biometricKey); const userKey = await sut.unlockWithBiometricsForUser(userId); expect(userKey).not.toBeNull(); - expect(userKey!.keyB64).toBe(biometricKey); + expect(userKey!.keyB64).toBe(biometricKey.toBase64()); expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); - expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - "testKeyHalf", - ); + expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId); }); }); @@ -318,37 +297,21 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; }); - it("should throw an error if no client key half is provided", async () => { - const userId = "test" as UserId; - const unlockKey = "testUnlockKey"; - - await expect(sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey)).rejects.toThrow( - "No client key half provided for user", - ); - }); - it("should call the platform specific setBiometricKey method", async () => { const userId = "test" as UserId; - const unlockKey = "testUnlockKey"; - - (sut as any).clientKeyHalves.set(userId, "testKeyHalf"); await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey); - expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - unlockKey, - "testKeyHalf", - ); + expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(userId, unlockKey); }); }); @@ -358,9 +321,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const osBiometricsService = mock(); (sut as any).osBiometricsService = osBiometricsService; @@ -369,10 +333,7 @@ describe("MainBiometricsService", function () { await sut.deleteBiometricUnlockKeyForUser(userId); - expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith( - "Bitwarden_biometric", - `${userId}_user_biometric`, - ); + expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(userId); }); }); @@ -384,9 +345,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); }); @@ -413,9 +375,10 @@ describe("MainBiometricsService", function () { i18nService, windowMain, logService, - messagingService, process.platform, biometricStateService, + encryptService, + cryptoFunctionService, ); const shouldAutoPrompt = await sut.getShouldAutopromptNow(); diff --git a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts index cf80fa5f7f3..a6a0e532655 100644 --- a/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/main-biometrics.service.ts @@ -1,6 +1,7 @@ +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; @@ -13,16 +14,16 @@ import { OsBiometricService } from "./os-biometrics.service"; export class MainBiometricsService extends DesktopBiometricsService { private osBiometricsService: OsBiometricService; - private clientKeyHalves = new Map(); private shouldAutoPrompt = true; constructor( private i18nService: I18nService, private windowMain: WindowMain, private logService: LogService, - private messagingService: MessagingService, - private platform: NodeJS.Platform, + platform: NodeJS.Platform, private biometricStateService: BiometricStateService, + private encryptService: EncryptService, + private cryptoFunctionService: CryptoFunctionService, ) { super(); if (platform === "win32") { @@ -32,6 +33,9 @@ export class MainBiometricsService extends DesktopBiometricsService { this.i18nService, this.windowMain, this.logService, + this.biometricStateService, + this.encryptService, + this.cryptoFunctionService, ); } else if (platform === "darwin") { // eslint-disable-next-line @@ -40,7 +44,11 @@ export class MainBiometricsService extends DesktopBiometricsService { } else if (platform === "linux") { // eslint-disable-next-line const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default; - this.osBiometricsService = new OsBiometricsServiceLinux(this.i18nService, this.windowMain); + this.osBiometricsService = new OsBiometricsServiceLinux( + this.biometricStateService, + this.encryptService, + this.cryptoFunctionService, + ); } else { throw new Error("Unsupported platform"); } @@ -55,11 +63,11 @@ export class MainBiometricsService extends DesktopBiometricsService { * @returns the status of the biometrics of the platform */ async getBiometricsStatus(): Promise { - if (!(await this.osBiometricsService.osSupportsBiometric())) { + if (!(await this.osBiometricsService.supportsBiometrics())) { return BiometricsStatus.HardwareUnavailable; } else { - if (await this.osBiometricsService.osBiometricsNeedsSetup()) { - if (await this.osBiometricsService.osBiometricsCanAutoSetup()) { + if (await this.osBiometricsService.needsSetup()) { + if (await this.osBiometricsService.canAutoSetup()) { return BiometricsStatus.AutoSetupNeeded; } else { return BiometricsStatus.ManualSetupNeeded; @@ -80,20 +88,12 @@ export class MainBiometricsService extends DesktopBiometricsService { if (!(await this.biometricStateService.getBiometricUnlockEnabled(userId))) { return BiometricsStatus.NotEnabledLocally; } - const platformStatus = await this.getBiometricsStatus(); if (!(platformStatus === BiometricsStatus.Available)) { return platformStatus; } - const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); - const clientKeyHalfB64 = this.clientKeyHalves.get(userId); - const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64; - if (!clientKeyHalfSatisfied) { - return BiometricsStatus.UnlockNeeded; - } - - return BiometricsStatus.Available; + return await this.osBiometricsService.getBiometricsFirstUnlockStatusForUser(userId); } async authenticateBiometric(): Promise { @@ -101,11 +101,7 @@ export class MainBiometricsService extends DesktopBiometricsService { } async setupBiometrics(): Promise { - return await this.osBiometricsService.osBiometricsSetup(); - } - - async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise { - this.clientKeyHalves.set(userId, value); + return await this.osBiometricsService.runSetup(); } async authenticateWithBiometrics(): Promise { @@ -113,43 +109,23 @@ export class MainBiometricsService extends DesktopBiometricsService { } async unlockWithBiometricsForUser(userId: UserId): Promise { - const biometricKey = await this.osBiometricsService.getBiometricKey( - "Bitwarden_biometric", - `${userId}_user_biometric`, - this.clientKeyHalves.get(userId) ?? undefined, - ); - if (biometricKey == null) { - return null; - } - - return SymmetricCryptoKey.fromString(biometricKey) as UserKey; + return (await this.osBiometricsService.getBiometricKey(userId)) as UserKey; } - async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise { - const service = "Bitwarden_biometric"; - const storageKey = `${userId}_user_biometric`; - if (!this.clientKeyHalves.has(userId)) { - throw new Error("No client key half provided for user"); - } - - return await this.osBiometricsService.setBiometricKey( - service, - storageKey, - value, - this.clientKeyHalves.get(userId) ?? undefined, - ); + async setBiometricProtectedUnlockKeyForUser( + userId: UserId, + key: SymmetricCryptoKey, + ): Promise { + return await this.osBiometricsService.setBiometricKey(userId, key); } async deleteBiometricUnlockKeyForUser(userId: UserId): Promise { - return await this.osBiometricsService.deleteBiometricKey( - "Bitwarden_biometric", - `${userId}_user_biometric`, - ); + return await this.osBiometricsService.deleteBiometricKey(userId); } /** * Set whether to auto-prompt the user for biometric unlock; this can be used to prevent auto-prompting being initiated by a process reload. - * Reasons for enabling auto prompt include: Starting the app, un-minimizing the app, manually account switching + * Reasons for enabling auto-prompt include: Starting the app, un-minimizing the app, manually account switching * @param value Whether to auto-prompt the user for biometric unlock */ async setShouldAutopromptNow(value: boolean): Promise { diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts index fb150f2a653..8d3c8e9795f 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-linux.service.ts @@ -1,10 +1,14 @@ import { spawn } from "child_process"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; import { biometrics, passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; -import { WindowMain } from "../../main/window.main"; import { isFlatpak, isLinux, isSnapStore } from "../../utils"; import { OsBiometricService } from "./os-biometrics.service"; @@ -28,59 +32,62 @@ const polkitPolicy = ` const policyFileName = "com.bitwarden.Bitwarden.policy"; const policyPath = "/usr/share/polkit-1/actions/"; +const SERVICE = "Bitwarden_biometric"; +function getLookupKeyForUser(userId: UserId): string { + return `${userId}_user_biometric`; +} + export default class OsBiometricsServiceLinux implements OsBiometricService { constructor( - private i18nservice: I18nService, - private windowMain: WindowMain, + private biometricStateService: BiometricStateService, + private encryptService: EncryptService, + private cryptoFunctionService: CryptoFunctionService, ) {} private _iv: string | null = null; // Use getKeyMaterial helper instead of direct access private _osKeyHalf: string | null = null; + private clientKeyHalves = new Map(); - async setBiometricKey( - service: string, - key: string, - value: string, - clientKeyPartB64: string | undefined, - ): Promise { + async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { + const clientKeyPartB64 = Utils.fromBufferToB64( + await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key), + ); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); await biometrics.setBiometricSecret( - service, - key, - value, + SERVICE, + getLookupKeyForUser(userId), + key.toBase64(), storageDetails.key_material, storageDetails.ivB64, ); } - async deleteBiometricKey(service: string, key: string): Promise { - await passwords.deletePassword(service, key); + async deleteBiometricKey(userId: UserId): Promise { + await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId)); } - async getBiometricKey( - service: string, - storageKey: string, - clientKeyPartB64: string | undefined, - ): Promise { + async getBiometricKey(userId: UserId): Promise { const success = await this.authenticateBiometric(); if (!success) { throw new Error("Biometric authentication failed"); } - const value = await passwords.getPassword(service, storageKey); + const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId)); if (value == null || value == "") { return null; } else { + const clientKeyHalf = this.clientKeyHalves.get(userId); + const clientKeyPartB64 = Utils.fromBufferToB64(clientKeyHalf); const encValue = new EncString(value); this.setIv(encValue.iv); const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); const storedValue = await biometrics.getBiometricSecret( - service, - storageKey, + SERVICE, + getLookupKeyForUser(userId), storageDetails.key_material, ); - return storedValue; + return SymmetricCryptoKey.fromString(storedValue); } } @@ -89,7 +96,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return await biometrics.prompt(hwnd, ""); } - async osSupportsBiometric(): Promise { + async supportsBiometrics(): Promise { // We assume all linux distros have some polkit implementation // that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup. // Snap does not have access at the moment to polkit @@ -99,7 +106,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return await passwords.isAvailable(); } - async osBiometricsNeedsSetup(): Promise { + async needsSetup(): Promise { if (isSnapStore()) { return false; } @@ -108,7 +115,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return !(await biometrics.available()); } - async osBiometricsCanAutoSetup(): Promise { + async canAutoSetup(): Promise { // We cannot auto setup on snap or flatpak since the filesystem is sandboxed. // The user needs to manually set up the polkit policy outside of the sandbox // since we allow access to polkit via dbus for the sandboxed clients, the authentication works from @@ -116,7 +123,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { return isLinux() && !isSnapStore() && !isFlatpak(); } - async osBiometricsSetup(): Promise { + async runSetup(): Promise { const process = spawn("pkexec", [ "bash", "-c", @@ -165,4 +172,46 @@ export default class OsBiometricsServiceLinux implements OsBiometricService { ivB64: this._iv, }; } + + private async getOrCreateBiometricEncryptionClientKeyHalf( + userId: UserId, + key: SymmetricCryptoKey, + ): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { + return null; + } + + if (this.clientKeyHalves.has(userId)) { + return this.clientKeyHalves.get(userId) || null; + } + + // Retrieve existing key half if it exists + let clientKeyHalf: Uint8Array | null = null; + const encryptedClientKeyHalf = + await this.biometricStateService.getEncryptedClientKeyHalf(userId); + if (encryptedClientKeyHalf != null) { + clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key); + } + if (clientKeyHalf == null) { + // Set a key half if it doesn't exist + const keyBytes = await this.cryptoFunctionService.randomBytes(32); + const encKey = await this.encryptService.encryptBytes(keyBytes, key); + await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); + } + + this.clientKeyHalves.set(userId, clientKeyHalf); + + return clientKeyHalf; + } + + async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + const clientKeyHalfB64 = this.clientKeyHalves.get(userId); + const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64; + if (!clientKeyHalfSatisfied) { + return BiometricsStatus.UnlockNeeded; + } + return BiometricsStatus.Available; + } } diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts index e361084726a..004495b6da9 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-mac.service.ts @@ -1,14 +1,22 @@ import { systemPreferences } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; import { passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus } from "@bitwarden/key-management"; import { OsBiometricService } from "./os-biometrics.service"; +const SERVICE = "Bitwarden_biometric"; +function getLookupKeyForUser(userId: UserId): string { + return `${userId}_user_biometric`; +} + export default class OsBiometricsServiceMac implements OsBiometricService { constructor(private i18nservice: I18nService) {} - async osSupportsBiometric(): Promise { + async supportsBiometrics(): Promise { return systemPreferences.canPromptTouchID(); } @@ -21,44 +29,52 @@ export default class OsBiometricsServiceMac implements OsBiometricService { } } - async getBiometricKey(service: string, key: string): Promise { + async getBiometricKey(userId: UserId): Promise { const success = await this.authenticateBiometric(); if (!success) { throw new Error("Biometric authentication failed"); } + const keyB64 = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId)); + if (keyB64 == null) { + return null; + } - return await passwords.getPassword(service, key); + return SymmetricCryptoKey.fromString(keyB64); } - async setBiometricKey(service: string, key: string, value: string): Promise { - if (await this.valueUpToDate(service, key, value)) { + async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { + if (await this.valueUpToDate(userId, key)) { return; } - return await passwords.setPassword(service, key, value); + return await passwords.setPassword(SERVICE, getLookupKeyForUser(userId), key.toBase64()); } - async deleteBiometricKey(service: string, key: string): Promise { - return await passwords.deletePassword(service, key); + async deleteBiometricKey(user: UserId): Promise { + return await passwords.deletePassword(SERVICE, getLookupKeyForUser(user)); } - private async valueUpToDate(service: string, key: string, value: string): Promise { + private async valueUpToDate(user: UserId, key: SymmetricCryptoKey): Promise { try { - const existing = await passwords.getPassword(service, key); - return existing === value; + const existing = await passwords.getPassword(SERVICE, getLookupKeyForUser(user)); + return existing === key.toBase64(); } catch { return false; } } - async osBiometricsNeedsSetup() { + async needsSetup() { return false; } - async osBiometricsCanAutoSetup(): Promise { + async canAutoSetup(): Promise { return false; } - async osBiometricsSetup(): Promise {} + async runSetup(): Promise {} + + async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { + return BiometricsStatus.Available; + } } diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts new file mode 100644 index 00000000000..d0fd8682f2a --- /dev/null +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.spec.ts @@ -0,0 +1,143 @@ +import { mock } from "jest-mock-extended"; + +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; + +import OsBiometricsServiceWindows from "./os-biometrics-windows.service"; + +jest.mock("@bitwarden/desktop-napi", () => ({ + biometrics: { + available: jest.fn(), + setBiometricSecret: jest.fn(), + getBiometricSecret: jest.fn(), + deriveKeyMaterial: jest.fn(), + prompt: jest.fn(), + }, + passwords: { + getPassword: jest.fn(), + deletePassword: jest.fn(), + }, +})); + +describe("OsBiometricsServiceWindows", () => { + let service: OsBiometricsServiceWindows; + let biometricStateService: BiometricStateService; + + beforeEach(() => { + const i18nService = mock(); + const logService = mock(); + biometricStateService = mock(); + const encryptionService = mock(); + const cryptoFunctionService = mock(); + service = new OsBiometricsServiceWindows( + i18nService, + null, + logService, + biometricStateService, + encryptionService, + cryptoFunctionService, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("getBiometricsFirstUnlockStatusForUser", () => { + const userId = "test-user-id" as UserId; + it("should return Available when requirePasswordOnRestart is false", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.Available); + }); + it("should return Available when requirePasswordOnRestart is true and client key half is set", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + (service as any).clientKeyHalves = new Map(); + (service as any).clientKeyHalves.set(userId, new Uint8Array([1, 2, 3, 4])); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.Available); + }); + it("should return UnlockNeeded when requirePasswordOnRestart is true and client key half is not set", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + (service as any).clientKeyHalves = new Map(); + const result = await service.getBiometricsFirstUnlockStatusForUser(userId); + expect(result).toBe(BiometricsStatus.UnlockNeeded); + }); + }); + + describe("getOrCreateBiometricEncryptionClientKeyHalf", () => { + const userId = "test-user-id" as UserId; + const key = new SymmetricCryptoKey(new Uint8Array(64)); + let encryptionService: EncryptService; + let cryptoFunctionService: CryptoFunctionService; + + beforeEach(() => { + encryptionService = mock(); + cryptoFunctionService = mock(); + service = new OsBiometricsServiceWindows( + mock(), + null, + mock(), + biometricStateService, + encryptionService, + cryptoFunctionService, + ); + }); + + it("should return null if getRequirePasswordOnRestart is false", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false); + const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); + expect(result).toBeNull(); + }); + + it("should return cached key half if already present", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + const cachedKeyHalf = new Uint8Array([10, 20, 30]); + (service as any).clientKeyHalves.set(userId.toString(), cachedKeyHalf); + const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); + expect(result).toBe(cachedKeyHalf); + }); + + it("should decrypt and return existing encrypted client key half", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + biometricStateService.getEncryptedClientKeyHalf = jest + .fn() + .mockResolvedValue(new Uint8Array([1, 2, 3])); + const decrypted = new Uint8Array([4, 5, 6]); + encryptionService.decryptBytes = jest.fn().mockResolvedValue(decrypted); + + const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); + + expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(userId); + expect(encryptionService.decryptBytes).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]), key); + expect(result).toEqual(decrypted); + expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(decrypted); + }); + + it("should generate, encrypt, store, and cache a new key half if none exists", async () => { + biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true); + biometricStateService.getEncryptedClientKeyHalf = jest.fn().mockResolvedValue(null); + const randomBytes = new Uint8Array([7, 8, 9]); + cryptoFunctionService.randomBytes = jest.fn().mockResolvedValue(randomBytes); + const encrypted = new Uint8Array([10, 11, 12]); + encryptionService.encryptBytes = jest.fn().mockResolvedValue(encrypted); + biometricStateService.setEncryptedClientKeyHalf = jest.fn().mockResolvedValue(undefined); + + const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); + + expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32); + expect(encryptionService.encryptBytes).toHaveBeenCalledWith(randomBytes, key); + expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith( + encrypted, + userId, + ); + expect(result).toBeNull(); + expect((service as any).clientKeyHalves.get(userId.toString())).toBeNull(); + }); + }); +}); diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts index 53647549295..dc4f8674d7f 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics-windows.service.ts @@ -1,10 +1,14 @@ +import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; +import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; import { biometrics, passwords } from "@bitwarden/desktop-napi"; +import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management"; import { WindowMain } from "../../main/window.main"; @@ -13,87 +17,107 @@ import { OsBiometricService } from "./os-biometrics.service"; const KEY_WITNESS_SUFFIX = "_witness"; const WITNESS_VALUE = "known key"; +const SERVICE = "Bitwarden_biometric"; +function getLookupKeyForUser(userId: UserId): string { + return `${userId}_user_biometric`; +} + export default class OsBiometricsServiceWindows implements OsBiometricService { // Use set helper method instead of direct access private _iv: string | null = null; // Use getKeyMaterial helper instead of direct access private _osKeyHalf: string | null = null; + private clientKeyHalves = new Map(); constructor( private i18nService: I18nService, private windowMain: WindowMain, private logService: LogService, + private biometricStateService: BiometricStateService, + private encryptService: EncryptService, + private cryptoFunctionService: CryptoFunctionService, ) {} - async osSupportsBiometric(): Promise { + async supportsBiometrics(): Promise { return await biometrics.available(); } - async getBiometricKey( - service: string, - storageKey: string, - clientKeyHalfB64: string, - ): Promise { - const value = await passwords.getPassword(service, storageKey); + async getBiometricKey(userId: UserId): Promise { + const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId)); + let clientKeyHalfB64: string | null = null; + if (this.clientKeyHalves.has(userId)) { + clientKeyHalfB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId)); + } if (value == null || value == "") { return null; } else if (!EncString.isSerializedEncString(value)) { // Update to format encrypted with client key half const storageDetails = await this.getStorageDetails({ - clientKeyHalfB64, + clientKeyHalfB64: clientKeyHalfB64, }); await biometrics.setBiometricSecret( - service, - storageKey, + SERVICE, + getLookupKeyForUser(userId), value, storageDetails.key_material, storageDetails.ivB64, ); - return value; + return SymmetricCryptoKey.fromString(value); } else { const encValue = new EncString(value); this.setIv(encValue.iv); const storageDetails = await this.getStorageDetails({ - clientKeyHalfB64, + clientKeyHalfB64: clientKeyHalfB64, }); - return await biometrics.getBiometricSecret(service, storageKey, storageDetails.key_material); + return SymmetricCryptoKey.fromString( + await biometrics.getBiometricSecret( + SERVICE, + getLookupKeyForUser(userId), + storageDetails.key_material, + ), + ); } } - async setBiometricKey( - service: string, - storageKey: string, - value: string, - clientKeyPartB64: string | undefined, - ): Promise { - const parsedValue = SymmetricCryptoKey.fromString(value); - if (await this.valueUpToDate({ value: parsedValue, clientKeyPartB64, service, storageKey })) { + async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise { + const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key); + + if ( + await this.valueUpToDate({ + value: key, + clientKeyPartB64: Utils.fromBufferToB64(clientKeyHalf), + service: SERVICE, + storageKey: getLookupKeyForUser(userId), + }) + ) { return; } - const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); + const storageDetails = await this.getStorageDetails({ + clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf), + }); const storedValue = await biometrics.setBiometricSecret( - service, - storageKey, - value, + SERVICE, + getLookupKeyForUser(userId), + key.toBase64(), storageDetails.key_material, storageDetails.ivB64, ); const parsedStoredValue = new EncString(storedValue); await this.storeValueWitness( - parsedValue, + key, parsedStoredValue, - service, - storageKey, - clientKeyPartB64, + SERVICE, + getLookupKeyForUser(userId), + Utils.fromBufferToB64(clientKeyHalf), ); } - async deleteBiometricKey(service: string, key: string): Promise { - await passwords.deletePassword(service, key); - await passwords.deletePassword(service, key + KEY_WITNESS_SUFFIX); + async deleteBiometricKey(userId: UserId): Promise { + await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId)); + await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId) + KEY_WITNESS_SUFFIX); } async authenticateBiometric(): Promise { @@ -240,13 +264,58 @@ export default class OsBiometricsServiceWindows implements OsBiometricService { return result; } - async osBiometricsNeedsSetup() { + async needsSetup() { return false; } - async osBiometricsCanAutoSetup(): Promise { + async canAutoSetup(): Promise { return false; } - async osBiometricsSetup(): Promise {} + async runSetup(): Promise {} + + async getOrCreateBiometricEncryptionClientKeyHalf( + userId: UserId, + key: SymmetricCryptoKey, + ): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { + return null; + } + + if (this.clientKeyHalves.has(userId)) { + return this.clientKeyHalves.get(userId); + } + + // Retrieve existing key half if it exists + let clientKeyHalf: Uint8Array | null = null; + const encryptedClientKeyHalf = + await this.biometricStateService.getEncryptedClientKeyHalf(userId); + if (encryptedClientKeyHalf != null) { + clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key); + } + if (clientKeyHalf == null) { + // Set a key half if it doesn't exist + const keyBytes = await this.cryptoFunctionService.randomBytes(32); + const encKey = await this.encryptService.encryptBytes(keyBytes, key); + await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); + } + + this.clientKeyHalves.set(userId, clientKeyHalf); + + return clientKeyHalf; + } + + async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise { + const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); + if (!requireClientKeyHalf) { + return BiometricsStatus.Available; + } + + if (this.clientKeyHalves.has(userId)) { + return BiometricsStatus.Available; + } else { + return BiometricsStatus.UnlockNeeded; + } + } } diff --git a/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts index f5132200149..63e0527c034 100644 --- a/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/os-biometrics.service.ts @@ -1,32 +1,28 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus } from "@bitwarden/key-management"; + export interface OsBiometricService { - osSupportsBiometric(): Promise; + supportsBiometrics(): Promise; /** * Check whether support for biometric unlock requires setup. This can be automatic or manual. * * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place) */ - osBiometricsNeedsSetup: () => Promise; + needsSetup(): Promise; /** * Check whether biometrics can be automatically setup, or requires user interaction. * * @returns true if biometrics support can be automatically setup, false if it requires user interaction. */ - osBiometricsCanAutoSetup: () => Promise; + canAutoSetup(): Promise; /** * Starts automatic biometric setup, which places the required configuration files / changes the required settings. */ - osBiometricsSetup: () => Promise; + runSetup(): Promise; authenticateBiometric(): Promise; - getBiometricKey( - service: string, - key: string, - clientKeyHalfB64: string | undefined, - ): Promise; - setBiometricKey( - service: string, - key: string, - value: string, - clientKeyHalfB64: string | undefined, - ): Promise; - deleteBiometricKey(service: string, key: string): Promise; + getBiometricKey(userId: UserId): Promise; + setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise; + deleteBiometricKey(userId: UserId): Promise; + getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise; } diff --git a/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts index 1404d65ae51..c7ed88d390f 100644 --- a/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts +++ b/apps/desktop/src/key-management/biometrics/renderer-biometrics.service.ts @@ -34,8 +34,14 @@ export class RendererBiometricsService extends DesktopBiometricsService { return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id); } - async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise { - return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(userId, value); + async setBiometricProtectedUnlockKeyForUser( + userId: UserId, + value: SymmetricCryptoKey, + ): Promise { + return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser( + userId, + value.toBase64(), + ); } async deleteBiometricUnlockKeyForUser(userId: UserId): Promise { @@ -46,10 +52,6 @@ export class RendererBiometricsService extends DesktopBiometricsService { return await ipc.keyManagement.biometric.setupBiometrics(); } - async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise { - return await ipc.keyManagement.biometric.setClientKeyHalf(userId, value); - } - async getShouldAutopromptNow(): Promise { return await ipc.keyManagement.biometric.getShouldAutoprompt(); } diff --git a/apps/desktop/src/key-management/electron-key.service.spec.ts b/apps/desktop/src/key-management/electron-key.service.spec.ts index 7a0464f5e27..730ad7e4652 100644 --- a/apps/desktop/src/key-management/electron-key.service.spec.ts +++ b/apps/desktop/src/key-management/electron-key.service.spec.ts @@ -9,14 +9,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management"; import { - makeEncString, - makeStaticByteArray, makeSymmetricCryptoKey, FakeAccountService, mockAccountServiceWith, @@ -80,7 +77,6 @@ describe("ElectronKeyService", () => { await keyService.setUserKey(userKey, mockUserId); - expect(biometricService.setClientKeyHalfForUser).not.toHaveBeenCalled(); expect(biometricService.setBiometricProtectedUnlockKeyForUser).not.toHaveBeenCalled(); expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId); @@ -96,14 +92,12 @@ describe("ElectronKeyService", () => { await keyService.setUserKey(userKey, mockUserId); - expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(mockUserId, null); expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( mockUserId, - userKey.keyB64, + userKey, ); expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId); - expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(mockUserId); }); describe("require password on start enabled", () => { @@ -111,73 +105,11 @@ describe("ElectronKeyService", () => { biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true); }); - it("sets new biometric client key half and biometric unlock key when no biometric client key half stored", async () => { - const clientKeyHalfBytes = makeStaticByteArray(32); - const clientKeyHalf = Utils.fromBufferToUtf8(clientKeyHalfBytes); - const encryptedClientKeyHalf = makeEncString(); - biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(null); - cryptoFunctionService.randomBytes.mockResolvedValue( - clientKeyHalfBytes.buffer as CsprngArray, - ); - encryptService.encryptString.mockResolvedValue(encryptedClientKeyHalf); - + it("sets biometric key", async () => { await keyService.setUserKey(userKey, mockUserId); - expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith( - mockUserId, - clientKeyHalf, - ); expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( mockUserId, - userKey.keyB64, - ); - expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith( - encryptedClientKeyHalf, - mockUserId, - ); - expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith( - mockUserId, - ); - expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32); - expect(encryptService.encryptString).toHaveBeenCalledWith(clientKeyHalf, userKey); - }); - - it("sets decrypted biometric client key half and biometric unlock key when existing biometric client key half stored", async () => { - const encryptedClientKeyHalf = makeEncString(); - const clientKeyHalf = Utils.fromBufferToUtf8(makeStaticByteArray(32)); - biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue( - encryptedClientKeyHalf, - ); - encryptService.decryptString.mockResolvedValue(clientKeyHalf); - - await keyService.setUserKey(userKey, mockUserId); - - expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith( - mockUserId, - clientKeyHalf, - ); - expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith( - mockUserId, - userKey.keyB64, - ); - expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled(); - expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith( - mockUserId, - ); - expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith( - mockUserId, - ); - expect(encryptService.decryptString).toHaveBeenCalledWith( - encryptedClientKeyHalf, userKey, ); }); diff --git a/apps/desktop/src/key-management/electron-key.service.ts b/apps/desktop/src/key-management/electron-key.service.ts index d31e717e7a5..8a6fbfa085f 100644 --- a/apps/desktop/src/key-management/electron-key.service.ts +++ b/apps/desktop/src/key-management/electron-key.service.ts @@ -8,9 +8,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; -import { CsprngString } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { @@ -77,10 +75,7 @@ export class ElectronKeyService extends DefaultKeyService { } private async storeBiometricsProtectedUserKey(userKey: UserKey, userId: UserId): Promise { - // May resolve to null, in which case no client key have is required - const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId); - await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf); - await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64); + await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey); } protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise { @@ -91,34 +86,4 @@ export class ElectronKeyService extends DefaultKeyService { await this.biometricService.deleteBiometricUnlockKeyForUser(userId); await super.clearAllStoredUserKeys(userId); } - - private async getBiometricEncryptionClientKeyHalf( - userKey: UserKey, - userId: UserId, - ): Promise { - const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); - if (!requireClientKeyHalf) { - return null; - } - - // Retrieve existing key half if it exists - let clientKeyHalf: CsprngString | null = null; - const encryptedClientKeyHalf = - await this.biometricStateService.getEncryptedClientKeyHalf(userId); - if (encryptedClientKeyHalf != null) { - clientKeyHalf = (await this.encryptService.decryptString( - encryptedClientKeyHalf, - userKey, - )) as CsprngString; - } - if (clientKeyHalf == null) { - // Set a key half if it doesn't exist - const keyBytes = await this.cryptoFunctionService.randomBytes(32); - clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString; - const encKey = await this.encryptService.encryptString(clientKeyHalf, userKey); - await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId); - } - - return clientKeyHalf; - } } diff --git a/apps/desktop/src/key-management/preload.ts b/apps/desktop/src/key-management/preload.ts index 3e90c27ab03..7f8576b8472 100644 --- a/apps/desktop/src/key-management/preload.ts +++ b/apps/desktop/src/key-management/preload.ts @@ -25,12 +25,13 @@ const biometric = { action: BiometricAction.GetStatusForUser, userId: userId, } satisfies BiometricMessage), - setBiometricProtectedUnlockKeyForUser: (userId: string, value: string): Promise => - ipcRenderer.invoke("biometric", { + setBiometricProtectedUnlockKeyForUser: (userId: string, keyB64: string): Promise => { + return ipcRenderer.invoke("biometric", { action: BiometricAction.SetKeyForUser, userId: userId, - key: value, - } satisfies BiometricMessage), + key: keyB64, + } satisfies BiometricMessage); + }, deleteBiometricUnlockKeyForUser: (userId: string): Promise => ipcRenderer.invoke("biometric", { action: BiometricAction.RemoveKeyForUser, @@ -40,12 +41,6 @@ const biometric = { ipcRenderer.invoke("biometric", { action: BiometricAction.Setup, } satisfies BiometricMessage), - setClientKeyHalf: (userId: string, value: string | null): Promise => - ipcRenderer.invoke("biometric", { - action: BiometricAction.SetClientKeyHalf, - userId: userId, - key: value, - } satisfies BiometricMessage), getShouldAutoprompt: (): Promise => ipcRenderer.invoke("biometric", { action: BiometricAction.GetShouldAutoprompt, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 5e0ea7f9fac..7d97805e9be 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -10,6 +10,7 @@ import { Subject, firstValueFrom } from "rxjs"; import { SsoUrlService } from "@bitwarden/auth/common"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { ClientType } from "@bitwarden/common/enums"; +import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation"; import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service"; import { Message, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- For dependency creation @@ -187,14 +188,19 @@ export class Main { this.desktopSettingsService = new DesktopSettingsService(stateProvider); const biometricStateService = new DefaultBiometricStateService(stateProvider); - + const encryptService = new EncryptServiceImplementation( + this.mainCryptoFunctionService, + this.logService, + true, + ); this.biometricsService = new MainBiometricsService( this.i18nService, this.windowMain, this.logService, - this.messagingService, process.platform, biometricStateService, + encryptService, + this.mainCryptoFunctionService, ); this.windowMain = new WindowMain( diff --git a/apps/desktop/src/types/biometric-message.ts b/apps/desktop/src/types/biometric-message.ts index 7616b265005..9711b49496d 100644 --- a/apps/desktop/src/types/biometric-message.ts +++ b/apps/desktop/src/types/biometric-message.ts @@ -9,8 +9,6 @@ export enum BiometricAction { SetKeyForUser = "setKeyForUser", RemoveKeyForUser = "removeKeyForUser", - SetClientKeyHalf = "setClientKeyHalf", - Setup = "setup", GetShouldAutoprompt = "getShouldAutoprompt", @@ -18,21 +16,13 @@ export enum BiometricAction { } export type BiometricMessage = - | { - action: BiometricAction.SetClientKeyHalf; - userId: string; - key: string | null; - } | { action: BiometricAction.SetKeyForUser; userId: string; key: string; } | { - action: Exclude< - BiometricAction, - BiometricAction.SetClientKeyHalf | BiometricAction.SetKeyForUser - >; + action: Exclude; userId?: string; data?: any; }; From 674886a28b1e9d338943499019d5641a8f887777 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 17 Jun 2025 15:42:45 +0200 Subject: [PATCH 04/31] [PM-22207] Remove wasm fallback for browser (#15003) We currently ship a transpiled version of the WebAssembly module to maintain backwards compataibility in case someone can't run the WebAssembly bundle. The filesize of this fallback now exceeds 4mb, but Firefox only supports javascript files 4mb and smaller in extensions. This resulted in us being unable to publish the latest version. This PR removes the fallback. --- apps/browser/src/_locales/en/messages.json | 4 ++ .../services/sdk/browser-sdk-load.service.ts | 10 ++--- .../src/platform/services/sdk/fallback.ts | 8 ---- apps/browser/src/popup/app.component.ts | 45 ++++++++++++++++--- apps/browser/src/popup/app.module.ts | 4 ++ 5 files changed, 51 insertions(+), 20 deletions(-) delete mode 100644 apps/browser/src/platform/services/sdk/fallback.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 3a8c7f14bc0..e8834b3ffdb 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5403,5 +5403,9 @@ }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." + }, + "wasmNotSupported": { + "message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.", + "description": "'WebAssembly' is a technical term and should not be translated." } } diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts index 409ff0dea06..3ad6dc2583d 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts @@ -35,9 +35,9 @@ if (BrowserApi.isManifestVersion(3)) { console.info("WebAssembly is supported in this environment"); loadingPromise = import("./wasm"); } else { - // eslint-disable-next-line no-console - console.info("WebAssembly is not supported in this environment"); - loadingPromise = import("./fallback"); + loadingPromise = new Promise((_, reject) => { + reject(new Error("WebAssembly is not supported in this environment")); + }); } } @@ -51,9 +51,7 @@ async function importModule(): Promise { console.info("WebAssembly is supported in this environment"); await import("./wasm"); } else { - // eslint-disable-next-line no-console - console.info("WebAssembly is not supported in this environment"); - await import("./fallback"); + throw new Error("WebAssembly is not supported in this environment"); } // the wasm and fallback imports mutate globalThis to add the initSdk function diff --git a/apps/browser/src/platform/services/sdk/fallback.ts b/apps/browser/src/platform/services/sdk/fallback.ts deleted file mode 100644 index cee3598feda..00000000000 --- a/apps/browser/src/platform/services/sdk/fallback.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as sdk from "@bitwarden/sdk-internal"; -import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js"; - -import { GlobalWithWasmInit } from "./browser-sdk-load.service"; - -(globalThis as GlobalWithWasmInit).initSdk = () => { - (sdk as any).init(wasm); -}; diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index b6d3615af94..6a26476de43 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -11,7 +11,17 @@ import { } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, map } from "rxjs"; +import { + Subject, + takeUntil, + firstValueFrom, + concatMap, + filter, + tap, + catchError, + of, + map, +} from "rxjs"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; @@ -23,6 +33,7 @@ import { AnimationControlService } from "@bitwarden/common/platform/abstractions import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; @@ -48,23 +59,45 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn styles: [], animations: [routerTransition], template: ` -
- -
- + @if (showSdkWarning | async) { +
+ + {{ "wasmNotSupported" | i18n }} + + {{ "learnMore" | i18n }} + + +
+ } @else { +
+ +
+ + } `, standalone: false, }) export class AppComponent implements OnInit, OnDestroy { private compactModeService = inject(PopupCompactModeService); + private sdkService = inject(SdkService); private lastActivity: Date; private activeUserId: UserId; - private recordActivitySubject = new Subject(); private routerAnimations = false; private destroy$ = new Subject(); + // Show a warning if the SDK is not available. + protected showSdkWarning = this.sdkService.client$.pipe( + map(() => false), + catchError(() => of(true)), + ); + constructor( private authService: AuthService, private i18nService: I18nService, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 8bea41da4d6..b400cb5eec8 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -20,6 +20,8 @@ import { ButtonModule, FormFieldModule, ToastModule, + CalloutModule, + LinkModule, } from "@bitwarden/components"; import { AccountComponent } from "../auth/popup/account-switching/account.component"; @@ -87,6 +89,8 @@ import "../platform/popup/locales"; CurrentAccountComponent, FormFieldModule, ExtensionAnonLayoutWrapperComponent, + CalloutModule, + LinkModule, ], declarations: [ AppComponent, From b8a1856fc6328e214feea61d6595e8255d754ca6 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 17 Jun 2025 11:05:14 -0400 Subject: [PATCH 05/31] [CL-696] un-revert "various drawer improvements" + bug fix (#14887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "[CL-622][CL-562][CL-621][CL-632] various drawer improvements …" This reverts commit 4b32d1f9ddf9958070ad5f6bc288991ef5106ecd. * fix virtual scroll: add .cdk-virtual-scrollable to scroll viewport target * remove references to main el * use directives instead of querySelector (#14950) * remove references to main el * wip * banish querySelector to the shadow realm * revert apps/ files * Add virtual scrolling docs Co-authored-by: Vicki League * add jsdoc * run eslint * fix skip links bug * Update libs/components/src/layout/layout.component.ts Co-authored-by: Vicki League * update tab handler * only run on tab * fix lint * fix virtual scroll issue due to Angular 19 upgrade (#15193) thanks Vicki --------- Co-authored-by: Vicki League --- .../manage/groups.component.html | 2 +- .../members/members.component.html | 2 +- .../organizations/members/members.module.ts | 2 + .../organizations/organization.module.ts | 3 + .../collection-dialog.component.ts | 2 +- .../device-management.component.spec.ts | 14 +- .../add-sponsorship-dialog.component.ts | 4 +- .../free-bitwarden-families.component.ts | 3 +- .../vault-items/vault-items.component.html | 2 +- .../vault-items/vault-items.module.ts | 3 +- .../vault-items/vault-items.stories.ts | 12 +- .../providers/manage/members.component.html | 2 +- .../providers/providers.module.ts | 3 +- .../clients/create-client-dialog.component.ts | 4 +- .../src/dialog/dialog.service.stories.ts | 66 ++++- libs/components/src/dialog/dialog.service.ts | 240 +++++++++++++----- .../src/dialog/dialog/dialog.component.html | 40 ++- .../src/dialog/dialog/dialog.component.ts | 37 ++- libs/components/src/dialog/dialogs.mdx | 3 + libs/components/src/dialog/index.ts | 2 +- .../src/drawer/drawer-body.component.ts | 18 +- .../components/src/drawer/drawer.component.ts | 4 +- libs/components/src/drawer/drawer.mdx | 2 + libs/components/src/drawer/drawer.service.ts | 20 ++ libs/components/src/layout/index.ts | 1 + .../src/layout/layout.component.html | 91 ++++--- .../components/src/layout/layout.component.ts | 49 +++- .../src/layout/scroll-layout.directive.ts | 98 +++++++ .../dialog-virtual-scroll-block.component.ts | 13 +- .../components/kitchen-sink-main.component.ts | 133 +++++----- .../kitchen-sink/kitchen-sink.stories.ts | 30 +-- .../src/stories/virtual-scrolling.mdx | 60 +++++ .../src/table/table-scroll.component.html | 2 +- .../src/table/table-scroll.component.ts | 5 +- libs/components/src/table/table.mdx | 12 +- libs/components/src/table/table.stories.ts | 68 +++-- .../components/src/utils/has-scrolled-from.ts | 41 +++ 37 files changed, 807 insertions(+), 286 deletions(-) create mode 100644 libs/components/src/drawer/drawer.service.ts create mode 100644 libs/components/src/layout/scroll-layout.directive.ts create mode 100644 libs/components/src/stories/virtual-scrolling.mdx create mode 100644 libs/components/src/utils/has-scrolled-from.ts diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.html b/apps/web/src/app/admin-console/organizations/manage/groups.component.html index caae23f500d..4518513ba7d 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.html @@ -22,7 +22,7 @@

{{ "noGroupsInList" | i18n }}

- + 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 610821cfd1b..962191021e8 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 @@ -75,7 +75,7 @@ - + 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 81697f8c845..98431758d2f 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 @@ -3,6 +3,7 @@ import { NgModule } from "@angular/core"; import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component"; import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; +import { ScrollLayoutDirective } from "@bitwarden/components"; import { LooseComponentsModule } from "../../../shared"; import { SharedOrganizationModule } from "../shared"; @@ -27,6 +28,7 @@ import { MembersComponent } from "./members.component"; PasswordCalloutComponent, ScrollingModule, PasswordStrengthV2Component, + ScrollLayoutDirective, ], declarations: [ BulkConfirmDialogComponent, diff --git a/apps/web/src/app/admin-console/organizations/organization.module.ts b/apps/web/src/app/admin-console/organizations/organization.module.ts index 459948d0f13..687361760c9 100644 --- a/apps/web/src/app/admin-console/organizations/organization.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization.module.ts @@ -1,6 +1,8 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { NgModule } from "@angular/core"; +import { ScrollLayoutDirective } from "@bitwarden/components"; + import { LooseComponentsModule } from "../../shared"; import { CoreOrganizationModule } from "./core"; @@ -18,6 +20,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; OrganizationsRoutingModule, LooseComponentsModule, ScrollingModule, + ScrollLayoutDirective, ], declarations: [GroupsComponent, GroupAddEditComponent], }) diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index e9865f14d54..8763b75ffca 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -591,5 +591,5 @@ export function openCollectionDialog( dialogService: DialogService, config: DialogConfig>, ) { - return dialogService.open(CollectionDialogComponent, config); + return dialogService.open(CollectionDialogComponent, config); } diff --git a/apps/web/src/app/auth/settings/security/device-management.component.spec.ts b/apps/web/src/app/auth/settings/security/device-management.component.spec.ts index 84c1dfcb63b..d86123f52be 100644 --- a/apps/web/src/app/auth/settings/security/device-management.component.spec.ts +++ b/apps/web/src/app/auth/settings/security/device-management.component.spec.ts @@ -9,7 +9,13 @@ import { DeviceType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; -import { DialogService, ToastService, TableModule, PopoverModule } from "@bitwarden/components"; +import { + DialogService, + ToastService, + TableModule, + PopoverModule, + LayoutComponent, +} from "@bitwarden/components"; import { SharedModule } from "../../../shared"; import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service"; @@ -115,6 +121,12 @@ describe("DeviceManagementComponent", () => { showError: jest.fn(), }, }, + { + provide: LayoutComponent, + useValue: { + mainContent: jest.fn(), + }, + }, ], }).compileComponents(); diff --git a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts index f3c01e41dbb..38ae39cabfe 100644 --- a/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts +++ b/apps/web/src/app/billing/members/add-sponsorship-dialog.component.ts @@ -1,4 +1,3 @@ -import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; import { AbstractControl, @@ -19,7 +18,10 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrgKey } from "@bitwarden/common/types/key"; import { + DialogRef, ButtonModule, + DialogConfig, + DIALOG_DATA, DialogModule, DialogService, FormFieldModule, diff --git a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts index 0e3b682104e..edd918ce059 100644 --- a/apps/web/src/app/billing/members/free-bitwarden-families.component.ts +++ b/apps/web/src/app/billing/members/free-bitwarden-families.component.ts @@ -1,4 +1,3 @@ -import { DialogRef } from "@angular/cdk/dialog"; import { formatDate } from "@angular/common"; import { Component, OnInit, signal } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -16,7 +15,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { StateProvider } from "@bitwarden/common/platform/state"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { DialogRef, DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component"; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 4b266ac5525..992c9c26bf3 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -1,4 +1,4 @@ - + diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.module.ts b/apps/web/src/app/vault/components/vault-items/vault-items.module.ts index e54a9c1141f..ab4f8bddb16 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.module.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { TableModule } from "@bitwarden/components"; +import { ScrollLayoutDirective, TableModule } from "@bitwarden/components"; import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections"; import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module"; @@ -26,6 +26,7 @@ import { VaultItemsComponent } from "./vault-items.component"; CollectionNameBadgeComponent, GroupBadgeModule, PipesModule, + ScrollLayoutDirective, ], declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent], exports: [VaultItemsComponent], diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index e2c6f204d72..62b53d71e84 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -2,7 +2,13 @@ // @ts-strict-ignore import { importProvidersFrom } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; +import { + applicationConfig, + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from "@storybook/angular"; import { BehaviorSubject, of } from "rxjs"; import { @@ -29,6 +35,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { LayoutComponent } from "@bitwarden/components"; import { RestrictedItemTypesService } from "@bitwarden/vault"; import { GroupView } from "../../../admin-console/organizations/core"; @@ -49,8 +56,9 @@ export default { title: "Web/Vault/Items", component: VaultItemsComponent, decorators: [ + componentWrapperDecorator((story) => `${story}`), moduleMetadata({ - imports: [VaultItemsModule, RouterModule], + imports: [VaultItemsModule, RouterModule, LayoutComponent], providers: [ { provide: EnvironmentService, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html index 66c42678442..f203b7a934a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/members.component.html @@ -55,7 +55,7 @@ > {{ "providerUsersNeedConfirmed" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 597acb0d4f0..01f1facfc15 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; import { FormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CardComponent, SearchModule } from "@bitwarden/components"; +import { CardComponent, ScrollLayoutDirective, SearchModule } from "@bitwarden/components"; import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component"; import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing"; import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component"; @@ -53,6 +53,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr ScrollingModule, VerifyBankAccountComponent, CardComponent, + ScrollLayoutDirective, PaymentComponent, ], declarations: [ diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts index e74682f64fe..c7d82c3ec09 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.ts @@ -1,4 +1,3 @@ -import { BasePortalOutlet } from "@angular/cdk/portal"; import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; @@ -33,8 +32,7 @@ export const openCreateClientDialog = ( dialogService: DialogService, dialogConfig: DialogConfig< CreateClientDialogParams, - DialogRef, - BasePortalOutlet + DialogRef >, ) => dialogService.open( diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index a9fe92ea4bf..7e2d8c62bb6 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -1,11 +1,17 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; -import { provideAnimations } from "@angular/platform-browser/animations"; -import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +import { NoopAnimationsModule, provideAnimations } from "@angular/platform-browser/animations"; +import { RouterTestingModule } from "@angular/router/testing"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; +import { getAllByRole, userEvent } from "@storybook/test"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ButtonModule } from "../button"; +import { IconButtonModule } from "../icon-button"; +import { LayoutComponent } from "../layout"; +import { SharedModule } from "../shared"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; import { DialogModule } from "./dialog.module"; @@ -16,7 +22,12 @@ interface Animal { } @Component({ - template: ``, + template: ` + + + + + `, imports: [ButtonModule], }) class StoryDialogComponent { @@ -29,6 +40,14 @@ class StoryDialogComponent { }, }); } + + openDrawer() { + this.dialogService.openDrawer(StoryDialogContentComponent, { + data: { + animal: "panda", + }, + }); + } } @Component({ @@ -64,7 +83,21 @@ export default { title: "Component Library/Dialogs/Service", component: StoryDialogComponent, decorators: [ + positionFixedWrapperDecorator(), moduleMetadata({ + declarations: [StoryDialogContentComponent], + imports: [ + SharedModule, + ButtonModule, + NoopAnimationsModule, + DialogModule, + IconButtonModule, + RouterTestingModule, + LayoutComponent, + ], + providers: [DialogService], + }), + applicationConfig({ providers: [ provideAnimations(), DialogService, @@ -73,7 +106,13 @@ export default { useFactory: () => { return new I18nMockService({ close: "Close", - loading: "Loading", + search: "Search", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", + toggleSideNavigation: "Toggle side navigation", + yes: "Yes", + no: "No", }); }, }, @@ -90,4 +129,21 @@ export default { type Story = StoryObj; -export const Default: Story = {}; +export const Default: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[0]; + await userEvent.click(button); + }, +}; + +/** Drawers must be a descendant of `bit-layout`. */ +export const Drawer: Story = { + play: async (context) => { + const canvas = context.canvasElement; + + const button = getAllByRole(canvas, "button")[1]; + await userEvent.click(button); + }, +}; diff --git a/libs/components/src/dialog/dialog.service.ts b/libs/components/src/dialog/dialog.service.ts index 83aaaff470e..409bf0a5b55 100644 --- a/libs/components/src/dialog/dialog.service.ts +++ b/libs/components/src/dialog/dialog.service.ts @@ -1,31 +1,25 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { - DEFAULT_DIALOG_CONFIG, - Dialog, - DialogConfig, - DialogRef, - DIALOG_SCROLL_STRATEGY, + Dialog as CdkDialog, + DialogConfig as CdkDialogConfig, + DialogRef as CdkDialogRefBase, + DIALOG_DATA, + DialogCloseOptions, } from "@angular/cdk/dialog"; -import { ComponentType, Overlay, OverlayContainer, ScrollStrategy } from "@angular/cdk/overlay"; -import { - Inject, - Injectable, - Injector, - OnDestroy, - Optional, - SkipSelf, - TemplateRef, -} from "@angular/core"; +import { ComponentType, ScrollStrategy } from "@angular/cdk/overlay"; +import { ComponentPortal, Portal } from "@angular/cdk/portal"; +import { Injectable, Injector, TemplateRef, inject } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; -import { filter, firstValueFrom, Subject, switchMap, takeUntil } from "rxjs"; +import { filter, firstValueFrom, map, Observable, Subject, switchMap } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DrawerService } from "../drawer/drawer.service"; + import { SimpleConfigurableDialogComponent } from "./simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component"; -import { SimpleDialogOptions, Translation } from "./simple-dialog/types"; +import { SimpleDialogOptions } from "./simple-dialog/types"; /** * The default `BlockScrollStrategy` does not work well with virtual scrolling. @@ -48,61 +42,163 @@ class CustomBlockScrollStrategy implements ScrollStrategy { detach() {} } +export abstract class DialogRef + implements Pick, "close" | "closed" | "disableClose" | "componentInstance"> +{ + abstract readonly isDrawer?: boolean; + + // --- From CdkDialogRef --- + abstract close(result?: R, options?: DialogCloseOptions): void; + abstract readonly closed: Observable; + abstract disableClose: boolean | undefined; + /** + * @deprecated + * Does not work with drawer dialogs. + **/ + abstract componentInstance: C | null; +} + +export type DialogConfig = Pick< + CdkDialogConfig, + "data" | "disableClose" | "ariaModal" | "positionStrategy" | "height" | "width" +>; + +class DrawerDialogRef implements DialogRef { + readonly isDrawer = true; + + private _closed = new Subject(); + closed = this._closed.asObservable(); + disableClose = false; + + /** The portal containing the drawer */ + portal?: Portal; + + constructor(private drawerService: DrawerService) {} + + close(result?: R, _options?: DialogCloseOptions): void { + if (this.disableClose) { + return; + } + this.drawerService.close(this.portal!); + this._closed.next(result); + this._closed.complete(); + } + + componentInstance: C | null = null; +} + +/** + * DialogRef that delegates functionality to the CDK implementation + **/ +export class CdkDialogRef implements DialogRef { + readonly isDrawer = false; + + /** This is not available until after construction, @see DialogService.open. */ + cdkDialogRefBase!: CdkDialogRefBase; + + // --- Delegated to CdkDialogRefBase --- + + close(result?: R, options?: DialogCloseOptions): void { + this.cdkDialogRefBase.close(result, options); + } + + get closed(): Observable { + return this.cdkDialogRefBase.closed; + } + + get disableClose(): boolean | undefined { + return this.cdkDialogRefBase.disableClose; + } + set disableClose(value: boolean | undefined) { + this.cdkDialogRefBase.disableClose = value; + } + + // Delegate the `componentInstance` property to the CDK DialogRef + get componentInstance(): C | null { + return this.cdkDialogRefBase.componentInstance; + } +} + @Injectable() -export class DialogService extends Dialog implements OnDestroy { - private _destroy$ = new Subject(); +export class DialogService { + private dialog = inject(CdkDialog); + private drawerService = inject(DrawerService); + private injector = inject(Injector); + private router = inject(Router, { optional: true }); + private authService = inject(AuthService, { optional: true }); + private i18nService = inject(I18nService); private backDropClasses = ["tw-fixed", "tw-bg-black", "tw-bg-opacity-30", "tw-inset-0"]; - private defaultScrollStrategy = new CustomBlockScrollStrategy(); + private activeDrawer: DrawerDialogRef | null = null; - constructor( - /** Parent class constructor */ - _overlay: Overlay, - _injector: Injector, - @Optional() @Inject(DEFAULT_DIALOG_CONFIG) _defaultOptions: DialogConfig, - @Optional() @SkipSelf() _parentDialog: Dialog, - _overlayContainer: OverlayContainer, - @Inject(DIALOG_SCROLL_STRATEGY) scrollStrategy: any, - - /** Not in parent class */ - @Optional() router: Router, - @Optional() authService: AuthService, - - protected i18nService: I18nService, - ) { - super(_overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, scrollStrategy); - + constructor() { + /** + * TODO: This logic should exist outside of `libs/components`. + * @see https://bitwarden.atlassian.net/browse/CL-657 + **/ /** Close all open dialogs if the vault locks */ - if (router && authService) { - router.events + if (this.router && this.authService) { + this.router.events .pipe( filter((event) => event instanceof NavigationEnd), - switchMap(() => authService.getAuthStatus()), + switchMap(() => this.authService!.getAuthStatus()), filter((v) => v !== AuthenticationStatus.Unlocked), - takeUntil(this._destroy$), + takeUntilDestroyed(), ) .subscribe(() => this.closeAll()); } } - override ngOnDestroy(): void { - this._destroy$.next(); - this._destroy$.complete(); - super.ngOnDestroy(); - } - - override open( + open( componentOrTemplateRef: ComponentType | TemplateRef, config?: DialogConfig>, ): DialogRef { - config = { + /** + * This is a bit circular in nature: + * We need the DialogRef instance for the DI injector that is passed *to* `Dialog.open`, + * but we get the base CDK DialogRef instance *from* `Dialog.open`. + * + * To break the circle, we define CDKDialogRef as a wrapper for the CDKDialogRefBase. + * This allows us to create the class instance and provide the base instance later, almost like "deferred inheritance". + **/ + const ref = new CdkDialogRef(); + const injector = this.createInjector({ + data: config?.data, + dialogRef: ref, + }); + + // Merge the custom config with the default config + const _config = { backdropClass: this.backDropClasses, scrollStrategy: this.defaultScrollStrategy, + injector, ...config, }; - return super.open(componentOrTemplateRef, config); + ref.cdkDialogRefBase = this.dialog.open(componentOrTemplateRef, _config); + return ref; + } + + /** Opens a dialog in the side drawer */ + openDrawer( + component: ComponentType, + config?: DialogConfig>, + ): DialogRef { + this.activeDrawer?.close(); + /** + * This is also circular. When creating the DrawerDialogRef, we do not yet have a portal instance to provide to the injector. + * Similar to `this.open`, we get around this with mutability. + */ + this.activeDrawer = new DrawerDialogRef(this.drawerService); + const portal = new ComponentPortal( + component, + null, + this.createInjector({ data: config?.data, dialogRef: this.activeDrawer }), + ); + this.activeDrawer.portal = portal; + this.drawerService.open(portal); + return this.activeDrawer; } /** @@ -113,8 +209,7 @@ export class DialogService extends Dialog implements OnDestroy { */ async openSimpleDialog(simpleDialogOptions: SimpleDialogOptions): Promise { const dialogRef = this.openSimpleDialogRef(simpleDialogOptions); - - return firstValueFrom(dialogRef.closed); + return firstValueFrom(dialogRef.closed.pipe(map((v: boolean | undefined) => !!v))); } /** @@ -134,20 +229,29 @@ export class DialogService extends Dialog implements OnDestroy { }); } - protected translate(translation: string | Translation, defaultKey?: string): string { - if (translation == null && defaultKey == null) { - return null; - } + /** Close all open dialogs */ + closeAll(): void { + return this.dialog.closeAll(); + } - if (translation == null) { - return this.i18nService.t(defaultKey); - } - - // Translation interface use implies we must localize. - if (typeof translation === "object") { - return this.i18nService.t(translation.key, ...(translation.placeholders ?? [])); - } - - return translation; + /** The injector that is passed to the opened dialog */ + private createInjector(opts: { data: unknown; dialogRef: DialogRef }): Injector { + return Injector.create({ + providers: [ + { + provide: DIALOG_DATA, + useValue: opts.data, + }, + { + provide: DialogRef, + useValue: opts.dialogRef, + }, + { + provide: CdkDialogRefBase, + useValue: opts.dialogRef, + }, + ], + parent: this.injector, + }); } } diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 01f05985127..eaf7fc2beec 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -1,12 +1,22 @@ +@let isDrawer = dialogRef?.isDrawer;
+ @let showHeaderBorder = !isDrawer || background === "alt" || bodyHasScrolledFrom().top;
-

} -

+ @@ -88,72 +149,6 @@ class KitchenSinkDialog { - - - - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

- - What did foo say to bar? - - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt - ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation - ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur - sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id - est laborum. -

-
-
`, }) export class KitchenSinkMainComponent { @@ -166,7 +161,7 @@ export class KitchenSinkMainComponent { } openDrawer() { - this.drawerOpen.set(true); + this.dialogService.openDrawer(KitchenSinkDialog); } navItems = [ diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index f57a9de4e68..d318e1b5f0e 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -14,7 +14,6 @@ import { import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; import { positionFixedWrapperDecorator } from "../storybook-decorators"; @@ -39,8 +38,20 @@ export default { KitchenSinkTable, KitchenSinkToggleList, ], + }), + applicationConfig({ providers: [ - DialogService, + provideNoopAnimations(), + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "bitwarden", pathMatch: "full" }, + { path: "bitwarden", component: KitchenSinkMainComponent }, + { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent }, + ], + { useHash: true }, + ), + ), { provide: I18nService, useFactory: () => { @@ -58,21 +69,6 @@ export default { }, ], }), - applicationConfig({ - providers: [ - provideNoopAnimations(), - importProvidersFrom( - RouterModule.forRoot( - [ - { path: "", redirectTo: "bitwarden", pathMatch: "full" }, - { path: "bitwarden", component: KitchenSinkMainComponent }, - { path: "virtual-scroll", component: DialogVirtualScrollBlockComponent }, - ], - { useHash: true }, - ), - ), - ], - }), ], } as Meta; diff --git a/libs/components/src/stories/virtual-scrolling.mdx b/libs/components/src/stories/virtual-scrolling.mdx new file mode 100644 index 00000000000..94a86090dce --- /dev/null +++ b/libs/components/src/stories/virtual-scrolling.mdx @@ -0,0 +1,60 @@ +import { Meta } from "@storybook/addon-docs"; + + + +# Virtual Scrolling + +Virtual scrolling is a technique that improves the rendering performance of very large lists by only +rendering whatever is currently visible within the viewport. We build on top of +[Angular CDK's `ScrollingModule`](https://material.angular.dev/cdk/scrolling/overview). + +## Scrolling the entire layout + +Often, a design calls for the scroll container to envelop the entire page. To support this, +AngularCDK provides a `scrollWindow` directive that sets the window to be virtual scroll viewport. +We export a similar directive, `bitScrollLayout`, that integrates with `bit-layout` and `popup-page` +and should be used instead of `scrollWindow`. + +```html + + + + +``` + +### Known footgun + +Due to the initialization order of Angular components and their templates, `bitScrollLayout` will +error if it is used _in the same template_ as the layout component: + +```html + + + + + +``` + +In this particular composition, the child content gets constructed before the template of +`bit-layout` and thus has no scroll container to reference. Workarounds include: + +1. Wrap the child in another component. (This tends to happen by default when the layout is + integrated with a `router-outlet`.) + +```html + + + +``` + +2. Use a `defer` block. + +```html + + @defer (on immediate) { + + + + } + +``` diff --git a/libs/components/src/table/table-scroll.component.html b/libs/components/src/table/table-scroll.component.html index 8f2c88ba3ad..523912cd7ac 100644 --- a/libs/components/src/table/table-scroll.component.html +++ b/libs/components/src/table/table-scroll.component.html @@ -1,5 +1,5 @@ diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts index b463b12f6ce..193d790e416 100644 --- a/libs/components/src/table/table-scroll.component.ts +++ b/libs/components/src/table/table-scroll.component.ts @@ -4,7 +4,6 @@ import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf, - CdkVirtualScrollableWindow, } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { @@ -21,6 +20,8 @@ import { TrackByFunction, } from "@angular/core"; +import { ScrollLayoutDirective } from "../layout"; + import { RowDirective } from "./row.directive"; import { TableComponent } from "./table.component"; @@ -52,10 +53,10 @@ export class BitRowDef { imports: [ CommonModule, CdkVirtualScrollViewport, - CdkVirtualScrollableWindow, CdkFixedSizeVirtualScroll, CdkVirtualForOf, RowDirective, + ScrollLayoutDirective, ], }) export class TableScrollComponent diff --git a/libs/components/src/table/table.mdx b/libs/components/src/table/table.mdx index 8d784190ed9..59bf5b773a3 100644 --- a/libs/components/src/table/table.mdx +++ b/libs/components/src/table/table.mdx @@ -142,7 +142,7 @@ dataSource.filter = (data) => data.orgType === "family"; Rudimentary string filtering is supported out of the box with `TableDataSource.simpleStringFilter`. It works by converting each entry into a string of it's properties. The provided string is then -compared against the filter value using a simple `indexOf` check. For convienence, you can also just +compared against the filter value using a simple `indexOf` check. For convenience, you can also just pass a string directly. ```ts @@ -153,7 +153,7 @@ dataSource.filter = "search value"; ### Virtual Scrolling -It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount +It's heavily advised to use virtual scrolling if you expect the table to have any significant amount of data. This is done by using the `bit-table-scroll` component instead of the `bit-table` component. This component behaves slightly different from the `bit-table` component. Instead of using the `*ngFor` directive to render the rows, you provide a `bitRowDef` template that will be @@ -178,6 +178,14 @@ height and align vertically. ``` +#### Deprecated approach + +Before `bit-table-scroll` was introduced, virtual scroll in tables was implemented manually via +constructs from Angular CDK. This included wrapping the table with a `cdk-virtual-scroll-viewport` +and targeting with `bit-layout`'s scroll container with the `bitScrollLayout` directive. + +This pattern is deprecated in favor of `bit-table-scroll`. + ## Accessibility - Always include a row or column header with your table; this allows assistive technology to better diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index e8ab24ee8b7..d696e6077dd 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -1,6 +1,13 @@ +import { RouterTestingModule } from "@angular/router/testing"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { countries } from "../form/countries"; +import { LayoutComponent } from "../layout"; +import { mockLayoutI18n } from "../layout/mocks"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; +import { I18nMockService } from "../utils"; import { TableDataSource } from "./table-data-source"; import { TableModule } from "./table.module"; @@ -8,8 +15,17 @@ import { TableModule } from "./table.module"; export default { title: "Component Library/Table", decorators: [ + positionFixedWrapperDecorator(), moduleMetadata({ - imports: [TableModule], + imports: [TableModule, LayoutComponent, RouterTestingModule], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService(mockLayoutI18n); + }, + }, + ], }), ], argTypes: { @@ -116,18 +132,20 @@ export const Scrollable: Story = { trackBy: (index: number, item: any) => item.id, }, template: ` - - - Id - Name - Other - - - {{ row.id }} - {{ row.name }} - {{ row.other }} - - + + + + Id + Name + Other + + + {{ row.id }} + {{ row.name }} + {{ row.other }} + + + `, }), }; @@ -144,17 +162,19 @@ export const Filterable: Story = { sortFn: (a: any, b: any) => a.id - b.id, }, template: ` - - - - Name - Value - - - {{ row.name }} - {{ row.value }} - - + + + + + Name + Value + + + {{ row.name }} + {{ row.value }} + + + `, }), }; diff --git a/libs/components/src/utils/has-scrolled-from.ts b/libs/components/src/utils/has-scrolled-from.ts new file mode 100644 index 00000000000..44c73465bdd --- /dev/null +++ b/libs/components/src/utils/has-scrolled-from.ts @@ -0,0 +1,41 @@ +import { CdkScrollable } from "@angular/cdk/scrolling"; +import { Signal, inject, signal } from "@angular/core"; +import { toObservable, toSignal } from "@angular/core/rxjs-interop"; +import { map, startWith, switchMap } from "rxjs"; + +export type ScrollState = { + /** `true` when the scrollbar is not at the top-most position */ + top: boolean; + + /** `true` when the scrollbar is not at the bottom-most position */ + bottom: boolean; +}; + +/** + * Check if a `CdkScrollable` instance has been scrolled + * @param scrollable The instance to check, defaults to the one provided by the current injector + * @returns {Signal} + */ +export const hasScrolledFrom = (scrollable?: Signal): Signal => { + const _scrollable = scrollable ?? signal(inject(CdkScrollable)); + const scrollable$ = toObservable(_scrollable); + + const scrollState$ = scrollable$.pipe( + switchMap((_scrollable) => + _scrollable.elementScrolled().pipe( + startWith(null), + map(() => ({ + top: _scrollable.measureScrollOffset("top") > 0, + bottom: _scrollable.measureScrollOffset("bottom") > 0, + })), + ), + ), + ); + + return toSignal(scrollState$, { + initialValue: { + top: false, + bottom: false, + }, + }); +}; From df4aae2fb2134abfdbe323b42d84bbefbac3e85f Mon Sep 17 00:00:00 2001 From: Vicki League Date: Tue, 17 Jun 2025 13:33:01 -0400 Subject: [PATCH 06/31] [CL-700] Move auth-owned layout components to UIF ownership (#15093) --- .github/CODEOWNERS | 1 + .../extension-login-component.service.spec.ts | 2 +- .../extension-login-component.service.ts | 2 +- apps/browser/src/popup/app-routing.module.ts | 14 ++++---- apps/browser/src/popup/app.module.ts | 2 +- ...ension-anon-layout-wrapper-data.service.ts | 2 +- ...tension-anon-layout-wrapper.component.html | 0 ...extension-anon-layout-wrapper.component.ts | 17 ++++----- .../extension-anon-layout-wrapper.stories.ts | 21 ++++++----- .../src/popup/services/services.module.ts | 10 ++++-- apps/desktop/src/app/app-routing.module.ts | 6 ++-- .../accept-family-sponsorship.component.ts | 5 ++- apps/web/src/app/oss-routing.module.ts | 6 ++-- .../send/send-access/access.component.ts | 3 +- .../browser-extension-prompt.service.spec.ts | 2 +- .../browser-extension-prompt.service.ts | 2 +- .../manage/accept-provider.component.ts | 4 +-- .../providers/providers-routing.module.ts | 2 +- .../setup/setup-provider.component.ts | 4 +-- .../bit-web/src/app/app-routing.module.ts | 2 +- .../setup/setup-business-unit.component.ts | 4 +-- .../src/services/jslib-services.module.ts | 8 +++-- libs/auth/src/angular/icons/index.ts | 4 --- libs/auth/src/angular/index.ts | 7 ---- .../login-decryption-options.component.ts | 3 +- .../auth/src/angular/login/login.component.ts | 2 +- .../registration-finish.component.ts | 3 +- .../registration-start.component.ts | 6 ++-- .../registration-start.stories.ts | 4 +-- .../two-factor-auth.component.spec.ts | 4 +-- .../two-factor-auth.component.ts | 2 +- .../anon-layout-wrapper-data.service.ts | 0 .../anon-layout-wrapper.component.html | 0 .../anon-layout-wrapper.component.ts | 8 ++--- .../src}/anon-layout/anon-layout-wrapper.mdx | 0 .../anon-layout-wrapper.stories.ts | 35 +++++++++++-------- .../anon-layout/anon-layout.component.html | 0 .../src}/anon-layout/anon-layout.component.ts | 14 +++----- .../src}/anon-layout/anon-layout.mdx | 4 +-- .../src}/anon-layout/anon-layout.stories.ts | 13 +++---- ...efault-anon-layout-wrapper-data.service.ts | 0 libs/components/src/anon-layout/index.ts | 4 +++ .../src/icon}/icons/bitwarden-logo.icon.ts | 4 +-- .../src/icon}/icons/bitwarden-shield.icon.ts | 4 +-- .../icons}/extension-bitwarden-logo.icon.ts | 2 +- libs/components/src/icon/icons/index.ts | 11 ++++-- .../src/icon}/icons/lock.icon.ts | 4 +-- .../icons/registration-check-email.icon.ts | 4 +-- libs/components/src/index.ts | 1 + .../src/lock/components/lock.component.ts | 2 +- 50 files changed, 126 insertions(+), 138 deletions(-) rename apps/browser/src/{auth/popup => popup/components}/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts (95%) rename apps/browser/src/{auth/popup => popup/components}/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html (100%) rename apps/browser/src/{auth/popup => popup/components}/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts (94%) rename apps/browser/src/{auth/popup => popup/components}/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts (94%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout-wrapper-data.service.ts (100%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout-wrapper.component.html (100%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout-wrapper.component.ts (95%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout-wrapper.mdx (100%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout-wrapper.stories.ts (88%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout.component.html (100%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout.component.ts (82%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout.mdx (97%) rename libs/{auth/src/angular => components/src}/anon-layout/anon-layout.stories.ts (96%) rename libs/{auth/src/angular => components/src}/anon-layout/default-anon-layout-wrapper-data.service.ts (100%) create mode 100644 libs/components/src/anon-layout/index.ts rename libs/{auth/src/angular => components/src/icon}/icons/bitwarden-logo.icon.ts (96%) rename libs/{auth/src/angular => components/src/icon}/icons/bitwarden-shield.icon.ts (85%) rename {apps/browser/src/auth/popup/extension-anon-layout-wrapper => libs/components/src/icon/icons}/extension-bitwarden-logo.icon.ts (99%) rename libs/{auth/src/angular => components/src/icon}/icons/lock.icon.ts (92%) rename libs/{auth/src/angular => components/src/icon}/icons/registration-check-email.icon.ts (90%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index df099efa1d6..fae279b08c5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -140,6 +140,7 @@ libs/components @bitwarden/team-ui-foundation libs/ui @bitwarden/team-ui-foundation apps/browser/src/platform/popup/layout @bitwarden/team-ui-foundation apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-ui-foundation +apps/browser/src/popup/components/extension-anon-layout-wrapper @bitwarden/team-ui-foundation apps/web/src/app/layouts @bitwarden/team-ui-foundation diff --git a/apps/browser/src/auth/popup/login/extension-login-component.service.spec.ts b/apps/browser/src/auth/popup/login/extension-login-component.service.spec.ts index 6d1f0571ae7..bd85ff9293e 100644 --- a/apps/browser/src/auth/popup/login/extension-login-component.service.spec.ts +++ b/apps/browser/src/auth/popup/login/extension-login-component.service.spec.ts @@ -16,7 +16,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; -import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; +import { ExtensionAnonLayoutWrapperDataService } from "../../../popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; import { ExtensionLoginComponentService } from "./extension-login-component.service"; diff --git a/apps/browser/src/auth/popup/login/extension-login-component.service.ts b/apps/browser/src/auth/popup/login/extension-login-component.service.ts index 49ed0635b7a..37d74616391 100644 --- a/apps/browser/src/auth/popup/login/extension-login-component.service.ts +++ b/apps/browser/src/auth/popup/login/extension-login-component.service.ts @@ -11,7 +11,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; +import { ExtensionAnonLayoutWrapperDataService } from "../../../popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; @Injectable() export class ExtensionLoginComponentService diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index b530a868b61..fbf4afaf14a 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -16,10 +16,7 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { - AnonLayoutWrapperComponent, - AnonLayoutWrapperData, DevicesIcon, - LockIcon, LoginComponent, LoginDecryptionOptionsComponent, LoginSecondaryContentComponent, @@ -41,13 +38,10 @@ import { UserLockIcon, VaultIcon, } from "@bitwarden/auth/angular"; +import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; -import { - ExtensionAnonLayoutWrapperComponent, - ExtensionAnonLayoutWrapperData, -} from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; @@ -89,6 +83,10 @@ import { TrashComponent } from "../vault/popup/settings/trash.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; import { RouteElevation } from "./app-routing.animations"; +import { + ExtensionAnonLayoutWrapperComponent, + ExtensionAnonLayoutWrapperData, +} from "./components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { TabsV2Component } from "./tabs-v2.component"; @@ -504,7 +502,7 @@ const routes: Routes = [ path: "lock", canActivate: [lockGuard()], data: { - pageIcon: LockIcon, + pageIcon: Icons.LockIcon, pageTitle: { key: "yourVaultIsLockedV2", }, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index b400cb5eec8..77c87838ff7 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -26,7 +26,6 @@ import { import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; -import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; @@ -44,6 +43,7 @@ import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popou import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; +import { ExtensionAnonLayoutWrapperComponent } from "./components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component"; import { UserVerificationComponent } from "./components/user-verification.component"; import { ServicesModule } from "./services/services.module"; import { TabsV2Component } from "./tabs-v2.component"; diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts similarity index 95% rename from apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts rename to apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts index 1b844d4b2c7..952c42b8367 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts +++ b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts @@ -3,7 +3,7 @@ import { Observable, Subject } from "rxjs"; import { AnonLayoutWrapperDataService, DefaultAnonLayoutWrapperDataService, -} from "@bitwarden/auth/angular"; +} from "@bitwarden/components"; import { ExtensionAnonLayoutWrapperData } from "./extension-anon-layout-wrapper.component"; diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html similarity index 100% rename from apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html rename to apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts similarity index 94% rename from apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts rename to apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index b335155d355..fc2b6590992 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -5,22 +5,23 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router"; import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { + Icon, + Icons, + IconModule, + Translation, AnonLayoutComponent, AnonLayoutWrapperData, AnonLayoutWrapperDataService, -} from "@bitwarden/auth/angular"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Icon, IconModule, Translation } from "@bitwarden/components"; +} from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; +import { AccountSwitcherService } from "../../../auth/popup/account-switching/services/account-switcher.service"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -import { CurrentAccountComponent } from "../account-switching/current-account.component"; -import { AccountSwitcherService } from "../account-switching/services/account-switcher.service"; - -import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon"; export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { showAcctSwitcher?: boolean; @@ -61,7 +62,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected hideFooter: boolean; protected theme: string; - protected logo = ExtensionBitwardenLogo; + protected logo = Icons.ExtensionBitwardenLogo; constructor( private router: Router, diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts similarity index 94% rename from apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts rename to apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index 78ca577a69d..2c3d09b79fb 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -9,7 +9,6 @@ import { } from "@storybook/angular"; import { of } from "rxjs"; -import { AnonLayoutWrapperDataService, LockIcon } from "@bitwarden/auth/angular"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; @@ -23,13 +22,15 @@ import { import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { ButtonModule, I18nMockService } from "@bitwarden/components"; +import { + AnonLayoutWrapperDataService, + ButtonModule, + Icons, + I18nMockService, +} from "@bitwarden/components"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; +import { AccountSwitcherService } from "../../../auth/popup/account-switching/services/account-switcher.service"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; -import { AccountSwitcherService } from "../account-switching/services/account-switcher.service"; import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service"; import { @@ -38,7 +39,7 @@ import { } from "./extension-anon-layout-wrapper.component"; export default { - title: "Auth/Extension Anon Layout Wrapper", + title: "Browser/Extension Anon Layout Wrapper", component: ExtensionAnonLayoutWrapperComponent, } as Meta; @@ -142,6 +143,8 @@ const decorators = (options: { switchAccounts: "Switch accounts", back: "Back", activeAccount: "Active account", + appLogoLabel: "app logo label", + bitwardenAccount: "Bitwarden Account", }); }, }, @@ -241,7 +244,7 @@ const initialData: ExtensionAnonLayoutWrapperData = { pageSubtitle: { key: "finishCreatingYourAccountBySettingAPassword", }, - pageIcon: LockIcon, + pageIcon: Icons.LockIcon, showAcctSwitcher: true, showBackButton: true, showLogo: true, @@ -255,7 +258,7 @@ const changedData: ExtensionAnonLayoutWrapperData = { pageSubtitle: { key: "checkYourEmail", }, - pageIcon: RegistrationCheckEmailIcon, + pageIcon: Icons.RegistrationCheckEmailIcon, showAcctSwitcher: false, showBackButton: false, showLogo: false, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 00e493fc035..9f79cf42553 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -22,7 +22,6 @@ import { } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { - AnonLayoutWrapperDataService, LoginComponentService, TwoFactorAuthComponentService, TwoFactorAuthEmailComponentService, @@ -121,7 +120,12 @@ import { } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; -import { CompactModeService, DialogService, ToastService } from "@bitwarden/components"; +import { + AnonLayoutWrapperDataService, + CompactModeService, + DialogService, + ToastService, +} from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BiometricsService, @@ -138,7 +142,6 @@ import { import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; -import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service"; import { ExtensionLogoutService } from "../../auth/popup/logout/extension-logout.service"; @@ -181,6 +184,7 @@ import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-u import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service"; import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; import { VaultFilterService } from "../../vault/services/vault-filter.service"; +import { ExtensionAnonLayoutWrapperDataService } from "../components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; import { DebounceNavigationService } from "./debounce-navigation.service"; import { InitService } from "./init.service"; diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 50036fb964c..d90f3cf0d26 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -16,11 +16,8 @@ import { } from "@bitwarden/angular/auth/guards"; import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { - AnonLayoutWrapperComponent, - AnonLayoutWrapperData, LoginComponent, LoginSecondaryContentComponent, - LockIcon, LoginViaAuthRequestComponent, PasswordHintComponent, RegistrationFinishComponent, @@ -42,6 +39,7 @@ import { DeviceVerificationIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; @@ -292,7 +290,7 @@ const routes: Routes = [ path: "lock", canActivate: [lockGuard()], data: { - pageIcon: LockIcon, + pageIcon: Icons.LockIcon, pageTitle: { key: "yourVaultIsLockedV2", }, diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index 4df6defe8ad..b0c89cd30ab 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -3,11 +3,10 @@ import { Component, inject } from "@angular/core"; import { Params } from "@angular/router"; -import { BitwardenLogo } from "@bitwarden/auth/angular"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { OrganizationSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/organization-sponsorship.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { ToastService } from "@bitwarden/components"; +import { Icons, ToastService } from "@bitwarden/components"; import { BaseAcceptComponent } from "../../../common/base.accept.component"; @@ -22,7 +21,7 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component"; standalone: false, }) export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent { - protected logo = BitwardenLogo; + protected logo = Icons.BitwardenLogo; failedShortMessage = "inviteAcceptFailedShort"; failedMessage = "inviteAcceptFailed"; diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 6a7cc51d3ba..783fe6ada0a 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -11,8 +11,6 @@ import { activeAuthGuard, } from "@bitwarden/angular/auth/guards"; import { - AnonLayoutWrapperComponent, - AnonLayoutWrapperData, PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, @@ -22,7 +20,6 @@ import { RegistrationLinkExpiredComponent, LoginComponent, LoginSecondaryContentComponent, - LockIcon, TwoFactorTimeoutIcon, UserLockIcon, SsoKeyIcon, @@ -39,6 +36,7 @@ import { NewDeviceVerificationComponent, DeviceVerificationIcon, } from "@bitwarden/auth/angular"; +import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components"; import { LockComponent } from "@bitwarden/key-management-ui"; import { VaultIcons } from "@bitwarden/vault"; @@ -399,7 +397,7 @@ const routes: Routes = [ pageTitle: { key: "yourVaultIsLockedV2", }, - pageIcon: LockIcon, + pageIcon: Icons.LockIcon, showReadonlyHostname: true, } satisfies AnonLayoutWrapperData, }, diff --git a/apps/web/src/app/tools/send/send-access/access.component.ts b/apps/web/src/app/tools/send/send-access/access.component.ts index 2676cb9bef4..bc2851f0df7 100644 --- a/apps/web/src/app/tools/send/send-access/access.component.ts +++ b/apps/web/src/app/tools/send/send-access/access.component.ts @@ -4,7 +4,6 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -17,7 +16,7 @@ import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { NoItemsModule, ToastService } from "@bitwarden/components"; +import { AnonLayoutWrapperDataService, NoItemsModule, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { ExpiredSendIcon } from "@bitwarden/send-ui"; diff --git a/apps/web/src/app/vault/services/browser-extension-prompt.service.spec.ts b/apps/web/src/app/vault/services/browser-extension-prompt.service.spec.ts index f6f4ec4fdb4..5b4c6665aa0 100644 --- a/apps/web/src/app/vault/services/browser-extension-prompt.service.spec.ts +++ b/apps/web/src/app/vault/services/browser-extension-prompt.service.spec.ts @@ -1,9 +1,9 @@ import { TestBed } from "@angular/core/testing"; -import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; +import { AnonLayoutWrapperDataService } from "@bitwarden/components"; import { BrowserExtensionPromptService, diff --git a/apps/web/src/app/vault/services/browser-extension-prompt.service.ts b/apps/web/src/app/vault/services/browser-extension-prompt.service.ts index 0f401c04abe..a164a106917 100644 --- a/apps/web/src/app/vault/services/browser-extension-prompt.service.ts +++ b/apps/web/src/app/vault/services/browser-extension-prompt.service.ts @@ -2,11 +2,11 @@ import { DestroyRef, Injectable } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { BehaviorSubject, fromEvent } from "rxjs"; -import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; +import { AnonLayoutWrapperDataService } from "@bitwarden/components"; export const BrowserPromptState = { Loading: "loading", diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index 7a90403b0b9..7696742277a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -3,12 +3,12 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { BitwardenLogo } from "@bitwarden/auth/angular"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Icons } from "@bitwarden/components"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; @Component({ @@ -17,7 +17,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept standalone: false, }) export class AcceptProviderComponent extends BaseAcceptComponent { - protected logo = BitwardenLogo; + protected logo = Icons.BitwardenLogo; providerName: string; providerId: string; providerUserId: string; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 9be09c295ae..482d2c881c1 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,8 +2,8 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; -import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { AnonLayoutWrapperComponent } from "@bitwarden/components"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts index 8d87b82bb88..47c30490af3 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup-provider.component.ts @@ -1,7 +1,7 @@ import { Component } from "@angular/core"; import { Params } from "@angular/router"; -import { BitwardenLogo } from "@bitwarden/auth/angular"; +import { Icons } from "@bitwarden/components"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; @Component({ @@ -10,7 +10,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept standalone: false, }) export class SetupProviderComponent extends BaseAcceptComponent { - protected logo = BitwardenLogo; + protected logo = Icons.BitwardenLogo; failedShortMessage = "inviteAcceptFailedShort"; failedMessage = "inviteAcceptFailed"; diff --git a/bitwarden_license/bit-web/src/app/app-routing.module.ts b/bitwarden_license/bit-web/src/app/app-routing.module.ts index 3f2803695eb..dc6f417c290 100644 --- a/bitwarden_license/bit-web/src/app/app-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/app-routing.module.ts @@ -2,7 +2,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { unauthGuardFn } from "@bitwarden/angular/auth/guards"; -import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; +import { AnonLayoutWrapperComponent } from "@bitwarden/components"; import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link/deep-link.guard"; import { RouteDataProperties } from "@bitwarden/web-vault/app/core"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts index 056339b6fb7..0634c891a05 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts @@ -3,7 +3,6 @@ import { ActivatedRoute, Params, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { filter, map, switchMap } from "rxjs/operators"; -import { BitwardenLogo } from "@bitwarden/auth/angular"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -13,6 +12,7 @@ import { StateProvider } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { ProviderKey } from "@bitwarden/common/types/key"; +import { Icons } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service"; import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component"; @@ -22,7 +22,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept standalone: false, }) export class SetupBusinessUnitComponent extends BaseAcceptComponent { - protected bitwardenLogo = BitwardenLogo; + protected bitwardenLogo = Icons.BitwardenLogo; failedMessage = "emergencyInviteAcceptFailed"; failedShortMessage = "emergencyInviteAcceptFailedShort"; diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index e1f806c4d3e..873cb4f9b63 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -14,8 +14,6 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { - AnonLayoutWrapperDataService, - DefaultAnonLayoutWrapperDataService, DefaultLoginApprovalComponentService, DefaultLoginComponentService, DefaultLoginDecryptionOptionsService, @@ -299,7 +297,11 @@ import { FolderService } from "@bitwarden/common/vault/services/folder/folder.se import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; -import { ToastService } from "@bitwarden/components"; +import { + AnonLayoutWrapperDataService, + DefaultAnonLayoutWrapperDataService, + ToastService, +} from "@bitwarden/components"; import { GeneratorHistoryService, LocalGeneratorHistoryService, diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 0ec92d54547..078e7f764c5 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -1,8 +1,4 @@ -export * from "./bitwarden-logo.icon"; -export * from "./bitwarden-shield.icon"; export * from "./devices.icon"; -export * from "./lock.icon"; -export * from "./registration-check-email.icon"; export * from "./user-lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; export * from "./wave.icon"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index f4f6cc71a42..fc5ffd71e9a 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -1,13 +1,6 @@ /** * This barrel file should only contain Angular exports */ - -// anon layout -export * from "./anon-layout/anon-layout.component"; -export * from "./anon-layout/anon-layout-wrapper.component"; -export * from "./anon-layout/anon-layout-wrapper-data.service"; -export * from "./anon-layout/default-anon-layout-wrapper-data.service"; - // change password export * from "./change-password/change-password.component"; export * from "./change-password/change-password.service.abstraction"; diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 172823f23da..bbdc0106786 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -30,6 +30,7 @@ import { UserId } from "@bitwarden/common/types/guid"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { + AnonLayoutWrapperDataService, AsyncActionsModule, ButtonModule, CheckboxModule, @@ -40,8 +41,6 @@ import { } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; - import { LoginDecryptionOptionsService } from "./login-decryption-options.service"; // FIXME: update to use a const object instead of a typescript enum diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index aaff86224ff..5e5d5bde4e3 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -32,6 +32,7 @@ import { UserId } from "@bitwarden/common/types/guid"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { + AnonLayoutWrapperDataService, AsyncActionsModule, ButtonModule, CheckboxModule, @@ -41,7 +42,6 @@ import { ToastService, } from "@bitwarden/components"; -import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; import { VaultIcon, WaveIcon } from "../icons"; import { LoginComponentService, PasswordPolicies } from "./login-component.service"; diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index c3a09a897e5..f987083fb01 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -16,14 +16,13 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { ToastService } from "@bitwarden/components"; +import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components"; import { LoginStrategyServiceAbstraction, LoginSuccessHandlerService, PasswordLoginCredentials, } from "../../../common"; -import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { InputPasswordComponent, InputPasswordFlow, diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts index d8a4ebb2b7d..2545f86f665 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts @@ -14,18 +14,18 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { + AnonLayoutWrapperDataService, AsyncActionsModule, ButtonModule, CheckboxModule, FormFieldModule, + Icons, IconModule, LinkModule, } from "@bitwarden/components"; import { LoginEmailService } from "../../../common"; -import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { RegistrationUserAddIcon } from "../../icons"; -import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon"; import { RegistrationEnvSelectorComponent } from "../registration-env-selector/registration-env-selector.component"; // FIXME: update to use a const object instead of a typescript enum @@ -170,7 +170,7 @@ export class RegistrationStartComponent implements OnInit, OnDestroy { pageTitle: { key: "checkYourEmail", }, - pageIcon: RegistrationCheckEmailIcon, + pageIcon: Icons.RegistrationCheckEmailIcon, }); this.registrationStartStateChange.emit(this.state); }; diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index e54e59a988a..d0f0343960a 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -18,6 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { + AnonLayoutWrapperData, + AnonLayoutWrapperDataService, AsyncActionsModule, ButtonModule, DialogModule, @@ -34,8 +36,6 @@ import { // eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests"; import { LoginEmailService } from "../../../common"; -import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; -import { AnonLayoutWrapperData } from "../../anon-layout/anon-layout-wrapper.component"; import { RegistrationStartComponent } from "./registration-start.component"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index 76cbfe994a5..00cad105f95 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -36,9 +36,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { UserId } from "@bitwarden/common/types/guid"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { DialogService, ToastService } from "@bitwarden/components"; - -import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; +import { DialogService, ToastService, AnonLayoutWrapperDataService } from "@bitwarden/components"; import { TwoFactorAuthComponentCacheService } from "./two-factor-auth-component-cache.service"; import { TwoFactorAuthComponentService } from "./two-factor-auth-component.service"; diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 315f8121cce..b811d48a48f 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -41,6 +41,7 @@ import { UserId } from "@bitwarden/common/types/guid"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { + AnonLayoutWrapperDataService, AsyncActionsModule, ButtonModule, CheckboxModule, @@ -49,7 +50,6 @@ import { ToastService, } from "@bitwarden/components"; -import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; import { TwoFactorAuthAuthenticatorIcon, TwoFactorAuthEmailIcon, diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper-data.service.ts b/libs/components/src/anon-layout/anon-layout-wrapper-data.service.ts similarity index 100% rename from libs/auth/src/angular/anon-layout/anon-layout-wrapper-data.service.ts rename to libs/components/src/anon-layout/anon-layout-wrapper-data.service.ts diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html b/libs/components/src/anon-layout/anon-layout-wrapper.component.html similarity index 100% rename from libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html rename to libs/components/src/anon-layout/anon-layout-wrapper.component.html diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts similarity index 95% rename from libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts rename to libs/components/src/anon-layout/anon-layout-wrapper.component.ts index 69f1dd1be63..ffc601bdf1d 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts @@ -4,13 +4,13 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router"; import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; -import { AnonLayoutComponent } from "@bitwarden/auth/angular"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { Icon, Translation } from "@bitwarden/components"; + +import { Translation } from "../dialog"; +import { Icon } from "../icon"; import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service"; +import { AnonLayoutComponent } from "./anon-layout.component"; export interface AnonLayoutWrapperData { /** diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.mdx b/libs/components/src/anon-layout/anon-layout-wrapper.mdx similarity index 100% rename from libs/auth/src/angular/anon-layout/anon-layout-wrapper.mdx rename to libs/components/src/anon-layout/anon-layout-wrapper.mdx diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts b/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts similarity index 88% rename from libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts rename to libs/components/src/anon-layout/anon-layout-wrapper.stories.ts index f106f9ee0dc..57fba034c7e 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.stories.ts @@ -14,24 +14,19 @@ import { EnvironmentService, Environment, } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { ButtonModule } from "@bitwarden/components"; -// FIXME: remove `/apps` import from `/libs` -// FIXME: remove `src` and fix import -// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports -import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests"; -import { LockIcon } from "../icons"; -import { RegistrationCheckEmailIcon } from "../icons/registration-check-email.icon"; +import { ButtonModule } from "../button"; +import { LockIcon, RegistrationCheckEmailIcon } from "../icon/icons"; +import { I18nMockService } from "../utils"; import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "./anon-layout-wrapper.component"; import { DefaultAnonLayoutWrapperDataService } from "./default-anon-layout-wrapper-data.service"; export default { - title: "Auth/Anon Layout Wrapper", + title: "Component Library/Anon Layout Wrapper", component: AnonLayoutWrapperComponent, } as Meta; @@ -84,13 +79,21 @@ const decorators = (options: { getClientType: () => options.clientType || ClientType.Web, } as Partial, }, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + setAStrongPassword: "Set a strong password", + appLogoLabel: "app logo label", + finishCreatingYourAccountBySettingAPassword: + "Finish creating your account by setting a password", + }); + }, + }, ], }), applicationConfig({ - providers: [ - importProvidersFrom(RouterModule.forRoot(options.routes)), - importProvidersFrom(PreloadedEnglishI18nModule), - ], + providers: [importProvidersFrom(RouterModule.forRoot(options.routes))], }), ]; }; @@ -102,18 +105,21 @@ type Story = StoryObj; @Component({ selector: "bit-default-primary-outlet-example-component", template: "

Primary Outlet Example:
your primary component goes here

", + standalone: false, }) export class DefaultPrimaryOutletExampleComponent {} @Component({ selector: "bit-default-secondary-outlet-example-component", template: "

Secondary Outlet Example:
your secondary component goes here

", + standalone: false, }) export class DefaultSecondaryOutletExampleComponent {} @Component({ selector: "bit-default-env-selector-outlet-example-component", template: "

Env Selector Outlet Example:
your env selector component goes here

", + standalone: false, }) export class DefaultEnvSelectorOutletExampleComponent {} @@ -188,6 +194,7 @@ const changedData: AnonLayoutWrapperData = { template: ` `, + standalone: false, }) export class DynamicContentExampleComponent { initialData = true; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/components/src/anon-layout/anon-layout.component.html similarity index 100% rename from libs/auth/src/angular/anon-layout/anon-layout.component.html rename to libs/components/src/anon-layout/anon-layout.component.html diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/components/src/anon-layout/anon-layout.component.ts similarity index 82% rename from libs/auth/src/angular/anon-layout/anon-layout.component.ts rename to libs/components/src/anon-layout/anon-layout.component.ts index 1a20dd6fb52..4155a186384 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/components/src/anon-layout/anon-layout.component.ts @@ -9,16 +9,10 @@ import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { IconModule, Icon } from "../../../../components/src/icon"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { SharedModule } from "../../../../components/src/shared"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { TypographyModule } from "../../../../components/src/typography"; -import { BitwardenLogo, BitwardenShield } from "../icons"; +import { IconModule, Icon } from "../icon"; +import { BitwardenLogo, BitwardenShield } from "../icon/icons"; +import { SharedModule } from "../shared"; +import { TypographyModule } from "../typography"; @Component({ selector: "auth-anon-layout", diff --git a/libs/auth/src/angular/anon-layout/anon-layout.mdx b/libs/components/src/anon-layout/anon-layout.mdx similarity index 97% rename from libs/auth/src/angular/anon-layout/anon-layout.mdx rename to libs/components/src/anon-layout/anon-layout.mdx index 8aec3a06767..039a1aa5f28 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.mdx +++ b/libs/components/src/anon-layout/anon-layout.mdx @@ -6,8 +6,8 @@ import * as stories from "./anon-layout.stories"; # AnonLayout Component -The Auth-owned AnonLayoutComponent is to be used primarily for unauthenticated pages\*, where we -don't know who the user is. +The AnonLayoutComponent is to be used primarily for unauthenticated pages\*, where we don't know who +the user is. \*There will be a few exceptions to this—that is, AnonLayout will also be used for the Unlock and View Send pages. diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/components/src/anon-layout/anon-layout.stories.ts similarity index 96% rename from libs/auth/src/angular/anon-layout/anon-layout.stories.ts rename to libs/components/src/anon-layout/anon-layout.stories.ts index 34d561d5210..395703fc018 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/components/src/anon-layout/anon-layout.stories.ts @@ -7,13 +7,9 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { ButtonModule } from "../../../../components/src/button"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service"; -import { LockIcon } from "../icons"; +import { ButtonModule } from "../button"; +import { LockIcon } from "../icon/icons"; +import { I18nMockService } from "../utils/i18n-mock.service"; import { AnonLayoutComponent } from "./anon-layout.component"; @@ -23,7 +19,7 @@ class MockPlatformUtilsService implements Partial { } export default { - title: "Auth/Anon Layout", + title: "Component Library/Anon Layout", component: AnonLayoutComponent, decorators: [ moduleMetadata({ @@ -38,6 +34,7 @@ export default { useFactory: () => { return new I18nMockService({ accessing: "Accessing", + appLogoLabel: "app logo label", }); }, }, diff --git a/libs/auth/src/angular/anon-layout/default-anon-layout-wrapper-data.service.ts b/libs/components/src/anon-layout/default-anon-layout-wrapper-data.service.ts similarity index 100% rename from libs/auth/src/angular/anon-layout/default-anon-layout-wrapper-data.service.ts rename to libs/components/src/anon-layout/default-anon-layout-wrapper-data.service.ts diff --git a/libs/components/src/anon-layout/index.ts b/libs/components/src/anon-layout/index.ts new file mode 100644 index 00000000000..764360e85dd --- /dev/null +++ b/libs/components/src/anon-layout/index.ts @@ -0,0 +1,4 @@ +export * from "./anon-layout-wrapper-data.service"; +export * from "./anon-layout-wrapper.component"; +export * from "./anon-layout.component"; +export * from "./default-anon-layout-wrapper-data.service"; diff --git a/libs/auth/src/angular/icons/bitwarden-logo.icon.ts b/libs/components/src/icon/icons/bitwarden-logo.icon.ts similarity index 96% rename from libs/auth/src/angular/icons/bitwarden-logo.icon.ts rename to libs/components/src/icon/icons/bitwarden-logo.icon.ts index 2df07c45ff9..27b8ece164d 100644 --- a/libs/auth/src/angular/icons/bitwarden-logo.icon.ts +++ b/libs/components/src/icon/icons/bitwarden-logo.icon.ts @@ -1,6 +1,4 @@ -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { svgIcon } from "@bitwarden/components"; +import { svgIcon } from "../icon"; export const BitwardenLogo = svgIcon` diff --git a/libs/auth/src/angular/icons/bitwarden-shield.icon.ts b/libs/components/src/icon/icons/bitwarden-shield.icon.ts similarity index 85% rename from libs/auth/src/angular/icons/bitwarden-shield.icon.ts rename to libs/components/src/icon/icons/bitwarden-shield.icon.ts index f40dc97e5ee..7abeaf40e3c 100644 --- a/libs/auth/src/angular/icons/bitwarden-shield.icon.ts +++ b/libs/components/src/icon/icons/bitwarden-shield.icon.ts @@ -1,6 +1,4 @@ -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { svgIcon } from "@bitwarden/components"; +import { svgIcon } from "../icon"; export const BitwardenShield = svgIcon` diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts b/libs/components/src/icon/icons/extension-bitwarden-logo.icon.ts similarity index 99% rename from apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts rename to libs/components/src/icon/icons/extension-bitwarden-logo.icon.ts index 1de4bd37239..a8a07d5d1ef 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts +++ b/libs/components/src/icon/icons/extension-bitwarden-logo.icon.ts @@ -1,4 +1,4 @@ -import { svgIcon } from "@bitwarden/components"; +import { svgIcon } from "../icon"; export const ExtensionBitwardenLogo = svgIcon` diff --git a/libs/auth/src/angular/icons/registration-check-email.icon.ts b/libs/components/src/icon/icons/registration-check-email.icon.ts similarity index 90% rename from libs/auth/src/angular/icons/registration-check-email.icon.ts rename to libs/components/src/icon/icons/registration-check-email.icon.ts index d32964d8cb1..f0e881e5b2d 100644 --- a/libs/auth/src/angular/icons/registration-check-email.icon.ts +++ b/libs/components/src/icon/icons/registration-check-email.icon.ts @@ -1,6 +1,4 @@ -// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. -// eslint-disable-next-line no-restricted-imports -import { svgIcon } from "@bitwarden/components"; +import { svgIcon } from "../icon"; export const RegistrationCheckEmailIcon = svgIcon` diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 284dc639746..d231048563c 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,5 +1,6 @@ export { ButtonType, ButtonLikeAbstraction } from "./shared/button-like.abstraction"; export * from "./a11y"; +export * from "./anon-layout"; export * from "./async-actions"; export * from "./avatar"; export * from "./badge-list"; diff --git a/libs/key-management-ui/src/lock/components/lock.component.ts b/libs/key-management-ui/src/lock/components/lock.component.ts index 89796148e23..cd731629b48 100644 --- a/libs/key-management-ui/src/lock/components/lock.component.ts +++ b/libs/key-management-ui/src/lock/components/lock.component.ts @@ -16,7 +16,6 @@ import { } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; import { LogoutService, PinServiceAbstraction } from "@bitwarden/auth/common"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; @@ -42,6 +41,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { UserKey } from "@bitwarden/common/types/key"; import { AsyncActionsModule, + AnonLayoutWrapperDataService, ButtonModule, DialogService, FormFieldModule, From e8e61d2796106cd80784cc301be0091566165d1a Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 17 Jun 2025 13:57:27 -0400 Subject: [PATCH 07/31] build(ci): remove the need to cherry pick version bumps to rc (#15188) --- .github/workflows/repository-management.yml | 102 ++++---------------- 1 file changed, 20 insertions(+), 82 deletions(-) diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 8ab74adf543..d91e0a12afd 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -36,8 +36,7 @@ on: description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" required: false type: string - - +permissions: {} jobs: setup: name: Setup @@ -57,51 +56,11 @@ jobs: fi echo "branch=$BRANCH" >> $GITHUB_OUTPUT - - - cut_branch: - name: Cut branch - if: ${{ needs.setup.outputs.branch == 'rc' }} - needs: setup - runs-on: ubuntu-24.04 - steps: - - name: Generate GH App token - uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 - id: app-token - with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} - - - name: Check out target ref - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{ inputs.target_ref }} - token: ${{ steps.app-token.outputs.token }} - - - name: Check if ${{ needs.setup.outputs.branch }} branch exists - env: - BRANCH_NAME: ${{ needs.setup.outputs.branch }} - run: | - if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then - echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY - exit 1 - fi - - - name: Cut branch - env: - BRANCH_NAME: ${{ needs.setup.outputs.branch }} - run: | - git switch --quiet --create $BRANCH_NAME - git push --quiet --set-upstream origin $BRANCH_NAME - - bump_version: name: Bump Version if: ${{ always() }} runs-on: ubuntu-24.04 - needs: - - cut_branch - - setup + needs: setup outputs: version_browser: ${{ steps.set-final-version-output.outputs.version_browser }} version_cli: ${{ steps.set-final-version-output.outputs.version_cli }} @@ -441,15 +400,13 @@ jobs: - name: Push changes if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} run: git push - - - cherry_pick: - name: Cherry-Pick Commit(s) + cut_branch: + name: Cut branch if: ${{ needs.setup.outputs.branch == 'rc' }} - runs-on: ubuntu-24.04 needs: - - bump_version - setup + - bump_version + runs-on: ubuntu-24.04 steps: - name: Generate GH App token uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3 @@ -458,43 +415,24 @@ jobs: app-id: ${{ secrets.BW_GHAPP_ID }} private-key: ${{ secrets.BW_GHAPP_KEY }} - - name: Check out main branch + - name: Check out target ref uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - fetch-depth: 0 - ref: main + ref: ${{ inputs.target_ref }} token: ${{ steps.app-token.outputs.token }} - - name: Configure Git + - name: Check if ${{ needs.setup.outputs.branch }} branch exists + env: + BRANCH_NAME: ${{ needs.setup.outputs.branch }} run: | - git config --local user.email "actions@github.com" - git config --local user.name "Github Actions" + if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then + echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY + exit 1 + fi - - name: Perform cherry-pick(s) + - name: Cut branch + env: + BRANCH_NAME: ${{ needs.setup.outputs.branch }} run: | - # Function for cherry-picking - cherry_pick () { - local package_path="apps/$1/package.json" - local source_branch=$2 - local destination_branch=$3 - - # Get project commit/version from source branch - git switch $source_branch - SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 $package_path) - SOURCE_VERSION=$(cat $package_path | jq -r '.version') - - # Get project commit/version from destination branch - git switch $destination_branch - DESTINATION_VERSION=$(cat $package_path | jq -r '.version') - - if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then - git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT - git push -u origin $destination_branch - fi - } - - # Cherry-pick from 'main' into 'rc' - cherry_pick browser main rc - cherry_pick cli main rc - cherry_pick desktop main rc - cherry_pick web main rc + git switch --quiet --create $BRANCH_NAME + git push --quiet --set-upstream origin $BRANCH_NAME \ No newline at end of file From 82877e9b97b69a288810c339c03d9ecc925864c9 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:26:30 -0400 Subject: [PATCH 08/31] [PM-18766] Changes to use the new Assign Collections Component in Desktop (#14180) * Initial changes to use the new Assign Collections Component in Desktop * Renaming component properly and adding the missing messages.json entries * Adding an option in right click menu to assign to collections * lint fix * prettier * updates so that the feature flag being on will show the new assign collections dialog * lint fix * set collections property after updating cipher collections * update revision date from server response in shareManyWithServer * Removing changes from non-feature flagged files, fixing the refresh issue * return CipherResponse instead of Record * adding in the master password reprompt check if they try and share --------- Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com> Co-authored-by: jaasen-livefront --- apps/desktop/src/app/app.module.ts | 2 + apps/desktop/src/locales/en/messages.json | 134 ++++++++++++++++++ .../assign-collections-desktop.component.html | 33 +++++ .../assign-collections-desktop.component.ts | 36 +++++ .../app/vault/assign-collections/index.ts | 1 + .../src/vault/app/vault/vault-v2.component.ts | 74 +++++++++- 6 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.html create mode 100644 apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts create mode 100644 apps/desktop/src/vault/app/vault/assign-collections/index.ts diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 9b2472106dd..58c3e10e334 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -9,6 +9,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { CalloutModule, DialogModule } from "@bitwarden/components"; +import { AssignCollectionsComponent } from "@bitwarden/vault"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginModule } from "../auth/login/login.module"; @@ -55,6 +56,7 @@ import { SharedModule } from "./shared/shared.module"; DeleteAccountComponent, UserVerificationComponent, NavComponent, + AssignCollectionsComponent, VaultV2Component, ], declarations: [ diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 0cc466196fb..1685de7d8d4 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -3812,5 +3812,139 @@ "message": "Learn more about SSH agent", "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" + }, + "assignToCollections": { + "message": "Assign to collections" + }, + "assignToTheseCollections": { + "message": "Assign to these collections" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "noCollectionsAssigned": { + "message": "No collections have been assigned" + }, + "assign": { + "message": "Assign" + }, + "bulkCollectionAssignmentDialogDescription": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemsTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + } + } + }, + "personalItemsWithOrgTransferWarning": { + "message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2 items" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + } } } diff --git a/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.html b/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.html new file mode 100644 index 00000000000..4f5b6234ad9 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.html @@ -0,0 +1,33 @@ + + + {{ "assignToCollections" | i18n }} + + {{ editableItemCount | pluralize: ("item" | i18n) : ("items" | i18n) }} + + + +
+ +
+ + + + + + diff --git a/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts b/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts new file mode 100644 index 00000000000..d81f1662c6c --- /dev/null +++ b/apps/desktop/src/vault/app/vault/assign-collections/assign-collections-desktop.component.ts @@ -0,0 +1,36 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PluralizePipe } from "@bitwarden/angular/pipes/pluralize.pipe"; +import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; +import { + AssignCollectionsComponent, + CollectionAssignmentParams, + CollectionAssignmentResult, +} from "@bitwarden/vault"; + +@Component({ + standalone: true, + templateUrl: "./assign-collections-desktop.component.html", + imports: [AssignCollectionsComponent, PluralizePipe, DialogModule, ButtonModule, JslibModule], +}) +export class AssignCollectionsDesktopComponent { + protected editableItemCount: number; + + constructor( + @Inject(DIALOG_DATA) public params: CollectionAssignmentParams, + private dialogRef: DialogRef, + ) {} + + protected async onCollectionAssign(result: CollectionAssignmentResult) { + this.dialogRef.close(result); + } + + static open(dialogService: DialogService, config: DialogConfig) { + return dialogService.open( + AssignCollectionsDesktopComponent, + config, + ); + } +} diff --git a/apps/desktop/src/vault/app/vault/assign-collections/index.ts b/apps/desktop/src/vault/app/vault/assign-collections/index.ts new file mode 100644 index 00000000000..1afe7128757 --- /dev/null +++ b/apps/desktop/src/vault/app/vault/assign-collections/index.ts @@ -0,0 +1 @@ +export * from "./assign-collections-desktop.component"; diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 50e6bfb51c7..7a457fb3a44 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -9,7 +9,7 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom } from "rxjs"; +import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observable } from "rxjs"; import { filter, map, take } from "rxjs/operators"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; @@ -19,6 +19,8 @@ import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/vie import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -57,6 +59,7 @@ import { CipherFormMode, CipherFormModule, CipherViewComponent, + CollectionAssignmentResult, DecryptionFailureDialogComponent, DefaultChangeLoginPasswordService, DefaultCipherFormConfigService, @@ -69,6 +72,7 @@ import { DesktopCredentialGenerationService } from "../../../services/desktop-ci import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service"; import { invokeMenu, RendererMenuItem } from "../../../utils"; +import { AssignCollectionsDesktopComponent } from "./assign-collections"; import { ItemFooterComponent } from "./item-footer.component"; import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; import { VaultFilterModule } from "./vault-filter/vault-filter.module"; @@ -142,6 +146,11 @@ export class VaultV2Component implements OnInit, OnDestroy { config: CipherFormConfig | null = null; isSubmitting = false; + private organizations$: Observable = this.accountService.activeAccount$.pipe( + map((a) => a?.id), + switchMap((id) => this.organizationService.organizations$(id)), + ); + protected canAccessAttachments$ = this.accountService.activeAccount$.pipe( filter((account): account is Account => !!account), switchMap((account) => @@ -151,6 +160,8 @@ export class VaultV2Component implements OnInit, OnDestroy { private modal: ModalRef | null = null; private componentIsDestroyed$ = new Subject(); + private allOrganizations: Organization[] = []; + private allCollections: CollectionView[] = []; constructor( private route: ActivatedRoute, @@ -176,6 +187,7 @@ export class VaultV2Component implements OnInit, OnDestroy { private formConfigService: CipherFormConfigService, private premiumUpgradePromptService: PremiumUpgradePromptService, private collectionService: CollectionService, + private organizationService: OrganizationService, private folderService: FolderService, ) {} @@ -312,6 +324,16 @@ export class VaultV2Component implements OnInit, OnDestroy { }); }); } + + this.organizations$.pipe(takeUntil(this.componentIsDestroyed$)).subscribe((orgs) => { + this.allOrganizations = orgs; + }); + + this.collectionService.decryptedCollections$ + .pipe(takeUntil(this.componentIsDestroyed$)) + .subscribe((collections) => { + this.allCollections = collections; + }); } ngOnDestroy() { @@ -420,6 +442,16 @@ export class VaultV2Component implements OnInit, OnDestroy { }, }); } + + if (cipher.canAssignToCollections) { + menu.push({ + label: this.i18nService.t("assignToCollections"), + click: () => + this.functionWithChangeDetection(async () => { + await this.shareCipher(cipher); + }), + }); + } } switch (cipher.type) { @@ -531,6 +563,36 @@ export class VaultV2Component implements OnInit, OnDestroy { await this.go().catch(() => {}); } + async shareCipher(cipher: CipherView) { + if (!cipher) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nothingSelected"), + }); + return; + } + + if (!(await this.passwordReprompt(cipher))) { + return; + } + + const availableCollections = this.getAvailableCollections(cipher); + + const dialog = AssignCollectionsDesktopComponent.open(this.dialogService, { + data: { + ciphers: [cipher], + organizationId: cipher.organizationId as OrganizationId, + availableCollections, + }, + }); + + const result = await lastValueFrom(dialog.closed); + if (result === CollectionAssignmentResult.Saved) { + await this.savedCipher(cipher); + } + } + async addCipher(type: CipherType) { if (this.action === "add") { return; @@ -603,6 +665,16 @@ export class VaultV2Component implements OnInit, OnDestroy { await this.go().catch(() => {}); } + private getAvailableCollections(cipher: CipherView): CollectionView[] { + const orgId = cipher.organizationId; + if (!orgId || orgId === "MyVault") { + return []; + } + + const organization = this.allOrganizations.find((o) => o.id === orgId); + return this.allCollections.filter((c) => c.organizationId === organization?.id && !c.readOnly); + } + private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string { if (vaultFilter.status === "favorites") { return "searchFavorites"; From 05b34e9d00bf44b69bd1df67a85e604ab00f604a Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 17 Jun 2025 14:31:11 -0400 Subject: [PATCH 09/31] PM-21160 (#15125) --- .../platform/popup/view-cache/popup-router-cache.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts index 5fc508ac2a6..bac435e2e8d 100644 --- a/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts @@ -105,9 +105,11 @@ export class PopupRouterCacheService { * Navigate back in history */ async back() { - await this.state.update((prevState) => (prevState ? prevState.slice(0, -1) : [])); + const history = await this.state.update((prevState) => + prevState ? prevState.slice(0, -1) : [], + ); - if (this.hasNavigated) { + if (this.hasNavigated && history.length) { this.location.back(); return; } From 71bc68444da723c4f7ed7909f871808910bbdb17 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Tue, 17 Jun 2025 14:47:10 -0400 Subject: [PATCH 10/31] [PM-22419] dismiss account nudge when biometric unlock is set (#15139) * update account-security-nudge service to look at biomentricUnlockEnabled$ observable, add success toast for biometric unlock --- apps/browser/src/_locales/en/messages.json | 3 + .../settings/account-security.component.ts | 5 ++ .../account-security-nudge.service.ts | 56 +++++++++++++++---- .../src/vault/services/nudges.service.spec.ts | 15 +++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index e8834b3ffdb..a9d2d75d64c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5062,6 +5062,9 @@ "unlockPinSet": { "message": "Unlock PIN set" }, + "unlockBiometricSet": { + "message": "Unlock biometrics set" + }, "authenticating": { "message": "Authenticating" }, 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 1fc4650b6f5..19f2d94e451 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -534,6 +534,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { if (!successful) { await this.biometricStateService.setFingerprintValidated(false); } + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("unlockBiometricSet"), + }); } catch (error) { this.form.controls.biometric.setValue(false); this.validationService.showError(error); diff --git a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts index 30bbd153c5e..99d7f3934ff 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/account-security-nudge.service.ts @@ -1,14 +1,18 @@ import { Injectable, inject } from "@angular/core"; import { Observable, combineLatest, from, of } from "rxjs"; -import { catchError, map } from "rxjs/operators"; +import { catchError, switchMap } from "rxjs/operators"; import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "@bitwarden/auth/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricStateService } from "@bitwarden/key-management"; import { DefaultSingleNudgeService } from "../default-single-nudge.service"; import { NudgeStatus, NudgeType } from "../nudges.service"; @@ -21,6 +25,9 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService { private logService = inject(LogService); private pinService = inject(PinServiceAbstraction); private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); + private biometricStateService = inject(BiometricStateService); + private policyService = inject(PolicyService); + private organizationService = inject(OrganizationService); nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable { const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe( @@ -36,16 +43,45 @@ export class AccountSecurityNudgeService extends DefaultSingleNudgeService { this.getNudgeStatus$(nudgeType, userId), of(Date.now() - THIRTY_DAYS_MS), from(this.pinService.isPinSet(userId)), - from(this.vaultTimeoutSettingsService.isBiometricLockSet(userId)), + this.biometricStateService.biometricUnlockEnabled$, + this.organizationService.organizations$(userId), + this.policyService.policiesByType$(PolicyType.RemoveUnlockWithPin, userId), ]).pipe( - map(([profileCreationDate, status, profileCutoff, isPinSet, isBiometricLockSet]) => { - const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff; - const hideNudge = profileOlderThanCutoff || isPinSet || isBiometricLockSet; - return { - hasBadgeDismissed: status.hasBadgeDismissed || hideNudge, - hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge, - }; - }), + switchMap( + async ([ + profileCreationDate, + status, + profileCutoff, + isPinSet, + biometricUnlockEnabled, + organizations, + policies, + ]) => { + const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff; + + const hasOrgWithRemovePinPolicyOn = organizations.some((org) => { + return policies.some( + (p) => p.type === PolicyType.RemoveUnlockWithPin && p.organizationId === org.id, + ); + }); + + const hideNudge = + profileOlderThanCutoff || + isPinSet || + biometricUnlockEnabled || + hasOrgWithRemovePinPolicyOn; + + const acctSecurityNudgeStatus = { + hasBadgeDismissed: status.hasBadgeDismissed || hideNudge, + hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge, + }; + + if (isPinSet || biometricUnlockEnabled || hasOrgWithRemovePinPolicyOn) { + await this.setNudgeStatus(nudgeType, acctSecurityNudgeStatus, userId); + } + return acctSecurityNudgeStatus; + }, + ), ); } } diff --git a/libs/angular/src/vault/services/nudges.service.spec.ts b/libs/angular/src/vault/services/nudges.service.spec.ts index f18d846232c..bf84674c669 100644 --- a/libs/angular/src/vault/services/nudges.service.spec.ts +++ b/libs/angular/src/vault/services/nudges.service.spec.ts @@ -6,6 +6,8 @@ import { firstValueFrom, of } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -13,6 +15,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { BiometricStateService } from "@bitwarden/key-management"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../../libs/common/spec"; @@ -91,6 +94,18 @@ describe("Vault Nudges Service", () => { provide: VaultTimeoutSettingsService, useValue: mock(), }, + { + provide: BiometricStateService, + useValue: mock(), + }, + { + provide: PolicyService, + useValue: mock(), + }, + { + provide: OrganizationService, + useValue: mock(), + }, ], }); }); From 6a273a3891721217e509fac161a9a8979fac96b9 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 17 Jun 2025 16:21:58 -0400 Subject: [PATCH 11/31] Removed isEnterpriseOrgGuard as that is covered in the canAccessIntegrations function (#15231) --- .../organizations/organization-routing.module.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts index 4d8971f74fd..ab32a0b1eef 100644 --- a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts @@ -17,7 +17,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { deepLinkGuard } from "../../auth/guards/deep-link/deep-link.guard"; import { VaultModule } from "./collections/vault.module"; -import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard"; import { organizationPermissionsGuard } from "./guards/org-permissions.guard"; import { organizationRedirectGuard } from "./guards/org-redirect.guard"; import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component"; @@ -42,10 +41,7 @@ const routes: Routes = [ }, { path: "integrations", - canActivate: [ - isEnterpriseOrgGuard(false), - organizationPermissionsGuard(canAccessIntegrations), - ], + canActivate: [organizationPermissionsGuard(canAccessIntegrations)], component: AdminConsoleIntegrationsComponent, data: { titleId: "integrations", From 58b53f73386a8234e7a2dd21203456988afda0a5 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:53:53 -0500 Subject: [PATCH 12/31] log viewed event when viewing a cipher on desktop (#15234) --- apps/desktop/src/vault/app/vault/vault-v2.component.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 7a457fb3a44..5ca4d929809 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -386,7 +386,14 @@ export class VaultV2Component implements OnInit, OnDestroy { cipher.collectionIds.includes(c.id), ) ?? null; this.action = "view"; + await this.go().catch(() => {}); + await this.eventCollectionService.collect( + EventType.Cipher_ClientViewed, + cipher.id, + false, + cipher.organizationId, + ); } async openAttachmentsDialog() { From b2b695a705d9981c17421d1548d1d5d11b3efb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Wed, 18 Jun 2025 11:31:00 +0200 Subject: [PATCH 13/31] PM-21553: Added support for credential.toJSON() (#15028) * Added support for credential.toJSON() * Changed to import type --- .../autofill/fido2/utils/webauthn-utils.ts | 2 + .../platform/services/fido2/fido2-utils.ts | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts index c8bcf5faa4b..0cccd91876d 100644 --- a/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts +++ b/apps/browser/src/autofill/fido2/utils/webauthn-utils.ts @@ -88,6 +88,7 @@ export class WebauthnUtils { getClientExtensionResults: () => ({ credProps: result.extensions.credProps, }), + toJSON: () => Fido2Utils.createResultToJson(result), } as PublicKeyCredential; // Modify prototype chains to fix `instanceof` calls. @@ -134,6 +135,7 @@ export class WebauthnUtils { } as AuthenticatorAssertionResponse, getClientExtensionResults: () => ({}), authenticatorAttachment: "platform", + toJSON: () => Fido2Utils.getResultToJson(result), } as PublicKeyCredential; // Modify prototype chains to fix `instanceof` calls. diff --git a/libs/common/src/platform/services/fido2/fido2-utils.ts b/libs/common/src/platform/services/fido2/fido2-utils.ts index b9f3c8f8c48..6413eeade04 100644 --- a/libs/common/src/platform/services/fido2/fido2-utils.ts +++ b/libs/common/src/platform/services/fido2/fido2-utils.ts @@ -1,6 +1,45 @@ // FIXME: Update this file to be type safe and remove this and next line +import type { + AssertCredentialResult, + CreateCredentialResult, +} from "../../abstractions/fido2/fido2-client.service.abstraction"; + // @ts-strict-ignore export class Fido2Utils { + static createResultToJson(result: CreateCredentialResult): any { + return { + id: result.credentialId, + rawId: result.credentialId, + response: { + clientDataJSON: result.clientDataJSON, + authenticatorData: result.authData, + transports: result.transports, + publicKey: result.publicKey, + publicKeyAlgorithm: result.publicKeyAlgorithm, + attestationObject: result.attestationObject, + }, + authenticatorAttachment: "platform", + clientExtensionResults: result.extensions, + type: "public-key", + }; + } + + static getResultToJson(result: AssertCredentialResult): any { + return { + id: result.credentialId, + rawId: result.credentialId, + response: { + clientDataJSON: result.clientDataJSON, + authenticatorData: result.authenticatorData, + signature: result.signature, + userHandle: result.userHandle, + }, + authenticatorAttachment: "platform", + clientExtensionResults: {}, + type: "public-key", + }; + } + static bufferToString(bufferSource: BufferSource): string { return Fido2Utils.fromBufferToB64(Fido2Utils.bufferSourceToUint8Array(bufferSource)) .replace(/\+/g, "-") From 2f47a90e790f0416b434117f45b5a771715aac60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85berg?= Date: Wed, 18 Jun 2025 11:31:11 +0200 Subject: [PATCH 14/31] Allow string 'true' instead of true (#14816) --- .../fido2/fido2-client.service.spec.ts | 21 +++++++++++++++++++ .../services/fido2/fido2-client.service.ts | 6 +++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts index 51c3d8617ab..4fd91fb19e6 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.spec.ts @@ -92,6 +92,27 @@ describe("FidoAuthenticatorService", () => { }); describe("createCredential", () => { + describe("Mapping params should handle variations in input formats", () => { + it.each([ + [true, true], + [false, false], + ["false", false], + ["", false], + ["true", true], + ])("requireResidentKey should handle %s as boolean %s", async (input, expected) => { + const params = createParams({ + authenticatorSelection: { requireResidentKey: input as any }, + extensions: { credProps: true }, + }); + + authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult()); + + const result = await client.createCredential(params, windowReference); + + expect(result.extensions.credProps?.rk).toBe(expected); + }); + }); + describe("input parameters validation", () => { // Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException. it("should throw error if sameOriginWithAncestors is false", async () => { diff --git a/libs/common/src/platform/services/fido2/fido2-client.service.ts b/libs/common/src/platform/services/fido2/fido2-client.service.ts index 2445cd366de..5d5f2a879cb 100644 --- a/libs/common/src/platform/services/fido2/fido2-client.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-client.service.ts @@ -483,11 +483,15 @@ function mapToMakeCredentialParams({ type: credential.type, })) ?? []; + /** + * Quirk: Accounts for the fact that some RP's mistakenly submits 'requireResidentKey' as a string + */ const requireResidentKey = params.authenticatorSelection?.residentKey === "required" || params.authenticatorSelection?.residentKey === "preferred" || (params.authenticatorSelection?.residentKey === undefined && - params.authenticatorSelection?.requireResidentKey === true); + (params.authenticatorSelection?.requireResidentKey === true || + (params.authenticatorSelection?.requireResidentKey as unknown as string) === "true")); const requireUserVerification = params.authenticatorSelection?.userVerification === "required" || From f8618bc33555eeba51a11322c55c316265f08099 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:20:52 +0200 Subject: [PATCH 15/31] [deps] Platform: Update @electron/notarize to v3 (#14511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- package-lock.json | 60 +++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b91c139a55..19ea5740057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,7 +85,7 @@ "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", - "@electron/notarize": "2.5.0", + "@electron/notarize": "3.0.1", "@electron/rebuild": "3.7.2", "@eslint/compat": "1.2.9", "@lit-labs/signals": "0.1.2", @@ -5402,34 +5402,17 @@ } }, "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.0.1.tgz", + "integrity": "sha512-5xzcOwvMGNjkSk7s0sPx4XcKWei9FYk4f2S5NkSorWW0ce5yktTOtlPa0W5yQHcREILh+C3JdH+t+M637g9TmQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", + "debug": "^4.4.0", "promise-retry": "^2.0.1" }, "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" + "node": ">= 22.12.0" } }, "node_modules/@electron/osx-sign": { @@ -14324,6 +14307,37 @@ "electron-builder-squirrel-windows": "26.0.12" } }, + "node_modules/app-builder-lib/node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/app-builder-lib/node_modules/@electron/rebuild": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz", diff --git a/package.json b/package.json index d2e480f6762..888e0c24329 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", - "@electron/notarize": "2.5.0", + "@electron/notarize": "3.0.1", "@electron/rebuild": "3.7.2", "@eslint/compat": "1.2.9", "@lit-labs/signals": "0.1.2", From 02a63d4a38605707d36a3124beeee62715156081 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 18 Jun 2025 10:16:25 -0400 Subject: [PATCH 16/31] [PM-22725] [Defect]Title and Username are removed when editing Identity items (#15221) * map sdk identity type back to null when undefined * refactored views to have consistent pattern with other fromSdk methods --- .../common/src/vault/models/view/card.view.ts | 11 ++++++++- .../src/vault/models/view/identity.view.ts | 23 ++++++++++++++++++- .../src/vault/models/view/login.view.ts | 19 +++++++++------ .../src/vault/models/view/secure-note.view.ts | 5 +++- .../src/vault/models/view/ssh-key.view.ts | 10 ++++---- 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts index 2adfbb39e89..dd7f5d6be57 100644 --- a/libs/common/src/vault/models/view/card.view.ts +++ b/libs/common/src/vault/models/view/card.view.ts @@ -157,6 +157,15 @@ export class CardView extends ItemView { return undefined; } - return Object.assign(new CardView(), obj); + const cardView = new CardView(); + + cardView.cardholderName = obj.cardholderName ?? null; + cardView.brand = obj.brand ?? null; + cardView.number = obj.number ?? null; + cardView.expMonth = obj.expMonth ?? null; + cardView.expYear = obj.expYear ?? null; + cardView.code = obj.code ?? null; + + return cardView; } } diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts index a75d11efd95..877940e4aea 100644 --- a/libs/common/src/vault/models/view/identity.view.ts +++ b/libs/common/src/vault/models/view/identity.view.ts @@ -169,6 +169,27 @@ export class IdentityView extends ItemView { return undefined; } - return Object.assign(new IdentityView(), obj); + const identityView = new IdentityView(); + + identityView.title = obj.title ?? null; + identityView.firstName = obj.firstName ?? null; + identityView.middleName = obj.middleName ?? null; + identityView.lastName = obj.lastName ?? null; + identityView.address1 = obj.address1 ?? null; + identityView.address2 = obj.address2 ?? null; + identityView.address3 = obj.address3 ?? null; + identityView.city = obj.city ?? null; + identityView.state = obj.state ?? null; + identityView.postalCode = obj.postalCode ?? null; + identityView.country = obj.country ?? null; + identityView.company = obj.company ?? null; + identityView.email = obj.email ?? null; + identityView.phone = obj.phone ?? null; + identityView.ssn = obj.ssn ?? null; + identityView.username = obj.username ?? null; + identityView.passportNumber = obj.passportNumber ?? null; + identityView.licenseNumber = obj.licenseNumber ?? null; + + return identityView; } } diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 6bdc23f42b1..c6e6ca001e4 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -116,13 +116,18 @@ export class LoginView extends ItemView { return undefined; } - const passwordRevisionDate = - obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); - const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || []; + const loginView = new LoginView(); - return Object.assign(new LoginView(), obj, { - passwordRevisionDate, - uris, - }); + loginView.username = obj.username ?? null; + loginView.password = obj.password ?? null; + loginView.passwordRevisionDate = + obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); + loginView.totp = obj.totp ?? null; + loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null; + loginView.uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || []; + // FIDO2 credentials are not decrypted here, they remain encrypted + loginView.fido2Credentials = null; + + return loginView; } } diff --git a/libs/common/src/vault/models/view/secure-note.view.ts b/libs/common/src/vault/models/view/secure-note.view.ts index 075e4dfc520..8e7a6b4652d 100644 --- a/libs/common/src/vault/models/view/secure-note.view.ts +++ b/libs/common/src/vault/models/view/secure-note.view.ts @@ -37,6 +37,9 @@ export class SecureNoteView extends ItemView { return undefined; } - return Object.assign(new SecureNoteView(), obj); + const secureNoteView = new SecureNoteView(); + secureNoteView.type = obj.type ?? null; + + return secureNoteView; } } diff --git a/libs/common/src/vault/models/view/ssh-key.view.ts b/libs/common/src/vault/models/view/ssh-key.view.ts index a3d091e4c07..a83793678dc 100644 --- a/libs/common/src/vault/models/view/ssh-key.view.ts +++ b/libs/common/src/vault/models/view/ssh-key.view.ts @@ -55,10 +55,12 @@ export class SshKeyView extends ItemView { return undefined; } - const keyFingerprint = obj.fingerprint; + const sshKeyView = new SshKeyView(); - return Object.assign(new SshKeyView(), obj, { - keyFingerprint, - }); + sshKeyView.privateKey = obj.privateKey ?? null; + sshKeyView.publicKey = obj.publicKey ?? null; + sshKeyView.keyFingerprint = obj.fingerprint ?? null; + + return sshKeyView; } } From 97417b6949e75b5235ac4d5bc40396bccb47040e Mon Sep 17 00:00:00 2001 From: Ketan Mehta <45426198+ketanMehtaa@users.noreply.github.com> Date: Wed, 18 Jun 2025 20:13:00 +0530 Subject: [PATCH 17/31] [PM-22253] fixed white background in darkmode (#15020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed white background in darkmode * removed tw-apperance-none typo * changed both Permission from simple to bit-select * Update apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html Co-authored-by: Vicki League * ui change for permission * added SelectModule in Test file * added selectModule in access stories --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> Co-authored-by: Vicki League --- .../access-selector.component.html | 47 ++++++++----------- .../access-selector.component.spec.ts | 2 + .../access-selector.stories.ts | 2 + 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html index 4226862fde7..088b5051fb1 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html @@ -1,22 +1,21 @@
- + {{ "permission" | i18n }} - + + + - + {{ selectorLabelText }} - +
@@ -79,28 +78,22 @@ - -
- - -
+ + + +
diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts index 86c348f0326..a5a632678c9 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts @@ -14,6 +14,7 @@ import { ButtonModule, FormFieldModule, IconButtonModule, + SelectModule, TableModule, TabsModule, } from "@bitwarden/components"; @@ -71,6 +72,7 @@ describe("AccessSelectorComponent", () => { PreloadedEnglishI18nModule, JslibModule, IconButtonModule, + SelectModule, ], declarations: [TestableAccessSelectorComponent, UserTypePipe], providers: [], diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts index 095be1df966..e98160d78d0 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts @@ -10,6 +10,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + SelectModule, TableModule, TabsModule, } from "@bitwarden/components"; @@ -47,6 +48,7 @@ export default { TableModule, JslibModule, IconButtonModule, + SelectModule, ], providers: [], }), From a659c0a32da1577d6af2235f48cf403883554e5b Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 18 Jun 2025 08:32:00 -0700 Subject: [PATCH 18/31] [PM-22734] Patch the cipher form after attachments are modified on Desktop (#15227) --- .../src/vault/app/vault/vault-v2.component.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 5ca4d929809..a84a868f4ca 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -64,6 +64,7 @@ import { DefaultChangeLoginPasswordService, DefaultCipherFormConfigService, PasswordRepromptService, + CipherFormComponent, } from "@bitwarden/vault"; import { NavComponent } from "../../../app/layout/nav.component"; @@ -123,6 +124,8 @@ export class VaultV2Component implements OnInit, OnDestroy { vaultFilterComponent: VaultFilterComponent | null = null; @ViewChild("folderAddEdit", { read: ViewContainerRef, static: true }) folderAddEditModalRef: ViewContainerRef | null = null; + @ViewChild(CipherFormComponent) + cipherFormComponent: CipherFormComponent | null = null; action: CipherFormMode | "view" | null = null; cipherId: string | null = null; @@ -410,6 +413,26 @@ export class VaultV2Component implements OnInit, OnDestroy { result?.action === AttachmentDialogResult.Uploaded ) { await this.vaultItemsComponent?.refresh().catch(() => {}); + + if (this.cipherFormComponent == null) { + return; + } + + const updatedCipher = await this.cipherService.get( + this.cipherId as CipherId, + this.activeUserId as UserId, + ); + const updatedCipherView = await this.cipherService.decrypt( + updatedCipher, + this.activeUserId as UserId, + ); + + this.cipherFormComponent.patchCipher((currentCipher) => { + currentCipher.attachments = updatedCipherView.attachments; + currentCipher.revisionDate = updatedCipherView.revisionDate; + + return currentCipher; + }); } } From 9e764481888f82652e01d8350d6e24cd5ce31584 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:12:21 -0700 Subject: [PATCH 19/31] docs(redirect-guard): [BEEEP] Document redirectGuard (#15196) --- libs/angular/src/auth/guards/index.ts | 2 +- .../src/auth/guards/redirect/README.md | 53 +++++++++++++++++++ .../guards/{ => redirect}/redirect.guard.ts | 14 +++-- 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 libs/angular/src/auth/guards/redirect/README.md rename libs/angular/src/auth/guards/{ => redirect}/redirect.guard.ts (86%) diff --git a/libs/angular/src/auth/guards/index.ts b/libs/angular/src/auth/guards/index.ts index 026848c4b08..8a4d0be8167 100644 --- a/libs/angular/src/auth/guards/index.ts +++ b/libs/angular/src/auth/guards/index.ts @@ -1,6 +1,6 @@ export * from "./auth.guard"; export * from "./active-auth.guard"; export * from "./lock.guard"; -export * from "./redirect.guard"; +export * from "./redirect/redirect.guard"; export * from "./tde-decryption-required.guard"; export * from "./unauth.guard"; diff --git a/libs/angular/src/auth/guards/redirect/README.md b/libs/angular/src/auth/guards/redirect/README.md new file mode 100644 index 00000000000..b7977a553b1 --- /dev/null +++ b/libs/angular/src/auth/guards/redirect/README.md @@ -0,0 +1,53 @@ +# Redirect Guard + +The `redirectGuard` redirects the user based on their `AuthenticationStatus`. It is applied to the root route (`/`). + +
+ +### Order of Operations + +The `redirectGuard` will redirect the user based on the following checks, _in order_: + +- **`AuthenticationStatus.LoggedOut`** → redirect to `/login` +- **`AuthenticationStatus.Unlocked`** → redirect to `/vault` +- **`AuthenticationStatus.Locked`** + - **TDE Locked State** → redirect to `/login-initiated` + - A user is in a TDE Locked State if they meet all 3 of the following conditions + 1. Auth status is `Locked` + 2. TDE is enabled + 3. User has never had a user key (that is, user has not unlocked/decrypted yet) + - **Standard Locked State** → redirect to `/lock` + +
+ +| Order | AuthenticationStatus | Redirect To | +| ----- | ------------------------------------------------------------------------------- | ------------------ | +| 1 | `LoggedOut` | `/login` | +| 2 | `Unlocked` | `/vault` | +| 3 | **TDE Locked State**
`Locked` +
`tdeEnabled` +
`!everHadUserKey` | `/login-initiated` | +| 4 | **Standard Locked State**
`Locked` | `/lock` | + +
+ +### Default Routes and Route Overrides + +The default redirect routes are mapped to object properties: + +```typescript +const defaultRoutes: RedirectRoutes = { + loggedIn: "/vault", + loggedOut: "/login", + locked: "/lock", + notDecrypted: "/login-initiated", +}; +``` + +But when applying the guard to the root route, the developer can override specific redirect routes by passing in a custom object. This is useful for subtle differences in client-specific routing: + +```typescript +// app-routing.module.ts (Browser Extension) +{ + path: "", + canActivate: [redirectGuard({ loggedIn: "/tabs/current"})], +} +``` diff --git a/libs/angular/src/auth/guards/redirect.guard.ts b/libs/angular/src/auth/guards/redirect/redirect.guard.ts similarity index 86% rename from libs/angular/src/auth/guards/redirect.guard.ts rename to libs/angular/src/auth/guards/redirect/redirect.guard.ts index b893614b405..45e552639c8 100644 --- a/libs/angular/src/auth/guards/redirect.guard.ts +++ b/libs/angular/src/auth/guards/redirect/redirect.guard.ts @@ -25,12 +25,14 @@ const defaultRoutes: RedirectRoutes = { }; /** - * Guard that consolidates all redirection logic, should be applied to root route. + * Redirects the user to the appropriate route based on their `AuthenticationStatus`. + * This guard should be applied to the root route. * * TODO: This should return Observable once we can get rid of all the promises */ export function redirectGuard(overrides: Partial = {}): CanActivateFn { const routes = { ...defaultRoutes, ...overrides }; + return async (route) => { const authService = inject(AuthService); const keyService = inject(KeyService); @@ -41,16 +43,21 @@ export function redirectGuard(overrides: Partial = {}): CanActiv const authStatus = await authService.getAuthStatus(); + // Logged Out if (authStatus === AuthenticationStatus.LoggedOut) { return router.createUrlTree([routes.loggedOut], { queryParams: route.queryParams }); } + // Unlocked if (authStatus === AuthenticationStatus.Unlocked) { return router.createUrlTree([routes.loggedIn], { queryParams: route.queryParams }); } - // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the - // login decryption options component. + // Locked: TDE Locked State + // - If user meets all 3 of the following conditions: + // 1. Auth status is Locked + // 2. TDE is enabled + // 3. User has never had a user key (has not decrypted yet) const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); const userId = await firstValueFrom(accountService.activeAccount$.pipe(getUserId)); const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$(userId)); @@ -64,6 +71,7 @@ export function redirectGuard(overrides: Partial = {}): CanActiv return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams }); } + // Locked: Standard Locked State if (authStatus === AuthenticationStatus.Locked) { return router.createUrlTree([routes.locked], { queryParams: route.queryParams }); } From a3d870c6aa84346fe8b118dbd20b9d543a6c4fda Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:45:10 -0400 Subject: [PATCH 20/31] [SM-915 ]Copy update for Service Account - Projects tab (#15073) * Copy update for SM * updates to copy on the service account projects component --- apps/web/src/locales/en/messages.json | 6 +++--- .../projects/service-account-projects.component.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 33468e0b306..6785c20d8f4 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7615,9 +7615,9 @@ "message": "Service account updated", "description": "Notifies that a service account has been updated" }, - "newSaSelectAccess": { - "message": "Type or select projects or secrets", - "description": "Instructions for selecting projects or secrets for a new service account" + "typeOrSelectProjects": { + "message": "Type or select projects", + "description": "Instructions for selecting projects for a service account" }, "newSaTypeToFilter": { "message": "Type to filter", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html index 623542bd33d..8e2889716e8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html @@ -9,7 +9,7 @@ [addButtonMode]="true" [items]="potentialGrantees" [label]="'projects' | i18n" - [hint]="'newSaSelectAccess' | i18n" + [hint]="'typeOrSelectProjects' | i18n" [columnTitle]="'projects' | i18n" [emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n" > From 95667310a2e3231af50553a37a0589c6259b4cc8 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:57:21 -0400 Subject: [PATCH 21/31] [SM-1246] Routing to new machine account after machine account is created (#15080) * Routing to new machine account after machine account is created * Updating the width of the tabbed content during responsive size changes * Removing responsive UI changes --- .../dialog/service-account-dialog.component.ts | 15 ++++++++++++--- .../service-accounts/service-account.service.ts | 14 +++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts index 815ea1dc60c..250e0870ecf 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts @@ -2,9 +2,9 @@ // @ts-strict-ignore import { Component, Inject, OnInit } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogRef, DIALOG_DATA, BitValidators, ToastService } from "@bitwarden/components"; import { ServiceAccountView } from "../../models/view/service-account.view"; @@ -46,8 +46,8 @@ export class ServiceAccountDialogComponent implements OnInit { @Inject(DIALOG_DATA) private data: ServiceAccountOperation, private serviceAccountService: ServiceAccountService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private toastService: ToastService, + private router: Router, ) {} async ngOnInit() { @@ -87,8 +87,17 @@ export class ServiceAccountDialogComponent implements OnInit { let serviceAccountMessage: string; if (this.data.operation == OperationType.Add) { - await this.serviceAccountService.create(this.data.organizationId, serviceAccountView); + const newServiceAccount = await this.serviceAccountService.create( + this.data.organizationId, + serviceAccountView, + ); serviceAccountMessage = this.i18nService.t("machineAccountCreated"); + await this.router.navigate([ + "sm", + this.data.organizationId, + "machine-accounts", + newServiceAccount.id, + ]); } else { await this.serviceAccountService.update( this.data.serviceAccountId, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts index c5d4f979ef4..19382793673 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.service.ts @@ -91,7 +91,10 @@ export class ServiceAccountService { ); } - async create(organizationId: string, serviceAccountView: ServiceAccountView) { + async create( + organizationId: string, + serviceAccountView: ServiceAccountView, + ): Promise { const orgKey = await this.getOrganizationKey(organizationId); const request = await this.getServiceAccountRequest(orgKey, serviceAccountView); const r = await this.apiService.send( @@ -101,9 +104,14 @@ export class ServiceAccountService { true, true, ); - this._serviceAccount.next( - await this.createServiceAccountView(orgKey, new ServiceAccountResponse(r)), + + const serviceAccount = await this.createServiceAccountView( + orgKey, + new ServiceAccountResponse(r), ); + this._serviceAccount.next(serviceAccount); + + return serviceAccount; } async delete(serviceAccounts: ServiceAccountView[]): Promise { From 8d4fc915906cd37c6996e97346e8ab9fd76e3963 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:57:39 -0400 Subject: [PATCH 22/31] Updating responsive width of ProjectPeople, ProjectServiceAcct, ServiceAccountPeople, and ServiceAccountProjects (#15084) --- .../projects/project/project-people.component.html | 2 +- .../projects/project/project-service-accounts.component.html | 2 +- .../people/service-account-people.component.html | 2 +- .../projects/service-account-projects.component.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html index 3f107486e27..cbac54fd7c6 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html @@ -1,5 +1,5 @@
-
+

{{ "projectPeopleDescription" | i18n }}

diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html index 5d22358277f..a3914ac9cf2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html @@ -1,5 +1,5 @@ -
+

{{ "projectMachineAccountsDescription" | i18n }}

diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html index 96f7ae4d2bf..49cafeccc3b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html @@ -1,5 +1,5 @@ -
+

{{ "machineAccountPeopleDescription" | i18n }}

diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html index 8e2889716e8..ab7d90ef078 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html @@ -1,5 +1,5 @@ -
+

{{ "machineAccountProjectsDescription" | i18n }}

From 5fa153e743c01d5716503e20b046902c8739efcd Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:13:38 -0700 Subject: [PATCH 23/31] [PM-20643] - [Vault] [Desktop] Front End Changes to Enforce "Remove card item type policy" (#15176) * add restricted item types to legacy vault components * filter out restricted item types from new menu item in desktop * use CIPHER_MENU_ITEMS * use CIPHER_MENU_ITEMS. move restricted cipher service to common * use move restricted item types service to libs. re-use cipher menu items * add shareReplay. change variable name * move restricted filter to search service. remove unecessary import * add reusable service method * clean up spec * add optional chain * remove duplicate import * move isCipherViewRestricted to service module * fix logic * fix logic * remove extra space --------- Co-authored-by: SmithThe4th --- apps/browser/src/_locales/en/messages.json | 3 + .../popup/settings/autofill.component.ts | 2 +- .../new-item-dropdown-v2.component.spec.ts | 5 +- .../new-item-dropdown-v2.component.ts | 3 +- .../vault-popup-list-filters.service.spec.ts | 5 +- .../vault-popup-list-filters.service.ts | 19 ++-- apps/desktop/src/locales/en/messages.json | 3 + .../vault/app/vault/add-edit.component.html | 4 +- .../src/vault/app/vault/add-edit.component.ts | 16 ++++ .../filters/type-filter.component.html | 90 ++++--------------- .../filters/type-filter.component.ts | 20 ++++- .../app/vault/vault-items-v2.component.html | 26 ++---- .../app/vault/vault-items-v2.component.ts | 4 +- .../vault/app/vault/vault-items.component.ts | 4 +- .../vault-filter/vault-filter.component.ts | 2 +- .../vault-items/vault-items.stories.ts | 2 +- .../components/vault-filter.component.ts | 2 +- .../shared/models/filter-function.spec.ts | 2 +- .../shared/models/filter-function.ts | 24 ++--- .../vault-header/vault-header.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 2 +- .../src/services/jslib-services.module.ts | 6 ++ .../vault/components/add-edit.component.ts | 10 --- .../vault/components/vault-items.component.ts | 29 +++++- libs/common/src/vault/service-utils.ts | 1 + .../restricted-item-types.service.spec.ts | 20 ++--- .../services/restricted-item-types.service.ts | 25 +++++- .../src/vault/types/cipher-menu-items.ts | 2 +- libs/vault/src/index.ts | 4 - 29 files changed, 166 insertions(+), 171 deletions(-) rename libs/{vault/src => common/src/vault}/services/restricted-item-types.service.spec.ts (89%) rename libs/{vault/src => common/src/vault}/services/restricted-item-types.service.ts (79%) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a9d2d75d64c..2d29efcc89e 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1922,6 +1922,9 @@ "typeSshKey": { "message": "SSH key" }, + "typeNote": { + "message": "Note" + }, "newItemHeader": { "message": "New $TYPE$", "placeholders": { diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 2b58c32c926..852b79cad1d 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -45,6 +45,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CardComponent, CheckboxModule, @@ -58,7 +59,6 @@ import { SelectModule, TypographyModule, } from "@bitwarden/components"; -import { RestrictedItemTypesService } from "@bitwarden/vault"; import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts index cc97027c82e..7e3db27640e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.spec.ts @@ -12,8 +12,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { + RestrictedCipherType, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; -import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; 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 caffd5e7119..fd7a0c4672b 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 @@ -8,9 +8,10 @@ import { map, Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CipherMenuItem, CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components"; -import { AddEditFolderDialogComponent, RestrictedItemTypesService } from "@bitwarden/vault"; +import { AddEditFolderDialogComponent } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index f8351fe0f61..8b2786fab77 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -20,7 +20,10 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault"; +import { + RestrictedCipherType, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CachedFilterState, diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index a3e5fc4c2bd..9f7363afd7e 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -39,9 +39,12 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { + isCipherViewRestricted, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; import { ChipSelectOption } from "@bitwarden/components"; -import { RestrictedItemTypesService } from "@bitwarden/vault"; const FILTER_VISIBILITY_KEY = new KeyDefinition(VAULT_SETTINGS_DISK, "filterVisibility", { deserializer: (obj) => obj, @@ -227,18 +230,8 @@ export class VaultPopupListFiltersService { } // Check if cipher type is restricted (with organization exemptions) - if (restrictions && restrictions.length > 0) { - const isRestricted = restrictions.some( - (restrictedType) => - restrictedType.cipherType === cipher.type && - (cipher.organizationId - ? !restrictedType.allowViewOrgIds.includes(cipher.organizationId) - : restrictedType.allowViewOrgIds.length === 0), - ); - - if (isRestricted) { - return false; - } + if (isCipherViewRestricted(cipher, restrictions)) { + return false; } if (filters.cipherType !== null && cipher.type !== filters.cipherType) { diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 1685de7d8d4..1431ab72020 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -23,6 +23,9 @@ "typeIdentity": { "message": "Identity" }, + "typeNote": { + "message": "Note" + }, "typeSecureNote": { "message": "Secure note" }, diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.html b/apps/desktop/src/vault/app/vault/add-edit.component.html index 9c316813d1d..2cd384885ce 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit.component.html @@ -12,7 +12,9 @@
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 eb04003a418..e9b18270f2d 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -3,6 +3,7 @@ import { DatePipe } from "@angular/common"; import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { NgForm } from "@angular/forms"; +import { map, shareReplay } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; @@ -22,6 +23,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault"; @@ -35,6 +38,18 @@ const BroadcasterSubscriptionId = "AddEditComponent"; export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy { @ViewChild("form") private form: NgForm; + menuItems$ = this.restrictedItemTypesService.restricted$.pipe( + map((restrictedItemTypes) => + // Filter out restricted item types from the default CIPHER_MENU_ITEMS array + CIPHER_MENU_ITEMS.filter( + (typeOption) => + !restrictedItemTypes.some( + (restrictedType) => restrictedType.cipherType === typeOption.type, + ), + ), + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); constructor( cipherService: CipherService, @@ -59,6 +74,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On cipherAuthorizationService: CipherAuthorizationService, sdkService: SdkService, sshImportPromptService: SshImportPromptService, + protected restrictedItemTypesService: RestrictedItemTypesService, ) { super( cipherService, 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 c3dcd191dfc..f8a83e01266 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 @@ -20,78 +20,20 @@
    -
  • - - - -
  • -
  • - - - -
  • -
  • - - - -
  • -
  • - - - -
  • -
  • - - - -
  • + @for (typeFilter of typeFilters$ | async; track typeFilter) { +
  • + + + +
  • + }
diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts index 5920233b206..27e7d5c5ecb 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.ts @@ -1,6 +1,9 @@ import { Component } from "@angular/core"; +import { map, shareReplay } from "rxjs"; import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; @Component({ selector: "app-type-filter", @@ -8,7 +11,22 @@ import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angul standalone: false, }) export class TypeFilterComponent extends BaseTypeFilterComponent { - constructor() { + protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe( + map((restrictedItemTypes) => + // Filter out restricted item types from the typeFilters array + CIPHER_MENU_ITEMS.filter( + (typeFilter) => + !restrictedItemTypes.some( + (restrictedType) => + restrictedType.allowViewOrgIds.length === 0 && + restrictedType.cipherType === typeFilter.type, + ), + ), + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + constructor(private restrictedItemTypesService: RestrictedItemTypesService) { super(); } } diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html index 63e648e3cf3..fcf38ee39bc 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.html @@ -72,25 +72,11 @@ - - - - - + @for (itemTypes of itemTypes$ | async; track itemTypes.type) { + + } diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts index 5a832ed79b0..1256c9e52e8 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { MenuModule } from "@bitwarden/components"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; @@ -25,8 +26,9 @@ export class VaultItemsV2Component extends BaseVaultItemsComponent { private readonly searchBarService: SearchBarService, cipherService: CipherService, accountService: AccountService, + restrictedItemTypesService: RestrictedItemTypesService, ) { - super(searchService, cipherService, accountService); + super(searchService, cipherService, accountService, restrictedItemTypesService); this.searchBarService.searchText$ .pipe(distinctUntilChanged(), takeUntilDestroyed()) diff --git a/apps/desktop/src/vault/app/vault/vault-items.component.ts b/apps/desktop/src/vault/app/vault/vault-items.component.ts index 2d1ba784753..8bf4955343d 100644 --- a/apps/desktop/src/vault/app/vault/vault-items.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items.component.ts @@ -8,6 +8,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; @@ -22,8 +23,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent { searchBarService: SearchBarService, cipherService: CipherService, accountService: AccountService, + protected restrictedItemTypesService: RestrictedItemTypesService, ) { - super(searchService, cipherService, accountService); + super(searchService, cipherService, accountService, restrictedItemTypesService); // eslint-disable-next-line rxjs-angular/prefer-takeuntil searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => { diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts index ff6ec9af0af..49bf43d60bf 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.component.ts @@ -11,8 +11,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { RestrictedItemTypesService } from "@bitwarden/vault"; import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component"; import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service"; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index 62b53d71e84..e65d423a57b 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -35,8 +35,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { LayoutComponent } from "@bitwarden/components"; -import { RestrictedItemTypesService } from "@bitwarden/vault"; import { GroupView } from "../../../admin-console/organizations/core"; import { PreloadedEnglishI18nModule } from "../../../core/tests"; 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 8987fff04cf..72766817eeb 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 @@ -22,8 +22,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { DialogService, ToastService } from "@bitwarden/components"; -import { RestrictedItemTypesService } from "@bitwarden/vault"; import { TrialFlowService } from "../../../../billing/services/trial-flow.service"; import { VaultFilterService } from "../services/abstractions/vault-filter.service"; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts index 660aeb293a4..2ec2b2c40a9 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts @@ -3,7 +3,7 @@ import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { RestrictedCipherType } from "@bitwarden/vault"; +import { RestrictedCipherType } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { createFilterFunction } from "./filter-function"; import { All } from "./routed-vault-filter.model"; 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 61305fa5e49..93071aecae3 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 @@ -1,7 +1,10 @@ import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { RestrictedCipherType } from "@bitwarden/vault"; +import { + isCipherViewRestricted, + RestrictedCipherType, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model"; @@ -83,24 +86,9 @@ export function createFilterFunction( ) { return false; } - // Restricted types - if (restrictedTypes && restrictedTypes.length > 0) { - // Filter the cipher if that type is restricted unless - // - The cipher belongs to an organization and that organization allows viewing the cipher type - // OR - // - The cipher belongs to the user's personal vault and at least one other organization does not restrict that type - if ( - restrictedTypes.some( - (restrictedType) => - restrictedType.cipherType === cipher.type && - (cipher.organizationId - ? !restrictedType.allowViewOrgIds.includes(cipher.organizationId) - : restrictedType.allowViewOrgIds.length === 0), - ) - ) { - return false; - } + if (restrictedTypes && isCipherViewRestricted(cipher, restrictedTypes)) { + return false; } return true; }; 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 48bc3a4268b..49e159143dd 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 @@ -18,13 +18,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co 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 { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { BreadcrumbsModule, DialogService, MenuModule, SimpleDialogOptions, } from "@bitwarden/components"; -import { RestrictedItemTypesService } from "@bitwarden/vault"; import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog"; import { HeaderModule } from "../../../layouts/header/header.module"; 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 2c9079c7279..3d59a186705 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -66,6 +66,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components"; import { @@ -79,7 +80,6 @@ import { DecryptionFailureDialogComponent, DefaultCipherFormConfigService, PasswordRepromptService, - RestrictedItemTypesService, } from "@bitwarden/vault"; import { diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 873cb4f9b63..c1c4844a61d 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -294,6 +294,7 @@ import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service"; import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks"; @@ -680,6 +681,11 @@ const safeProviders: SafeProvider[] = [ KdfConfigService, ], }), + safeProvider({ + provide: RestrictedItemTypesService, + useClass: RestrictedItemTypesService, + deps: [ConfigService, AccountService, OrganizationServiceAbstraction, PolicyServiceAbstraction], + }), safeProvider({ provide: PasswordStrengthServiceAbstraction, useClass: PasswordStrengthService, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 5d6343b0b3c..ec79ac9ef18 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -84,7 +84,6 @@ export class AddEditComponent implements OnInit, OnDestroy { showCardNumber = false; showCardCode = false; cipherType = CipherType; - typeOptions: any[]; cardBrandOptions: any[]; cardExpMonthOptions: any[]; identityTitleOptions: any[]; @@ -139,13 +138,6 @@ export class AddEditComponent implements OnInit, OnDestroy { protected sdkService: SdkService, private sshImportPromptService: SshImportPromptService, ) { - this.typeOptions = [ - { name: i18nService.t("typeLogin"), value: CipherType.Login }, - { name: i18nService.t("typeCard"), value: CipherType.Card }, - { name: i18nService.t("typeIdentity"), value: CipherType.Identity }, - { name: i18nService.t("typeSecureNote"), value: CipherType.SecureNote }, - ]; - this.cardBrandOptions = [ { name: "-- " + i18nService.t("select") + " --", value: null }, { name: "Visa", value: "Visa" }, @@ -215,8 +207,6 @@ export class AddEditComponent implements OnInit, OnDestroy { this.writeableCollections = await this.loadCollections(); this.canUseReprompt = await this.passwordRepromptService.enabled(); - - this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey }); } ngOnDestroy() { diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index c34816994be..db9ac581d41 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -8,7 +8,9 @@ import { combineLatest, filter, from, + map, of, + shareReplay, switchMap, takeUntil, } from "rxjs"; @@ -20,6 +22,11 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + isCipherViewRestricted, + RestrictedItemTypesService, +} from "@bitwarden/common/vault/services/restricted-item-types.service"; +import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items"; @Directive() export class VaultItemsComponent implements OnInit, OnDestroy { @@ -35,6 +42,19 @@ export class VaultItemsComponent implements OnInit, OnDestroy { organization: Organization; CipherType = CipherType; + protected itemTypes$ = this.restrictedItemTypesService.restricted$.pipe( + map((restrictedItemTypes) => + // Filter out restricted item types + CIPHER_MENU_ITEMS.filter( + (itemType) => + !restrictedItemTypes.some( + (restrictedType) => restrictedType.cipherType === itemType.type, + ), + ), + ), + shareReplay({ bufferSize: 1, refCount: true }), + ); + protected searchPending = false; /** Construct filters as an observable so it can be appended to the cipher stream. */ @@ -62,6 +82,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected searchService: SearchService, protected cipherService: CipherService, protected accountService: AccountService, + protected restrictedItemTypesService: RestrictedItemTypesService, ) { this.subscribeToCiphers(); } @@ -143,18 +164,22 @@ export class VaultItemsComponent implements OnInit, OnDestroy { this._searchText$, this._filter$, of(userId), + this.restrictedItemTypesService.restricted$, ]), ), - switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => { + switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId, restricted]) => { let allCiphers = indexedCiphers ?? []; const _failedCiphers = failedCiphers ?? []; allCiphers = [..._failedCiphers, ...allCiphers]; + const restrictedTypeFilter = (cipher: CipherView) => + isCipherViewRestricted(cipher, restricted); + return this.searchService.searchCiphers( userId, searchText, - [filter, this.deletedFilter], + [filter, this.deletedFilter, restrictedTypeFilter], allCiphers, ); }), diff --git a/libs/common/src/vault/service-utils.ts b/libs/common/src/vault/service-utils.ts index 96ae406fae4..9595434223f 100644 --- a/libs/common/src/vault/service-utils.ts +++ b/libs/common/src/vault/service-utils.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore + import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node"; export class ServiceUtils { diff --git a/libs/vault/src/services/restricted-item-types.service.spec.ts b/libs/common/src/vault/services/restricted-item-types.service.spec.ts similarity index 89% rename from libs/vault/src/services/restricted-item-types.service.spec.ts rename to libs/common/src/vault/services/restricted-item-types.service.spec.ts index 7ff48f0642b..9b549665184 100644 --- a/libs/vault/src/services/restricted-item-types.service.spec.ts +++ b/libs/common/src/vault/services/restricted-item-types.service.spec.ts @@ -1,4 +1,3 @@ -import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; @@ -49,19 +48,16 @@ describe("RestrictedItemTypesService", () => { fakeAccount = { id: Utils.newGuid() as UserId } as Account; accountService.activeAccount$ = of(fakeAccount); - TestBed.configureTestingModule({ - providers: [ - { provide: PolicyService, useValue: policyService }, - { provide: OrganizationService, useValue: organizationService }, - { provide: AccountService, useValue: accountService }, - { provide: ConfigService, useValue: configService }, - ], - }); - configService.getFeatureFlag$.mockReturnValue(of(true)); organizationService.organizations$.mockReturnValue(of([org1, org2])); policyService.policiesByType$.mockReturnValue(of([])); - service = TestBed.inject(RestrictedItemTypesService); + + service = new RestrictedItemTypesService( + configService, + accountService, + organizationService, + policyService, + ); }); it("emits empty array when feature flag is disabled", async () => { @@ -106,7 +102,6 @@ describe("RestrictedItemTypesService", () => { }); it("returns empty allowViewOrgIds when all orgs restrict the same type", async () => { - configService.getFeatureFlag$.mockReturnValue(of(true)); organizationService.organizations$.mockReturnValue(of([org1, org2])); policyService.policiesByType$.mockReturnValue(of([policyOrg1, policyOrg2])); @@ -117,7 +112,6 @@ describe("RestrictedItemTypesService", () => { }); it("aggregates multiple types and computes allowViewOrgIds correctly", async () => { - configService.getFeatureFlag$.mockReturnValue(of(true)); organizationService.organizations$.mockReturnValue(of([org1, org2])); policyService.policiesByType$.mockReturnValue( of([ diff --git a/libs/vault/src/services/restricted-item-types.service.ts b/libs/common/src/vault/services/restricted-item-types.service.ts similarity index 79% rename from libs/vault/src/services/restricted-item-types.service.ts rename to libs/common/src/vault/services/restricted-item-types.service.ts index b24533fb2f6..63c9577bc09 100644 --- a/libs/vault/src/services/restricted-item-types.service.ts +++ b/libs/common/src/vault/services/restricted-item-types.service.ts @@ -1,4 +1,3 @@ -import { Injectable } from "@angular/core"; import { combineLatest, map, of, Observable } from "rxjs"; import { switchMap, distinctUntilChanged, shareReplay } from "rxjs/operators"; @@ -10,13 +9,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; export type RestrictedCipherType = { cipherType: CipherType; allowViewOrgIds: string[]; }; -@Injectable({ providedIn: "root" }) export class RestrictedItemTypesService { /** * Emits an array of RestrictedCipherType objects: @@ -78,3 +77,25 @@ export class RestrictedItemTypesService { private policyService: PolicyService, ) {} } + +/** + * Filter that returns whether a cipher is restricted from being viewed by the user + * Criteria: + * - the cipher's type is restricted by at least one org + * UNLESS + * - the cipher belongs to an organization and that organization does not restrict that type + * OR + * - the cipher belongs to the user's personal vault and at least one other organization does not restrict that type + */ +export function isCipherViewRestricted( + cipher: CipherView, + restrictedTypes: RestrictedCipherType[], +) { + return restrictedTypes.some( + (restrictedType) => + restrictedType.cipherType === cipher.type && + (cipher.organizationId + ? !restrictedType.allowViewOrgIds.includes(cipher.organizationId) + : restrictedType.allowViewOrgIds.length === 0), + ); +} diff --git a/libs/common/src/vault/types/cipher-menu-items.ts b/libs/common/src/vault/types/cipher-menu-items.ts index e88c0457081..7108d0d0bd6 100644 --- a/libs/common/src/vault/types/cipher-menu-items.ts +++ b/libs/common/src/vault/types/cipher-menu-items.ts @@ -19,6 +19,6 @@ export const CIPHER_MENU_ITEMS = Object.freeze([ { type: CipherType.Login, icon: "bwi-globe", labelKey: "typeLogin" }, { type: CipherType.Card, icon: "bwi-credit-card", labelKey: "typeCard" }, { type: CipherType.Identity, icon: "bwi-id-card", labelKey: "typeIdentity" }, - { type: CipherType.SecureNote, icon: "bwi-sticky-note", labelKey: "note" }, + { type: CipherType.SecureNote, icon: "bwi-sticky-note", labelKey: "typeNote" }, { type: CipherType.SshKey, icon: "bwi-key", labelKey: "typeSshKey" }, ] as const) satisfies readonly CipherMenuItem[]; diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 7229b558f30..b39bb85ab30 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -24,10 +24,6 @@ export * as VaultIcons from "./icons"; export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service"; export { SshImportPromptService } from "./services/ssh-import-prompt.service"; -export { - RestrictedItemTypesService, - RestrictedCipherType, -} from "./services/restricted-item-types.service"; export * from "./abstractions/change-login-password.service"; export * from "./services/default-change-login-password.service"; From aa4a9babc504dea12f35f08537f7daf9e724da1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Wed, 18 Jun 2025 21:56:56 +0200 Subject: [PATCH 24/31] fix(desktop_proxy): [PM-22452] Fix desktop_proxy signing for DMG --- apps/desktop/scripts/after-pack.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/scripts/after-pack.js b/apps/desktop/scripts/after-pack.js index 99c3d91be52..cdb5e098440 100644 --- a/apps/desktop/scripts/after-pack.js +++ b/apps/desktop/scripts/after-pack.js @@ -89,7 +89,7 @@ async function run(context) { } else { // For non-Appstore builds, we don't need the inherit binary as they are not sandboxed, // but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly. - const entitlementsName = "entitlements.mac.plist"; + const entitlementsName = "entitlements.mac.inherit.plist"; const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName); child_process.execSync( `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`, From e8f53fe9b716b4ac0ae6778245e704ddfd094e20 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 18 Jun 2025 14:44:21 -0700 Subject: [PATCH 25/31] [PM-22756] Send minimizeOnCopy message during copy on Desktop platform (#15232) * [PM-22756] Send minimizeOnCopy message during copy on Desktop platform * [PM-22756] Introduce optional CopyClickListener pattern * [PM-22756] Introduce CopyService that wraps PlatformUtilsService.copyToClipboard to allow scoped implementations * [PM-22756] Introduce DesktopVaultCopyService that sends the minimizeOnCopy message * [PM-22756] Remove leftover onCopy method * [PM-22756] Fix failing tests * [PM-22756] Revert CopyService solution * [PM-22756] Cleanup * [PM-22756] Update test * [PM-22756] Cleanup leftover test changes --- .../src/vault/app/vault/vault-v2.component.ts | 19 ++++++++++++----- .../copy-click/copy-click.directive.spec.ts | 17 +++++++++++---- .../src/copy-click/copy-click.directive.ts | 21 ++++++++++++++----- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index a84a868f4ca..354752c8b36 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -13,8 +13,6 @@ import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observabl import { filter, map, take } from "rxjs/operators"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -45,6 +43,8 @@ import { DialogService, ItemModule, ToastService, + CopyClickListener, + COPY_CLICK_LISTENER, } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { @@ -115,9 +115,13 @@ const BroadcasterSubscriptionId = "VaultComponent"; useClass: DesktopPremiumUpgradePromptService, }, { provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService }, + { + provide: COPY_CLICK_LISTENER, + useExisting: VaultV2Component, + }, ], }) -export class VaultV2Component implements OnInit, OnDestroy { +export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener { @ViewChild(VaultItemsV2Component, { static: true }) vaultItemsComponent: VaultItemsV2Component | null = null; @ViewChild(VaultFilterComponent, { static: true }) @@ -161,7 +165,6 @@ export class VaultV2Component implements OnInit, OnDestroy { ), ); - private modal: ModalRef | null = null; private componentIsDestroyed$ = new Subject(); private allOrganizations: Organization[] = []; private allCollections: CollectionView[] = []; @@ -170,7 +173,6 @@ export class VaultV2Component implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private i18nService: I18nService, - private modalService: ModalService, private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, @@ -378,6 +380,13 @@ export class VaultV2Component implements OnInit, OnDestroy { } } + /** + * Handler for Vault level CopyClickDirectives to send the minimizeOnCopy message + */ + onCopy() { + this.messagingService.send("minimizeOnCopy"); + } + async viewCipher(cipher: CipherView) { if (await this.shouldReprompt(cipher, "view")) { return; diff --git a/libs/components/src/copy-click/copy-click.directive.spec.ts b/libs/components/src/copy-click/copy-click.directive.spec.ts index 38f8ccb43cb..321a18596e4 100644 --- a/libs/components/src/copy-click/copy-click.directive.spec.ts +++ b/libs/components/src/copy-click/copy-click.directive.spec.ts @@ -1,10 +1,11 @@ import { Component, ElementRef, ViewChild } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "../"; +import { ToastService, CopyClickListener, COPY_CLICK_LISTENER } from "../"; import { CopyClickDirective } from "./copy-click.directive"; @@ -34,10 +35,12 @@ describe("CopyClickDirective", () => { let fixture: ComponentFixture; const copyToClipboard = jest.fn(); const showToast = jest.fn(); + const copyClickListener = mock(); beforeEach(async () => { copyToClipboard.mockClear(); showToast.mockClear(); + copyClickListener.onCopy.mockClear(); await TestBed.configureTestingModule({ imports: [TestCopyClickComponent], @@ -55,6 +58,7 @@ describe("CopyClickDirective", () => { }, { provide: PlatformUtilsService, useValue: { copyToClipboard } }, { provide: ToastService, useValue: { showToast } }, + { provide: COPY_CLICK_LISTENER, useValue: copyClickListener }, ], }).compileComponents(); @@ -92,7 +96,6 @@ describe("CopyClickDirective", () => { successToastButton.click(); expect(showToast).toHaveBeenCalledWith({ message: "copySuccessful", - title: null, variant: "success", }); }); @@ -103,7 +106,6 @@ describe("CopyClickDirective", () => { infoToastButton.click(); expect(showToast).toHaveBeenCalledWith({ message: "copySuccessful", - title: null, variant: "info", }); }); @@ -115,8 +117,15 @@ describe("CopyClickDirective", () => { expect(showToast).toHaveBeenCalledWith({ message: "valueCopied Content", - title: null, variant: "success", }); }); + + it("should call copyClickListener.onCopy when value is copied", () => { + const successToastButton = fixture.componentInstance.successToastButton.nativeElement; + + successToastButton.click(); + + expect(copyClickListener.onCopy).toHaveBeenCalledWith("success toast shown"); + }); }); diff --git a/libs/components/src/copy-click/copy-click.directive.ts b/libs/components/src/copy-click/copy-click.directive.ts index 1dfaf4387dc..514a55a0242 100644 --- a/libs/components/src/copy-click/copy-click.directive.ts +++ b/libs/components/src/copy-click/copy-click.directive.ts @@ -1,12 +1,19 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Directive, HostListener, Input } from "@angular/core"; +import { Directive, HostListener, Input, InjectionToken, Inject, Optional } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService, ToastVariant } from "../"; +/** + * Listener that can be provided to receive copy events to allow for customized behavior. + */ +export interface CopyClickListener { + onCopy(value: string): void; +} + +export const COPY_CLICK_LISTENER = new InjectionToken("CopyClickListener"); + @Directive({ selector: "[appCopyClick]", }) @@ -18,6 +25,7 @@ export class CopyClickDirective { private platformUtilsService: PlatformUtilsService, private toastService: ToastService, private i18nService: I18nService, + @Optional() @Inject(COPY_CLICK_LISTENER) private copyListener?: CopyClickListener, ) {} @Input("appCopyClick") valueToCopy = ""; @@ -26,7 +34,7 @@ export class CopyClickDirective { * When set, the toast displayed will show ` copied` * instead of the default messaging. */ - @Input() valueLabel: string; + @Input() valueLabel?: string; /** * When set without a value, a success toast will be shown when the value is copied @@ -54,6 +62,10 @@ export class CopyClickDirective { @HostListener("click") onClick() { this.platformUtilsService.copyToClipboard(this.valueToCopy); + if (this.copyListener) { + this.copyListener.onCopy(this.valueToCopy); + } + if (this._showToast) { const message = this.valueLabel ? this.i18nService.t("valueCopied", this.valueLabel) @@ -61,7 +73,6 @@ export class CopyClickDirective { this.toastService.showToast({ variant: this.toastVariant, - title: null, message, }); } From b35583a5ac6fe68024cc681ebb63cdb214985da1 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:27:14 -0700 Subject: [PATCH 26/31] prevent double MP prompt on copy and delete (#15218) --- .../src/vault/app/vault/item-footer.component.ts | 5 +++-- .../src/vault/app/vault/vault-v2.component.html | 1 + .../src/vault/app/vault/vault-v2.component.ts | 13 ++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.ts b/apps/desktop/src/vault/app/vault/item-footer.component.ts index d83a530cc40..675a8403b20 100644 --- a/apps/desktop/src/vault/app/vault/item-footer.component.ts +++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts @@ -25,6 +25,7 @@ export class ItemFooterComponent implements OnInit { @Input() collectionId: string | null = null; @Input({ required: true }) action: string = "view"; @Input() isSubmitting: boolean = false; + @Input() masterPasswordAlreadyPrompted: boolean = false; @Output() onEdit = new EventEmitter(); @Output() onClone = new EventEmitter(); @Output() onDelete = new EventEmitter(); @@ -34,8 +35,7 @@ export class ItemFooterComponent implements OnInit { canDeleteCipher$: Observable = new Observable(); activeUserId: UserId | null = null; - - private passwordReprompted = false; + passwordReprompted: boolean = false; constructor( protected cipherService: CipherService, @@ -51,6 +51,7 @@ export class ItemFooterComponent implements OnInit { async ngOnInit() { this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher); this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.passwordReprompted = this.masterPasswordAlreadyPrompted; } async clone() { diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.html b/apps/desktop/src/vault/app/vault/vault-v2.component.html index 4dd23466126..f1cb28f3ea5 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.html +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.html @@ -20,6 +20,7 @@ (onDelete)="deleteCipher()" (onCancel)="cancelCipher($event)" [isSubmitting]="isSubmitting" + [masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId" >
diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 354752c8b36..849899bfe66 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -744,10 +744,17 @@ export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener { } async editFolder(folderId: string) { + if (!this.activeUserId) { + return; + } const folderView = await firstValueFrom( this.folderService.getDecrypted$(folderId, this.activeUserId), ); + if (!folderView) { + return; + } + const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, { editFolderConfig: { folder: { @@ -762,7 +769,7 @@ export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener { result === AddEditFolderDialogResult.Deleted || result === AddEditFolderDialogResult.Created ) { - await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter); + await this.vaultFilterComponent?.reloadCollectionsAndFolders(this.activeFilter); } } @@ -807,10 +814,6 @@ export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener { .catch(() => {}); } - private addCipherWithChangeDetection(type: CipherType) { - this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {})); - } - private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) { this.functionWithChangeDetection(() => { (async () => { From f9b31d2906dee4a11ee858479f22b359819e1df7 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 18 Jun 2025 15:27:34 -0700 Subject: [PATCH 27/31] remove legacy attachment upload (#15237) --- libs/common/src/abstractions/api.service.ts | 10 ---- libs/common/src/services/api.service.ts | 18 ------ .../file-upload/cipher-file-upload.service.ts | 60 +------------------ 3 files changed, 1 insertion(+), 87 deletions(-) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 44b5e34a4a4..cabde4093c4 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -228,16 +228,6 @@ export abstract class ApiService { request: CipherBulkRestoreRequest, ) => Promise>; - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - postCipherAttachmentLegacy: (id: string, data: FormData) => Promise; - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - postCipherAttachmentAdminLegacy: (id: string, data: FormData) => Promise; postCipherAttachment: ( id: string, request: AttachmentRequest, diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 1971cd86363..a2cc86a57ad 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -639,24 +639,6 @@ export class ApiService implements ApiServiceAbstraction { return new AttachmentUploadDataResponse(r); } - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postCipherAttachmentLegacy(id: string, data: FormData): Promise { - const r = await this.send("POST", "/ciphers/" + id + "/attachment", data, true, true); - return new CipherResponse(r); - } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postCipherAttachmentAdminLegacy(id: string, data: FormData): Promise { - const r = await this.send("POST", "/ciphers/" + id + "/attachment-admin", data, true, true); - return new CipherResponse(r); - } - deleteCipherAttachment(id: string, attachmentId: string): Promise { return this.send("DELETE", "/ciphers/" + id + "/attachment/" + attachmentId, null, true, true); } diff --git a/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts b/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts index 4dd2f7f7338..10fa1d9580c 100644 --- a/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts +++ b/libs/common/src/vault/services/file-upload/cipher-file-upload.service.ts @@ -6,7 +6,6 @@ import { FileUploadApiMethods, FileUploadService, } from "../../../platform/abstractions/file-upload/file-upload.service"; -import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -47,18 +46,7 @@ export class CipherFileUploadService implements CipherFileUploadServiceAbstracti this.generateMethods(uploadDataResponse, response, request.adminRequest), ); } catch (e) { - if ( - (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) || - (e as ErrorResponse).statusCode === 405 - ) { - response = await this.legacyServerAttachmentFileUpload( - request.adminRequest, - cipher.id, - encFileName, - encData, - dataEncKey[1], - ); - } else if (e instanceof ErrorResponse) { + if (e instanceof ErrorResponse) { throw new Error((e as ErrorResponse).getSingleMessage()); } else { throw e; @@ -113,50 +101,4 @@ export class CipherFileUploadService implements CipherFileUploadServiceAbstracti } }; } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async legacyServerAttachmentFileUpload( - admin: boolean, - cipherId: string, - encFileName: EncString, - encData: EncArrayBuffer, - key: EncString, - ) { - const fd = new FormData(); - try { - const blob = new Blob([encData.buffer], { type: "application/octet-stream" }); - fd.append("key", key.encryptedString); - fd.append("data", blob, encFileName.encryptedString); - } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append("key", key.encryptedString); - fd.append( - "data", - Buffer.from(encData.buffer) as any, - { - filepath: encFileName.encryptedString, - contentType: "application/octet-stream", - } as any, - ); - } else { - throw e; - } - } - - let response: CipherResponse; - try { - if (admin) { - response = await this.apiService.postCipherAttachmentAdminLegacy(cipherId, fd); - } else { - response = await this.apiService.postCipherAttachmentLegacy(cipherId, fd); - } - } catch (e) { - throw new Error((e as ErrorResponse).getSingleMessage()); - } - - return response; - } } From 3b830faf09e55c5fec2a043c44432487029cbbfc Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:08:13 -0700 Subject: [PATCH 28/31] fix logic for restrictedTypeFilter (#15253) --- libs/angular/src/vault/components/vault-items.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index db9ac581d41..0679d141bbd 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -174,7 +174,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy { allCiphers = [..._failedCiphers, ...allCiphers]; const restrictedTypeFilter = (cipher: CipherView) => - isCipherViewRestricted(cipher, restricted); + !isCipherViewRestricted(cipher, restricted); return this.searchService.searchCiphers( userId, From 1b3877a3d2fe538f84d72cffa6c62fd2676fe6ee Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 18 Jun 2025 16:53:13 -0700 Subject: [PATCH 29/31] [PM-22764] Fix Desktop footer button permissions (#15254) * [PM-22764] Fix desktop footer button permissions * [PM-22764] Fix desktop edit button permission --- apps/desktop/src/vault/app/vault/item-footer.component.html | 4 ++-- apps/desktop/src/vault/app/vault/item-footer.component.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html index 5a067da372e..c41bf254c80 100644 --- a/apps/desktop/src/vault/app/vault/item-footer.component.html +++ b/apps/desktop/src/vault/app/vault/item-footer.component.html @@ -36,7 +36,7 @@ class="primary" (click)="restore()" appA11yTitle="{{ 'restore' | i18n }}" - *ngIf="cipher.isDeleted" + *ngIf="cipher.isDeleted && cipher.permissions.restore" > @@ -50,7 +50,7 @@ -
+