From 598348fcc1a3f0ffabd0e8cc2b901972bb52194d Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Tue, 5 Aug 2025 17:31:41 -0400 Subject: [PATCH 01/12] eslint/ts disable removals (#15418) --- .../src/autofill/background/tabs.background.ts | 6 ++---- .../browser/src/autofill/content/autofill-init.ts | 6 ++---- apps/browser/src/autofill/content/autofiller.ts | 8 ++------ .../content/bootstrap-autofill-overlay-menu.ts | 4 +--- .../bootstrap-autofill-overlay-notifications.ts | 6 ++---- .../content/bootstrap-autofill-overlay.ts | 6 ++---- .../content/trigger-autofill-script-injection.ts | 4 +--- .../src/autofill/services/autofill.service.ts | 15 ++++++--------- apps/browser/src/background/idle.background.ts | 9 +++------ apps/browser/src/background/runtime.background.ts | 4 ++-- 10 files changed, 23 insertions(+), 45 deletions(-) diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index 4d520680980..cd2c1595d69 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -20,10 +20,8 @@ export default class TabsBackground { return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.updateCurrentTabData(); - this.setupTabEventListeners(); + void this.updateCurrentTabData(); + void this.setupTabEventListeners(); } /** diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index 8f69937ac60..b6fc6c3392e 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { EVENTS } from "@bitwarden/common/autofill/constants"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -122,7 +120,7 @@ class AutofillInit implements AutofillInitInterface { * @param {AutofillExtensionMessage} message */ private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) { - if ((document.defaultView || window).location.href !== pageDetailsUrl) { + if ((document.defaultView || window).location.href !== pageDetailsUrl || !fillScript) { return; } @@ -177,7 +175,7 @@ class AutofillInit implements AutofillInitInterface { message: AutofillExtensionMessage, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void, - ): boolean => { + ): boolean | null => { const command: string = message.command; const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command); if (!handler) { diff --git a/apps/browser/src/autofill/content/autofiller.ts b/apps/browser/src/autofill/content/autofiller.ts index c7a742f1fe1..bc9fd0bb05f 100644 --- a/apps/browser/src/autofill/content/autofiller.ts +++ b/apps/browser/src/autofill/content/autofiller.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { setupExtensionDisconnectAction } from "../utils"; if (document.readyState === "loading") { @@ -9,7 +7,7 @@ if (document.readyState === "loading") { } function loadAutofiller() { - let pageHref: string = null; + let pageHref: null | string = null; let filledThisHref = false; let delayFillTimeout: number; let doFillInterval: number | NodeJS.Timeout; @@ -51,9 +49,7 @@ function loadAutofiller() { sender: "autofiller", }; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - chrome.runtime.sendMessage(msg); + void chrome.runtime.sendMessage(msg); } } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts index 605ffff0fec..40e32843fd4 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; @@ -11,7 +9,7 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - let inlineMenuContentService: AutofillInlineMenuContentService; + let inlineMenuContentService: undefined | AutofillInlineMenuContentService; if (globalThis.self === globalThis.top) { inlineMenuContentService = new AutofillInlineMenuContentService(); } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts index 495ae0e22db..8a079fa26c8 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import DomElementVisibilityService from "../services/dom-element-visibility.service"; @@ -20,7 +18,7 @@ import AutofillInit from "./autofill-init"; inlineMenuFieldQualificationService, ); - let overlayNotificationsContentService: OverlayNotificationsContentService; + let overlayNotificationsContentService: undefined | OverlayNotificationsContentService; if (globalThis.self === globalThis.top) { overlayNotificationsContentService = new OverlayNotificationsContentService(); } @@ -29,7 +27,7 @@ import AutofillInit from "./autofill-init"; domQueryService, domElementVisibilityService, autofillOverlayContentService, - null, + undefined, overlayNotificationsContentService, ); setupAutofillInitDisconnectAction(windowContext); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index 1777b135fe9..d204362ee25 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; @@ -12,8 +10,8 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - let inlineMenuContentService: AutofillInlineMenuContentService; - let overlayNotificationsContentService: OverlayNotificationsContentService; + let inlineMenuContentService: undefined | AutofillInlineMenuContentService; + let overlayNotificationsContentService: undefined | OverlayNotificationsContentService; if (globalThis.self === globalThis.top) { inlineMenuContentService = new AutofillInlineMenuContentService(); overlayNotificationsContentService = new OverlayNotificationsContentService(); diff --git a/apps/browser/src/autofill/content/trigger-autofill-script-injection.ts b/apps/browser/src/autofill/content/trigger-autofill-script-injection.ts index 95a2391991b..9db691c1359 100644 --- a/apps/browser/src/autofill/content/trigger-autofill-script-injection.ts +++ b/apps/browser/src/autofill/content/trigger-autofill-script-injection.ts @@ -1,5 +1,3 @@ (function () { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" }); + void chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" }); })(); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 6aa99bbda41..099f345cb75 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -213,9 +213,7 @@ export default class AutofillService implements AutofillServiceInterface { this.autofillScriptPortsSet.delete(port); }); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.injectAutofillScriptsInAllTabs(); + void this.injectAutofillScriptsInAllTabs(); } /** @@ -470,9 +468,7 @@ export default class AutofillService implements AutofillServiceInterface { await this.cipherService.updateLastUsedDate(options.cipher.id, activeAccount.id); } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessage( + void BrowserApi.tabSendMessage( tab, { command: options.autoSubmitLogin ? "triggerAutoSubmitLogin" : "fillForm", @@ -502,9 +498,10 @@ export default class AutofillService implements AutofillServiceInterface { ); if (didAutofill) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientAutofilled, options.cipher.id); + await this.eventCollectionService.collect( + EventType.Cipher_ClientAutofilled, + options.cipher.id, + ); if (totp !== null) { return totp; } else { diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index 90276eaea0a..5b3a7f7d163 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -15,7 +13,7 @@ const IdleInterval = 60 * 5; // 5 minutes export default class IdleBackground { private idle: typeof chrome.idle | typeof browser.idle | null; - private idleTimer: number | NodeJS.Timeout = null; + private idleTimer: null | number | NodeJS.Timeout = null; private idleState = "active"; constructor( @@ -80,9 +78,8 @@ export default class IdleBackground { globalThis.clearTimeout(this.idleTimer); this.idleTimer = null; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.idle.queryState(IdleInterval, (state: string) => { + + void this.idle?.queryState(IdleInterval, (state: string) => { if (state !== this.idleState) { this.idleState = state; handler(state); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 1e7a0140022..725ca1b2780 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -48,7 +48,7 @@ export default class RuntimeBackground { private platformUtilsService: BrowserPlatformUtilsService, private notificationsService: NotificationsService, private autofillSettingsService: AutofillSettingsServiceAbstraction, - private processReloadSerivce: ProcessReloadServiceAbstraction, + private processReloadService: ProcessReloadServiceAbstraction, private environmentService: BrowserEnvironmentService, private messagingService: MessagingService, private logService: LogService, @@ -241,7 +241,7 @@ export default class RuntimeBackground { await closeUnlockPopout(); } - this.processReloadSerivce.cancelProcessReload(); + this.processReloadService.cancelProcessReload(); if (item) { await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); From 61cd0c4f51d9fe390c93f61f2f55076de33b57cf Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:27:52 +1000 Subject: [PATCH 02/12] [PM-23920] Admin Console - adopt strongly typed guids (#15814) Update organization, collection and policy to use strongly typed IDs --- .../open-attachments.component.spec.ts | 6 +++--- .../request/organization-collection.request.ts | 3 ++- .../organizations/collections/vault.component.ts | 12 +++++++++++- .../collection-dialog.component.ts | 7 ++++--- .../models/requests/request-sm-access.request.ts | 4 ++-- .../request-sm-access.component.ts | 3 +-- .../components/vault-items/vault-items.stories.ts | 7 ++++--- .../services/routed-vault-filter.service.ts | 8 ++++++-- .../vault-filter/services/vault-filter.service.ts | 4 ++-- .../models/routed-vault-filter-bridge.model.ts | 5 +++-- .../shared/models/routed-vault-filter.model.ts | 11 +++++++++-- .../collections/models/collection-admin.view.ts | 3 +++ .../common/collections/models/collection.spec.ts | 2 +- .../src/common/collections/models/collection.ts | 5 +++-- .../common/collections/models/collection.view.ts | 5 +++-- .../empty-vault-nudge.service.ts | 4 ++-- .../vault-settings-import-nudge.service.ts | 4 ++-- .../admin-console/models/domain/organization.ts | 5 +++-- .../src/admin-console/models/domain/policy.ts | 6 +++--- .../models/export/collection-with-id.export.ts | 4 +++- .../common/src/models/export/collection.export.ts | 5 +++-- libs/common/src/types/guid.ts | 5 +++++ libs/importer/src/importers/base-importer.ts | 5 +++-- .../importers/keeper/keeper-csv-importer.spec.ts | 5 +++-- .../importers/keeper/keeper-json-importer.spec.ts | 3 ++- .../netwrix-passwordsecure-csv-importer.spec.ts | 5 +++-- .../src/importers/nordpass-csv-importer.spec.ts | 3 ++- .../onepassword/onepassword-1pux-importer.spec.ts | 3 ++- .../passsordxp/passwordxp-csv-importer.spec.ts | 5 +++-- .../password-depot-17-xml-importer.spec.ts | 3 ++- .../protonpass/protonpass-json-importer.spec.ts | 3 ++- .../importers/psono/psono-json-importer.spec.ts | 3 ++- .../src/importers/zohovault-csv-importer.spec.ts | 3 ++- libs/importer/src/services/import.service.spec.ts | 15 ++++++++------- libs/importer/src/services/import.service.ts | 10 +++++++--- .../item-details-section.component.spec.ts | 5 +++-- 36 files changed, 122 insertions(+), 67 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 19779d73a11..c669ba167df 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -10,7 +10,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; 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"; @@ -163,7 +163,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Free; - org.id = cipherView.organizationId; + org.id = cipherView.organizationId as OrganizationId; await component.ngOnInit(); @@ -173,7 +173,7 @@ describe("OpenAttachmentsComponent", () => { it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => { cipherView.organizationId = "888-333-333"; org.productTierType = ProductTierType.Families; - org.id = cipherView.organizationId; + org.id = cipherView.organizationId as OrganizationId; await component.ngOnInit(); diff --git a/apps/cli/src/admin-console/models/request/organization-collection.request.ts b/apps/cli/src/admin-console/models/request/organization-collection.request.ts index b5f796afe2d..14714e4758b 100644 --- a/apps/cli/src/admin-console/models/request/organization-collection.request.ts +++ b/apps/cli/src/admin-console/models/request/organization-collection.request.ts @@ -1,13 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { SelectionReadOnly } from "../selection-read-only"; export class OrganizationCollectionRequest extends CollectionExport { static template(): OrganizationCollectionRequest { const req = new OrganizationCollectionRequest(); - req.organizationId = "00000000-0000-0000-0000-000000000000"; + req.organizationId = "00000000-0000-0000-0000-000000000000" as OrganizationId; req.name = "Collection name"; req.externalId = null; req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()]; diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index 5dc217b7a50..706b2022109 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -278,9 +278,16 @@ export class VaultComponent implements OnInit, OnDestroy { ); const filter$ = this.routedVaultFilterService.filter$; + + // FIXME: The RoutedVaultFilterModel uses `organizationId: Unassigned` to represent the individual vault, + // but that is never used in Admin Console. This function narrows the type so it doesn't pollute our code here, + // but really we should change to using our own vault filter model that only represents valid states in AC. + const isOrganizationId = (value: OrganizationId | Unassigned): value is OrganizationId => + value !== Unassigned; const organizationId$ = filter$.pipe( map((filter) => filter.organizationId), filter((filter) => filter !== undefined), + filter(isOrganizationId), distinctUntilChanged(), ); @@ -373,9 +380,12 @@ export class VaultComponent implements OnInit, OnDestroy { this.allCollectionsWithoutUnassigned$, ]).pipe( map(([organizationId, allCollections]) => { + // FIXME: We should not assert that the Unassigned type is a CollectionId. + // Instead we should consider representing the Unassigned collection as a different object, given that + // it is not actually a collection. const noneCollection = new CollectionAdminView(); noneCollection.name = this.i18nService.t("unassigned"); - noneCollection.id = Unassigned; + noneCollection.id = Unassigned as CollectionId; noneCollection.organizationId = organizationId; return allCollections.concat(noneCollection); }), 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 fabfb65fc6b..a0964a90fca 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 @@ -39,6 +39,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { getById } from "@bitwarden/common/platform/misc"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { DIALOG_DATA, DialogConfig, @@ -87,8 +88,8 @@ enum ButtonType { } export interface CollectionDialogParams { - collectionId?: string; - organizationId: string; + collectionId?: CollectionId; + organizationId: OrganizationId; initialTab?: CollectionDialogTabType; parentCollectionId?: string; showOrgSelector?: boolean; @@ -136,7 +137,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { externalId: { value: "", disabled: true }, parent: undefined as string | undefined, access: [[] as AccessItemValue[]], - selectedOrg: "", + selectedOrg: "" as OrganizationId, }); protected PermissionMode = PermissionMode; protected showDeleteButton = false; diff --git a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts index 5edd8bc046e..7292e13a6a5 100644 --- a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts +++ b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Guid } from "@bitwarden/common/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; export class RequestSMAccessRequest { - OrganizationId: Guid; + OrganizationId: OrganizationId; EmailContent: string; } diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts index 443b3e03e5f..0e32321a0b3 100644 --- a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts @@ -10,7 +10,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Guid } from "@bitwarden/common/types/guid"; import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components"; import { HeaderModule } from "../../layouts/header/header.module"; @@ -63,7 +62,7 @@ export class RequestSMAccessComponent implements OnInit { const formValue = this.requestAccessForm.value; const request = new RequestSMAccessRequest(); - request.OrganizationId = formValue.selectedOrganization.id as Guid; + request.OrganizationId = formValue.selectedOrganization.id; request.EmailContent = formValue.requestAccessEmailContents; await this.smLandingApiService.requestSMAccessFromAdmins(request); 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 78c4d21dede..c114cb6d7c2 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 @@ -29,6 +29,7 @@ import { } from "@bitwarden/common/platform/abstractions/environment.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -262,7 +263,7 @@ export const OrganizationTrash: Story = { }; const unassignedCollection = new CollectionAdminView(); -unassignedCollection.id = Unassigned; +unassignedCollection.id = Unassigned as CollectionId; unassignedCollection.name = "Unassigned"; export const OrganizationTopLevelCollection: Story = { args: { @@ -327,7 +328,7 @@ function createCollectionView(i: number): CollectionAdminView { const organization = organizations[i % (organizations.length + 1)]; const group = groups[i % (groups.length + 1)]; const view = new CollectionAdminView(); - view.id = `collection-${i}`; + view.id = `collection-${i}` as CollectionId; view.name = `Collection ${i}`; view.organizationId = organization?.id; view.manage = true; @@ -357,7 +358,7 @@ function createGroupView(i: number): GroupView { function createOrganization(i: number): Organization { const organization = new Organization(); - organization.id = `organization-${i}`; + organization.id = `organization-${i}` as OrganizationId; organization.name = `Organization ${i}`; organization.type = OrganizationUserType.Owner; organization.permissions = new PermissionsApi(); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service.ts index a42b5228272..a5a99428b2d 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service.ts @@ -2,6 +2,8 @@ import { Injectable, OnDestroy } from "@angular/core"; import { ActivatedRoute, NavigationExtras } from "@angular/router"; import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; + import { isRoutedVaultFilterItemType, RoutedVaultFilterModel, @@ -31,10 +33,12 @@ export class RoutedVaultFilterService implements OnDestroy { const type = isRoutedVaultFilterItemType(unsafeType) ? unsafeType : undefined; return { - collectionId: queryParams.get("collectionId") ?? undefined, + collectionId: (queryParams.get("collectionId") as CollectionId) ?? undefined, folderId: queryParams.get("folderId") ?? undefined, organizationId: - params.get("organizationId") ?? queryParams.get("organizationId") ?? undefined, + (params.get("organizationId") as OrganizationId) ?? + (queryParams.get("organizationId") as OrganizationId) ?? + undefined, organizationIdParamType: params.get("organizationId") != undefined ? ("path" as const) : ("query" as const), type, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index 266676e418b..11e074db985 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -28,7 +28,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -209,7 +209,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { protected getOrganizationFilterMyVault(): TreeNode { const myVault = new Organization() as OrganizationFilter; - myVault.id = "MyVault"; + myVault.id = "MyVault" as OrganizationId; myVault.icon = "bwi-user"; myVault.enabled = true; myVault.hideOptions = true; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts index fe236a089e0..02d536eb6ab 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts @@ -1,4 +1,5 @@ import { Unassigned } from "@bitwarden/admin-console/common"; +import { CollectionId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -65,7 +66,7 @@ export class RoutedVaultFilterBridge implements VaultFilter { let type: RoutedVaultFilterItemType | undefined; if (value?.node.id === "AllItems" && this.routedFilter.organizationIdParamType === "path") { - type = "all"; + type = All; } else if ( value?.node.id === "AllItems" && this.routedFilter.organizationIdParamType === "query" @@ -98,7 +99,7 @@ export class RoutedVaultFilterBridge implements VaultFilter { return this.legacyFilter.selectedCollectionNode; } set selectedCollectionNode(value: TreeNode) { - let collectionId: string | undefined; + let collectionId: CollectionId | All | Unassigned | undefined; if (value != null && value.node.id === null) { collectionId = Unassigned; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts index 866ba1d9848..280ffd15732 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts @@ -1,4 +1,11 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; + +/** + * A constant used to represent viewing "all" of a particular filter. + */ export const All = "all"; +export type All = typeof All; // TODO: Remove `All` when moving to vertical navigation. const itemTypes = [ @@ -19,9 +26,9 @@ export function isRoutedVaultFilterItemType(value: unknown): value is RoutedVaul } export interface RoutedVaultFilterModel { - collectionId?: string; + collectionId?: CollectionId | All | Unassigned; folderId?: string; - organizationId?: string; + organizationId?: OrganizationId | Unassigned; type?: RoutedVaultFilterItemType; organizationIdParamType?: "path" | "query"; diff --git a/libs/admin-console/src/common/collections/models/collection-admin.view.ts b/libs/admin-console/src/common/collections/models/collection-admin.view.ts index dd7a57013ca..dcc88551551 100644 --- a/libs/admin-console/src/common/collections/models/collection-admin.view.ts +++ b/libs/admin-console/src/common/collections/models/collection-admin.view.ts @@ -4,7 +4,10 @@ import { CollectionAccessSelectionView } from "./collection-access-selection.vie import { CollectionAccessDetailsResponse } from "./collection.response"; import { CollectionView } from "./collection.view"; +// TODO: this is used to represent the pseudo "Unassigned" collection as well as +// the user's personal vault (as a pseudo organization). This should be separated out into different values. export const Unassigned = "unassigned"; +export type Unassigned = typeof Unassigned; export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; diff --git a/libs/admin-console/src/common/collections/models/collection.spec.ts b/libs/admin-console/src/common/collections/models/collection.spec.ts index 925490d22b9..fb38f1507f9 100644 --- a/libs/admin-console/src/common/collections/models/collection.spec.ts +++ b/libs/admin-console/src/common/collections/models/collection.spec.ts @@ -54,7 +54,7 @@ describe("Collection", () => { it("Decrypt", async () => { const collection = new Collection(); - collection.id = "id"; + collection.id = "id" as CollectionId; collection.organizationId = "orgId" as OrganizationId; collection.name = mockEnc("encName"); collection.externalId = "extId"; diff --git a/libs/admin-console/src/common/collections/models/collection.ts b/libs/admin-console/src/common/collections/models/collection.ts index 7bbd018fa96..d1709d1751d 100644 --- a/libs/admin-console/src/common/collections/models/collection.ts +++ b/libs/admin-console/src/common/collections/models/collection.ts @@ -1,5 +1,6 @@ import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; import Domain, { EncryptableKeys } from "@bitwarden/common/platform/models/domain/domain-base"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { OrgKey } from "@bitwarden/common/types/key"; import { CollectionData } from "./collection.data"; @@ -13,8 +14,8 @@ export const CollectionTypes = { export type CollectionType = (typeof CollectionTypes)[keyof typeof CollectionTypes]; export class Collection extends Domain { - id: string | undefined; - organizationId: string | undefined; + id: CollectionId | undefined; + organizationId: OrganizationId | undefined; name: EncString | undefined; externalId: string | undefined; readOnly: boolean = false; diff --git a/libs/admin-console/src/common/collections/models/collection.view.ts b/libs/admin-console/src/common/collections/models/collection.view.ts index f75ff565100..3a60320856d 100644 --- a/libs/admin-console/src/common/collections/models/collection.view.ts +++ b/libs/admin-console/src/common/collections/models/collection.view.ts @@ -2,6 +2,7 @@ import { Jsonify } from "type-fest"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { View } from "@bitwarden/common/models/view/view"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; import { Collection, CollectionType, CollectionTypes } from "./collection"; @@ -10,8 +11,8 @@ import { CollectionAccessDetailsResponse } from "./collection.response"; export const NestingDelimiter = "/"; export class CollectionView implements View, ITreeNodeObject { - id: string | undefined; - organizationId: string | undefined; + id: CollectionId | undefined; + organizationId: OrganizationId | undefined; name: string = ""; externalId: string | undefined; // readOnly applies to the items within a collection diff --git a/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts index 57df2d03398..d0dfd44e41d 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/empty-vault-nudge.service.ts @@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DefaultSingleNudgeService } from "../default-single-nudge.service"; @@ -42,7 +42,7 @@ export class EmptyVaultNudgeService extends DefaultSingleNudgeService { const orgIds = new Set(orgs.map((org) => org.id)); const canCreateCollections = orgs.some((org) => org.canCreateNewCollections); const hasManageCollections = collections.some( - (c) => c.manage && orgIds.has(c.organizationId!), + (c) => c.manage && orgIds.has(c.organizationId! as OrganizationId), ); // When the user has dismissed the nudge or spotlight, return the nudge status directly diff --git a/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts b/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts index 2529fc40b73..a8f29830f89 100644 --- a/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts +++ b/libs/angular/src/vault/services/custom-nudges-services/vault-settings-import-nudge.service.ts @@ -5,7 +5,7 @@ import { combineLatest, Observable, of, switchMap } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { UserId } from "@bitwarden/common/types/guid"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DefaultSingleNudgeService } from "../default-single-nudge.service"; @@ -46,7 +46,7 @@ export class VaultSettingsImportNudgeService extends DefaultSingleNudgeService { const orgIds = new Set(orgs.map((org) => org.id)); const canCreateCollections = orgs.some((org) => org.canCreateNewCollections); const hasManageCollections = collections.some( - (c) => c.manage && orgIds.has(c.organizationId!), + (c) => c.manage && orgIds.has(c.organizationId! as OrganizationId), ); // When the user has dismissed the nudge or spotlight, return the nudge status directly diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index f8de5293913..130b32e519e 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -3,12 +3,13 @@ import { Jsonify } from "type-fest"; import { ProductTierType } from "../../../billing/enums"; +import { OrganizationId } from "../../../types/guid"; import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; import { OrganizationData } from "../data/organization.data"; export class Organization { - id: string; + id: OrganizationId; name: string; status: OrganizationUserStatusType; @@ -99,7 +100,7 @@ export class Organization { return; } - this.id = obj.id; + this.id = obj.id as OrganizationId; this.name = obj.name; this.status = obj.status; this.type = obj.type; diff --git a/libs/common/src/admin-console/models/domain/policy.ts b/libs/common/src/admin-console/models/domain/policy.ts index b45acb9920d..8408f4832df 100644 --- a/libs/common/src/admin-console/models/domain/policy.ts +++ b/libs/common/src/admin-console/models/domain/policy.ts @@ -2,14 +2,14 @@ // @ts-strict-ignore import { ListResponse } from "../../../models/response/list.response"; import Domain from "../../../platform/models/domain/domain-base"; -import { PolicyId } from "../../../types/guid"; +import { OrganizationId, PolicyId } from "../../../types/guid"; import { PolicyType } from "../../enums"; import { PolicyData } from "../data/policy.data"; import { PolicyResponse } from "../response/policy.response"; export class Policy extends Domain { id: PolicyId; - organizationId: string; + organizationId: OrganizationId; type: PolicyType; data: any; @@ -26,7 +26,7 @@ export class Policy extends Domain { } this.id = obj.id; - this.organizationId = obj.organizationId; + this.organizationId = obj.organizationId as OrganizationId; this.type = obj.type; this.data = obj.data; this.enabled = obj.enabled; diff --git a/libs/common/src/models/export/collection-with-id.export.ts b/libs/common/src/models/export/collection-with-id.export.ts index a93f07b54e5..c973472e0bb 100644 --- a/libs/common/src/models/export/collection-with-id.export.ts +++ b/libs/common/src/models/export/collection-with-id.export.ts @@ -4,10 +4,12 @@ // eslint-disable-next-line no-restricted-imports import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common"; +import { CollectionId } from "../../types/guid"; + import { CollectionExport } from "./collection.export"; export class CollectionWithIdExport extends CollectionExport { - id: string; + id: CollectionId; static toView(req: CollectionWithIdExport, view = new CollectionView()) { view.id = req.id; diff --git a/libs/common/src/models/export/collection.export.ts b/libs/common/src/models/export/collection.export.ts index 88b68c08c58..b141346d03f 100644 --- a/libs/common/src/models/export/collection.export.ts +++ b/libs/common/src/models/export/collection.export.ts @@ -5,13 +5,14 @@ import { Collection as CollectionDomain, CollectionView } from "@bitwarden/admin-console/common"; import { EncString } from "../../key-management/crypto/models/enc-string"; +import { emptyGuid, OrganizationId } from "../../types/guid"; import { safeGetString } from "./utils"; export class CollectionExport { static template(): CollectionExport { const req = new CollectionExport(); - req.organizationId = "00000000-0000-0000-0000-000000000000"; + req.organizationId = emptyGuid as OrganizationId; req.name = "Collection name"; req.externalId = null; return req; @@ -35,7 +36,7 @@ export class CollectionExport { return domain; } - organizationId: string; + organizationId: OrganizationId; name: string; externalId: string; diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index bd0980cd36c..5a6aaf2ce51 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -20,3 +20,8 @@ export type OrganizationIntegrationConfigurationId = Opaque< string, "OrganizationIntegrationConfigurationId" >; + +/** + * A string representation of an empty guid. + */ +export const emptyGuid = "00000000-0000-0000-0000-000000000000"; diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 1a97bc5a325..4c25a01f965 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -9,6 +9,7 @@ import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; @@ -20,7 +21,7 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note. import { ImportResult } from "../models/import-result"; export abstract class BaseImporter { - organizationId: string = null; + organizationId: OrganizationId = null; // FIXME: This should be replaced by injecting the log service. protected logService: LogService = new ConsoleLogService(false); @@ -279,7 +280,7 @@ export abstract class BaseImporter { result.collections = result.folders.map((f) => { const collection = new CollectionView(); collection.name = f.name; - collection.id = f.id ?? undefined; // folder id may be null, which is not suitable for collections. + collection.id = (f.id as CollectionId) ?? undefined; // folder id may be null, which is not suitable for collections. return collection; }); result.folderRelationships = []; diff --git a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts index 026c501cf5a..b326bc5d351 100644 --- a/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-csv-importer.spec.ts @@ -1,4 +1,5 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { testData as TestData, @@ -103,7 +104,7 @@ describe("Keeper CSV Importer", () => { }); it("should create collections, with subcollections and relationships", async () => { - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(TestData); expect(result != null).toBe(true); @@ -126,7 +127,7 @@ describe("Keeper CSV Importer", () => { }); it("should create collections tree, with child collections and relationships", async () => { - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(testDataMultiCollection); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts index 22008f3b4c1..1141897a044 100644 --- a/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts +++ b/libs/importer/src/importers/keeper/keeper-json-importer.spec.ts @@ -1,4 +1,5 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { testData as TestData } from "../spec-data/keeper-json/testdata.json"; @@ -93,7 +94,7 @@ describe("Keeper Json Importer", () => { }); it("should create collections if part of an organization", async () => { - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(testDataJson); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts index 8736b3df0c8..b5479ce83cc 100644 --- a/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts +++ b/libs/importer/src/importers/netwrix/netwrix-passwordsecure-csv-importer.spec.ts @@ -1,4 +1,5 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { credentialsData, @@ -79,7 +80,7 @@ describe("Netwrix Password Secure CSV Importer", () => { }); it("should parse an item and create a collection when importing into an organization", async () => { - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(credentialsData); expect(result).not.toBeNull(); @@ -93,7 +94,7 @@ describe("Netwrix Password Secure CSV Importer", () => { }); it("should parse multiple collections", async () => { - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(credentialsDataWithFolders); expect(result).not.toBeNull(); diff --git a/libs/importer/src/importers/nordpass-csv-importer.spec.ts b/libs/importer/src/importers/nordpass-csv-importer.spec.ts index cadc7bca28c..e633310e6ee 100644 --- a/libs/importer/src/importers/nordpass-csv-importer.spec.ts +++ b/libs/importer/src/importers/nordpass-csv-importer.spec.ts @@ -1,4 +1,5 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { SecureNoteType, CipherType, FieldType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; @@ -224,7 +225,7 @@ describe("NordPass CSV Importer", () => { }); it("should parse an item and create a collection if organizationId is set", async () => { - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(secureNoteData); expect(result).not.toBeNull(); diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts index d4976f7a198..1ca12a9ce69 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.spec.ts @@ -1,4 +1,5 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; @@ -691,7 +692,7 @@ describe("1Password 1Pux Importer", () => { it("should create collections if part of an organization", async () => { const importer = new OnePassword1PuxImporter(); - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(SanitizedExportJson); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts index 12cfbbe62bb..67d90ef966a 100644 --- a/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts +++ b/libs/importer/src/importers/passsordxp/passwordxp-csv-importer.spec.ts @@ -1,3 +1,4 @@ +import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ImportResult } from "../../models/import-result"; @@ -146,7 +147,7 @@ describe("PasswordXPCsvImporter", () => { }); it("should convert folders to collections when importing into an organization", async () => { - importer.organizationId = "someOrg"; + importer.organizationId = "someOrg" as OrganizationId; const result: ImportResult = await importer.parse(withFolders); expect(result.success).toBe(true); expect(result.ciphers.length).toBe(5); @@ -172,7 +173,7 @@ describe("PasswordXPCsvImporter", () => { }); it("should convert multi-level folders to collections when importing into an organization", async () => { - importer.organizationId = "someOrg"; + importer.organizationId = "someOrg" as OrganizationId; const result: ImportResult = await importer.parse(withMultipleFolders); expect(result.success).toBe(true); expect(result.ciphers.length).toBe(5); diff --git a/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts index ea84603aef4..dc438c0edee 100644 --- a/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts +++ b/libs/importer/src/importers/password-depot/password-depot-17-xml-importer.spec.ts @@ -1,6 +1,7 @@ // 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 { CollectionView } from "@bitwarden/admin-console/common"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, SecureNoteType } from "@bitwarden/common/vault/enums"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherType } from "@bitwarden/sdk-internal"; @@ -485,7 +486,7 @@ describe("Password Depot 17 Xml Importer", () => { it("should parse groups nodes into collections when importing into an organization", async () => { const importer = new PasswordDepot17XmlImporter(); - importer.organizationId = "someOrgId"; + importer.organizationId = "someOrgId" as OrganizationId; const collection = new CollectionView(); collection.name = "tempDB"; const actual = [collection]; diff --git a/libs/importer/src/importers/protonpass/protonpass-json-importer.spec.ts b/libs/importer/src/importers/protonpass/protonpass-json-importer.spec.ts index b8550bcb191..a97239fbd10 100644 --- a/libs/importer/src/importers/protonpass/protonpass-json-importer.spec.ts +++ b/libs/importer/src/importers/protonpass/protonpass-json-importer.spec.ts @@ -2,6 +2,7 @@ import { MockProxy } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { testData } from "../spec-data/protonpass-json/protonpass.json"; @@ -90,7 +91,7 @@ describe("Protonpass Json Importer", () => { it("should create collections if part of an organization", async () => { const testDataJson = JSON.stringify(testData); - importer.organizationId = Utils.newGuid(); + importer.organizationId = Utils.newGuid() as OrganizationId; const result = await importer.parse(testDataJson); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/psono/psono-json-importer.spec.ts b/libs/importer/src/importers/psono/psono-json-importer.spec.ts index 3b4fcf67a30..29e5a8fb4ab 100644 --- a/libs/importer/src/importers/psono/psono-json-importer.spec.ts +++ b/libs/importer/src/importers/psono/psono-json-importer.spec.ts @@ -1,3 +1,4 @@ +import { OrganizationId } from "@bitwarden/common/types/guid"; import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; @@ -236,7 +237,7 @@ describe("PSONO JSON Importer", () => { it("should create collections if part of an organization", async () => { const importer = new PsonoJsonImporter(); - importer.organizationId = "someOrg"; + importer.organizationId = "someOrg" as OrganizationId; const result = await importer.parse(FoldersTestDataJson); expect(result != null).toBe(true); diff --git a/libs/importer/src/importers/zohovault-csv-importer.spec.ts b/libs/importer/src/importers/zohovault-csv-importer.spec.ts index e49b527cbbf..d3904fb521a 100644 --- a/libs/importer/src/importers/zohovault-csv-importer.spec.ts +++ b/libs/importer/src/importers/zohovault-csv-importer.spec.ts @@ -1,3 +1,4 @@ +import { OrganizationId } from "@bitwarden/common/types/guid"; 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"; @@ -73,7 +74,7 @@ describe("Zoho Vault CSV Importer", () => { it("should create collection and assign ciphers when importing into an organization", async () => { const importer = new ZohoVaultCsvImporter(); - importer.organizationId = "someOrgId"; + importer.organizationId = "someOrgId" as OrganizationId; const result = await importer.parse(samplezohovaultcsvdata); expect(result != null).toBe(true); expect(result.success).toBe(true); diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 4245b770ce4..ac560ed6f7f 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -9,6 +9,7 @@ import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin. import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -67,7 +68,7 @@ describe("ImportService", () => { describe("getImporterInstance", () => { describe("Get bitPasswordProtected importer", () => { let importer: Importer; - const organizationId = Utils.newGuid(); + const organizationId = Utils.newGuid() as OrganizationId; const password = Utils.newGuid(); const promptForPassword_callback = async () => { return password; @@ -98,7 +99,7 @@ describe("ImportService", () => { }); describe("setImportTarget", () => { - const organizationId = Utils.newGuid(); + const organizationId = Utils.newGuid() as OrganizationId; let importResult: ImportResult; @@ -145,19 +146,19 @@ describe("ImportService", () => { }); const mockImportTargetCollection = new CollectionView(); - mockImportTargetCollection.id = "myImportTarget"; + mockImportTargetCollection.id = "myImportTarget" as CollectionId; mockImportTargetCollection.name = "myImportTarget"; mockImportTargetCollection.organizationId = organizationId; const mockCollection1 = new CollectionView(); - mockCollection1.id = "collection1"; + mockCollection1.id = "collection1" as CollectionId; mockCollection1.name = "collection1"; mockCollection1.organizationId = organizationId; const mockCollection2 = new CollectionView(); - mockCollection1.id = "collection2"; - mockCollection1.name = "collection2"; - mockCollection1.organizationId = organizationId; + mockCollection2.id = "collection2" as CollectionId; + mockCollection2.name = "collection2"; + mockCollection2.organizationId = organizationId; it("passing importTarget adds it to collections", async () => { await importService["setImportTarget"]( diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 9acd4514b31..d3880f63bd5 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -19,6 +19,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response" import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType, toCipherTypeName } from "@bitwarden/common/vault/enums"; @@ -130,7 +131,7 @@ export class ImportService implements ImportServiceAbstraction { async import( importer: Importer, fileContents: string, - organizationId: string = null, + organizationId: OrganizationId = null, selectedImportTarget: FolderView | CollectionView = null, canAccessImportExport: boolean, ): Promise { @@ -204,7 +205,7 @@ export class ImportService implements ImportServiceAbstraction { getImporter( format: ImportType | "bitwardenpasswordprotected", promptForPassword_callback: () => Promise, - organizationId: string = null, + organizationId: OrganizationId = null, ): Importer { if (promptForPassword_callback == null) { return null; @@ -393,7 +394,10 @@ export class ImportService implements ImportServiceAbstraction { return await this.importApiService.postImportCiphers(request); } - private async handleOrganizationalImport(importResult: ImportResult, organizationId: string) { + private async handleOrganizationalImport( + importResult: ImportResult, + organizationId: OrganizationId, + ) { const request = new ImportOrganizationCiphersRequest(); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts index 3c513a2f067..1f2aaa8904b 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.spec.ts @@ -14,6 +14,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SelectComponent } from "@bitwarden/components"; @@ -33,9 +34,9 @@ const createMockCollection = ( canEdit = true, ): CollectionView => { return { - id, + id: id as CollectionId, name, - organizationId, + organizationId: organizationId as OrganizationId, externalId: "", readOnly, hidePasswords: false, From 29e16fc5e04e96dd0c0471aa1e9f324eceff5a4d Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 6 Aug 2025 09:34:43 -0400 Subject: [PATCH 03/12] [PM-22107] Update Remove Individual Vault policy dialog (#15323) * WIP * switch to signal * fix ts strict errors * clean up * refactor policy list service * implement vnext component * refactor to include feature flag check in display() * CR feedback * refactor submit to cancel before request is built * clean up * Fix typo --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../policies/base-policy.component.ts | 46 ++++++++++----- .../organizations/policies/index.ts | 1 + .../organization-data-ownership.component.ts | 10 ++++ .../policies/policies.component.html | 59 +++++++++++-------- .../policies/policies.component.ts | 43 ++++++-------- .../policies/policy-edit.component.html | 4 +- .../policies/policy-edit.component.ts | 7 +++ .../policies/require-sso.component.ts | 6 +- .../policies/reset-password.component.ts | 11 +++- .../restricted-item-types.component.ts | 8 +++ .../policies/single-org.component.ts | 3 + ...organization-data-ownership.component.html | 57 ++++++++++++++++++ ...t-organization-data-ownership.component.ts | 50 ++++++++++++++++ apps/web/src/app/app.component.ts | 4 ++ apps/web/src/locales/en/messages.json | 31 ++++++++++ .../policies/activate-autofill.component.ts | 6 +- .../models/request/policy.request.ts | 6 +- 17 files changed, 276 insertions(+), 76 deletions(-) create mode 100644 apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.html create mode 100644 apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy.component.ts index 01c45264236..3af99644dd2 100644 --- a/apps/web/src/app/admin-console/organizations/policies/base-policy.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/base-policy.component.ts @@ -1,12 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Directive, Input, OnInit } from "@angular/core"; import { UntypedFormControl, UntypedFormGroup } from "@angular/forms"; +import { Observable, of } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; export abstract class BasePolicy { abstract name: string; @@ -14,38 +14,56 @@ export abstract class BasePolicy { abstract type: PolicyType; abstract component: any; - display(organization: Organization) { - return true; + /** + * If true, the description will be reused in the policy edit modal. Set this to false if you + * have more complex requirements that you will implement in your template instead. + **/ + showDescription: boolean = true; + + display(organization: Organization, configService: ConfigService): Observable { + return of(true); } } @Directive() export abstract class BasePolicyComponent implements OnInit { - @Input() policyResponse: PolicyResponse; - @Input() policy: BasePolicy; + @Input() policyResponse: PolicyResponse | undefined; + @Input() policy: BasePolicy | undefined; enabled = new UntypedFormControl(false); - data: UntypedFormGroup = null; + data: UntypedFormGroup | undefined; ngOnInit(): void { - this.enabled.setValue(this.policyResponse.enabled); + this.enabled.setValue(this.policyResponse?.enabled); - if (this.policyResponse.data != null) { + if (this.policyResponse?.data != null) { this.loadData(); } } buildRequest() { - const request = new PolicyRequest(); - request.enabled = this.enabled.value; - request.type = this.policy.type; - request.data = this.buildRequestData(); + if (!this.policy) { + throw new Error("Policy was not found"); + } + + const request: PolicyRequest = { + type: this.policy.type, + enabled: this.enabled.value, + data: this.buildRequestData(), + }; return Promise.resolve(request); } + /** + * Enable optional validation before sumitting a respose for policy submission + * */ + confirm(): Promise | boolean { + return true; + } + protected loadData() { - this.data.patchValue(this.policyResponse.data ?? {}); + this.data?.patchValue(this.policyResponse?.data ?? {}); } protected buildRequestData() { diff --git a/apps/web/src/app/admin-console/organizations/policies/index.ts b/apps/web/src/app/admin-console/organizations/policies/index.ts index 828aa8230fa..6b6b2303b2f 100644 --- a/apps/web/src/app/admin-console/organizations/policies/index.ts +++ b/apps/web/src/app/admin-console/organizations/policies/index.ts @@ -3,6 +3,7 @@ export { BasePolicy, BasePolicyComponent } from "./base-policy.component"; export { DisableSendPolicy } from "./disable-send.component"; export { MasterPasswordPolicy } from "./master-password.component"; export { PasswordGeneratorPolicy } from "./password-generator.component"; +export { vNextOrganizationDataOwnershipPolicy } from "./vnext-organization-data-ownership.component"; export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component"; export { RequireSsoPolicy } from "./require-sso.component"; export { ResetPasswordPolicy } from "./reset-password.component"; diff --git a/apps/web/src/app/admin-console/organizations/policies/organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/organization-data-ownership.component.ts index 1c1710f7662..beb9fd5752a 100644 --- a/apps/web/src/app/admin-console/organizations/policies/organization-data-ownership.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/organization-data-ownership.component.ts @@ -1,6 +1,10 @@ import { Component } from "@angular/core"; +import { map, Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -9,6 +13,12 @@ export class OrganizationDataOwnershipPolicy extends BasePolicy { description = "personalOwnershipPolicyDesc"; type = PolicyType.OrganizationDataOwnership; component = OrganizationDataOwnershipPolicyComponent; + + display(organization: Organization, configService: ConfigService): Observable { + return configService + .getFeatureFlag$(FeatureFlag.CreateDefaultLocation) + .pipe(map((enabled) => !enabled)); + } } @Component({ diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.html b/apps/web/src/app/admin-console/organizations/policies/policies.component.html index 016d631019e..8eb204b65a4 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.html @@ -1,38 +1,45 @@ @let organization = organization$ | async; - + @if (isBreadcrumbingEnabled$ | async) { + + } - + @if (loading) { {{ "loading" | i18n }} - - - - - - - {{ - "on" | i18n - }} - {{ p.description | i18n }} - - - - + } + @if (!loading) { + + + @for (p of policies; track p.name) { + @if (p.display(organization, configService) | async) { + + + + @if (policiesEnabledMap.get(p.type)) { + {{ "on" | i18n }} + } + {{ p.description | i18n }} + + + } + } + + + } diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 8b6894871bd..3dfc4cc0c20 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -15,7 +15,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; import { @@ -25,7 +24,7 @@ import { import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { PolicyListService } from "../../core/policy-list.service"; -import { BasePolicy, RestrictedItemTypesPolicy } from "../policies"; +import { BasePolicy } from "../policies"; import { CollectionDialogTabType } from "../shared/components/collection-dialog"; import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component"; @@ -53,7 +52,7 @@ export class PoliciesComponent implements OnInit { private policyListService: PolicyListService, private organizationBillingService: OrganizationBillingServiceAbstraction, private dialogService: DialogService, - private configService: ConfigService, + protected configService: ConfigService, ) {} async ngOnInit() { @@ -71,35 +70,31 @@ export class PoliciesComponent implements OnInit { await this.load(); // Handle policies component launch from Event message - /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ - this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - if (qParams.policyId != null) { - const policyIdFromEvents: string = qParams.policyId; - for (const orgPolicy of this.orgPolicies) { - if (orgPolicy.id === policyIdFromEvents) { - for (let i = 0; i < this.policies.length; i++) { - if (this.policies[i].type === orgPolicy.type) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.edit(this.policies[i]); - break; + this.route.queryParams + .pipe(first()) + /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ + .subscribe(async (qParams) => { + if (qParams.policyId != null) { + const policyIdFromEvents: string = qParams.policyId; + for (const orgPolicy of this.orgPolicies) { + if (orgPolicy.id === policyIdFromEvents) { + for (let i = 0; i < this.policies.length; i++) { + if (this.policies[i].type === orgPolicy.type) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.edit(this.policies[i]); + break; + } } + break; } - break; } } - } - }); + }); }); } async load() { - if ( - (await this.configService.getFeatureFlag(FeatureFlag.RemoveCardItemTypePolicy)) && - this.policyListService.getPolicies().every((p) => !(p instanceof RestrictedItemTypesPolicy)) - ) { - this.policyListService.addPolicies([new RestrictedItemTypesPolicy()]); - } const response = await this.policyApiService.getPolicies(this.organizationId); this.orgPolicies = response.data != null && response.data.length > 0 ? response.data : []; this.orgPolicies.forEach((op) => { diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 7f33f08f888..90cfb52e5ad 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -22,7 +22,9 @@ {{ "loading" | i18n }}
-

{{ policy.description | i18n }}

+ @if (policy.showDescription) { +

{{ policy.description | i18n }}

+ }
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index d3d03d2aaae..2984db67d39 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -128,13 +128,20 @@ export class PolicyEditComponent implements AfterViewInit { } submit = async () => { + if ((await this.policyComponent.confirm()) == false) { + this.dialogRef.close(); + return; + } + let request: PolicyRequest; + try { request = await this.policyComponent.buildRequest(); } catch (e) { this.toastService.showToast({ variant: "error", title: null, message: e.message }); return; } + await this.policyApiService.putPolicy(this.data.organizationId, this.data.policy.type, request); this.toastService.showToast({ variant: "success", diff --git a/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts b/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts index 21de143dea6..3a0d196c593 100644 --- a/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/require-sso.component.ts @@ -1,7 +1,9 @@ import { Component } from "@angular/core"; +import { of } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -11,8 +13,8 @@ export class RequireSsoPolicy extends BasePolicy { type = PolicyType.RequireSso; component = RequireSsoPolicyComponent; - display(organization: Organization) { - return organization.useSso; + display(organization: Organization, configService: ConfigService) { + return of(organization.useSso); } } diff --git a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts index 62fc42f6a06..93a42285fbc 100644 --- a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; import { getOrganizationById, @@ -10,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -19,8 +20,8 @@ export class ResetPasswordPolicy extends BasePolicy { type = PolicyType.ResetPassword; component = ResetPasswordPolicyComponent; - display(organization: Organization) { - return organization.useResetPassword; + display(organization: Organization, configService: ConfigService) { + return of(organization.useResetPassword); } } @@ -52,6 +53,10 @@ export class ResetPasswordPolicyComponent extends BasePolicyComponent implements throw new Error("No user found."); } + if (!this.policyResponse) { + throw new Error("Policies not found"); + } + const organization = await firstValueFrom( this.organizationService .organizations$(userId) diff --git a/apps/web/src/app/admin-console/organizations/policies/restricted-item-types.component.ts b/apps/web/src/app/admin-console/organizations/policies/restricted-item-types.component.ts index 1bee5583718..6cad0fc0170 100644 --- a/apps/web/src/app/admin-console/organizations/policies/restricted-item-types.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/restricted-item-types.component.ts @@ -1,6 +1,10 @@ import { Component } from "@angular/core"; +import { Observable } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -9,6 +13,10 @@ export class RestrictedItemTypesPolicy extends BasePolicy { description = "restrictedItemTypePolicyDesc"; type = PolicyType.RestrictedItemTypes; component = RestrictedItemTypesPolicyComponent; + + display(organization: Organization, configService: ConfigService): Observable { + return configService.getFeatureFlag$(FeatureFlag.RemoveCardItemTypePolicy); + } } @Component({ diff --git a/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts b/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts index ad32b4218bc..613253ef8d9 100644 --- a/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/single-org.component.ts @@ -20,6 +20,9 @@ export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnI async ngOnInit() { super.ngOnInit(); + if (!this.policyResponse) { + throw new Error("Policies not found"); + } if (!this.policyResponse.canToggleState) { this.enabled.disable(); } diff --git a/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.html b/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.html new file mode 100644 index 00000000000..0abc40da683 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.html @@ -0,0 +1,57 @@ +

+ {{ "organizationDataOwnershipContent" | i18n }} + + {{ "organizationDataOwnershipContentAnchor" | i18n }}. + +

+ + + + {{ "turnOn" | i18n }} + + + + + {{ "organizationDataOwnershipWarningTitle" | i18n }} + +
+ {{ "organizationDataOwnershipWarningContentTop" | i18n }} +
+
    +
  • + {{ "organizationDataOwnershipWarning1" | i18n }} +
  • +
  • + {{ "organizationDataOwnershipWarning2" | i18n }} +
  • +
  • + {{ "organizationDataOwnershipWarning3" | i18n }} +
  • +
+
+ {{ "organizationDataOwnershipWarningContentBottom" | i18n }} + + {{ "organizationDataOwnershipContentAnchor" | i18n }}. + +
+
+ + + + + + +
+
diff --git a/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts b/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts new file mode 100644 index 00000000000..11b1548d9f9 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/policies/vnext-organization-data-ownership.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core"; +import { lastValueFrom, Observable } from "rxjs"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { DialogService } from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; + +export class vNextOrganizationDataOwnershipPolicy extends BasePolicy { + name = "organizationDataOwnership"; + description = "organizationDataOwnershipDesc"; + type = PolicyType.OrganizationDataOwnership; + component = vNextOrganizationDataOwnershipPolicyComponent; + showDescription = false; + + override display(organization: Organization, configService: ConfigService): Observable { + return configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation); + } +} + +@Component({ + selector: "vnext-policy-organization-data-ownership", + templateUrl: "vnext-organization-data-ownership.component.html", + standalone: true, + imports: [SharedModule], +}) +export class vNextOrganizationDataOwnershipPolicyComponent + extends BasePolicyComponent + implements OnInit +{ + constructor(private dialogService: DialogService) { + super(); + } + + @ViewChild("dialog", { static: true }) warningContent!: TemplateRef; + + override async confirm(): Promise { + if (this.policyResponse?.enabled && !this.enabled.value) { + const dialogRef = this.dialogService.open(this.warningContent); + const result = await lastValueFrom(dialogRef.closed); + return Boolean(result); + } + return true; + } +} diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index ceb2c788e75..694d0c6eb9a 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -35,12 +35,14 @@ import { MasterPasswordPolicy, PasswordGeneratorPolicy, OrganizationDataOwnershipPolicy, + vNextOrganizationDataOwnershipPolicy, RequireSsoPolicy, ResetPasswordPolicy, SendOptionsPolicy, SingleOrgPolicy, TwoFactorAuthenticationPolicy, RemoveUnlockWithPinPolicy, + RestrictedItemTypesPolicy, } from "./admin-console/organizations/policies"; const BroadcasterSubscriptionId = "AppComponent"; @@ -244,8 +246,10 @@ export class AppComponent implements OnDestroy, OnInit { new SingleOrgPolicy(), new RequireSsoPolicy(), new OrganizationDataOwnershipPolicy(), + new vNextOrganizationDataOwnershipPolicy(), new DisableSendPolicy(), new SendOptionsPolicy(), + new RestrictedItemTypesPolicy(), ]); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 62f73fd4935..edcc153bcf4 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -5429,6 +5429,37 @@ "organizationDataOwnership": { "message": "Enforce organization data ownership" }, + "organizationDataOwnershipDesc": { + "message": "Require all items to be owned by an organization, removing the option to store items at the account level.", + "description": "This is the policy description shown in the policy list." + }, + "organizationDataOwnershipContent": { + "message": "All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the ", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection be available for each member to store items. Learn more about managing the credential lifecycle.'" + }, + "organizationDataOwnershipContentAnchor":{ + "message": "credential lifecycle", + "description": "This will be used as a hyperlink" + }, + "organizationDataOwnershipWarningTitle":{ + "message": "Are you sure you want to proceed?" + }, + "organizationDataOwnershipWarning1":{ + "message": "will remain accessible to members" + }, + "organizationDataOwnershipWarning2":{ + "message": "will not be automatically selected when creating new items" + }, + "organizationDataOwnershipWarning3":{ + "message": "cannot be managed from the Admin Console until the user is offboarded" + }, + "organizationDataOwnershipWarningContentTop":{ + "message": "By turning this policy off, the default collection: " + }, + "organizationDataOwnershipWarningContentBottom":{ + "message": "Learn more about the ", + "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about the credential lifecycle.'" + }, "personalOwnership": { "message": "Remove individual vault" }, diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts index 61e2133d059..821509b43e2 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.ts @@ -1,7 +1,9 @@ import { Component } from "@angular/core"; +import { of } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { BasePolicy, BasePolicyComponent, @@ -13,8 +15,8 @@ export class ActivateAutofillPolicy extends BasePolicy { type = PolicyType.ActivateAutofill; component = ActivateAutofillPolicyComponent; - display(organization: Organization) { - return organization.useActivateAutofillPolicy; + display(organization: Organization, configService: ConfigService) { + return of(organization.useActivateAutofillPolicy); } } diff --git a/libs/common/src/admin-console/models/request/policy.request.ts b/libs/common/src/admin-console/models/request/policy.request.ts index 0f3b1be7d88..7b2e4f76063 100644 --- a/libs/common/src/admin-console/models/request/policy.request.ts +++ b/libs/common/src/admin-console/models/request/policy.request.ts @@ -1,9 +1,7 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { PolicyType } from "../../enums"; -export class PolicyRequest { +export type PolicyRequest = { type: PolicyType; enabled: boolean; data: any; -} +}; From 8980016d2d9dd0f4cc06654dd8a88abc1177d19f Mon Sep 17 00:00:00 2001 From: Ketan Mehta <45426198+ketanMehtaa@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:15:38 +0530 Subject: [PATCH 04/12] [PM-23378] clear selection after event on (#15465) * clear selection after event on individual part * added changes in org * added clearSelection in refresh() --------- Co-authored-by: Jason Ng --- .../organizations/collections/vault.component.html | 1 + .../organizations/collections/vault.component.ts | 6 +++++- .../vault/components/vault-items/vault-items.component.ts | 4 ++++ .../web/src/app/vault/individual-vault/vault.component.html | 1 + apps/web/src/app/vault/individual-vault/vault.component.ts | 3 +++ 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.html b/apps/web/src/app/admin-console/organizations/collections/vault.component.html index ddfcda04c76..1122f10e8f7 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.html +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.html @@ -84,6 +84,7 @@ {{ trashCleanupWarning }} (0); private vaultItemDialogRef?: DialogRef | undefined; + @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; + private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe( map((account) => account?.id), switchMap((id) => @@ -1430,6 +1433,7 @@ export class VaultComponent implements OnInit, OnDestroy { private refresh() { this.refresh$.next(); + this.vaultItemsComponent?.clearSelection(); } private go(queryParams: any = null) { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 96d274727dd..a8dd0056806 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -166,6 +166,10 @@ export class VaultItemsComponent { ); } + clearSelection() { + this.selection.clear(); + } + get showExtraColumn() { return this.showCollections || this.showGroups || this.showOwner; } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index c20209a0192..35b1a1876a1 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -37,6 +37,7 @@ {{ trashCleanupWarning }} implements OnInit, OnDestroy { @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; + @ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent; trashCleanupWarning: string = null; kdfIterations: number; @@ -1281,6 +1283,7 @@ export class VaultComponent implements OnInit, OnDestr private refresh() { this.refresh$.next(); + this.vaultItemsComponent?.clearSelection(); } private async go(queryParams: any = null) { From 55464a0fc9e4566465234183897ea4325b4e823c Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 6 Aug 2025 09:49:23 -0400 Subject: [PATCH 05/12] PM-24242 Add IDs to AtRisk Notification for automation (#15865) * PM-24242 * fix ts issue in storybook --- .../autofill/content/components/buttons/action-button.ts | 3 +++ .../at-risk-notification/container.lit-stories.ts | 8 ++++++-- .../components/notification/at-risk-password/container.ts | 4 +++- .../components/notification/at-risk-password/footer.ts | 1 + apps/browser/src/autofill/notification/bar.ts | 3 ++- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts index 339b628875c..b43bed7f96b 100644 --- a/apps/browser/src/autofill/content/components/buttons/action-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -8,6 +8,7 @@ import { Spinner } from "../icons"; export type ActionButtonProps = { buttonText: string | TemplateResult; + dataTestId?: string; disabled?: boolean; isLoading?: boolean; theme: Theme; @@ -17,6 +18,7 @@ export type ActionButtonProps = { export function ActionButton({ buttonText, + dataTestId, disabled = false, isLoading = false, theme, @@ -32,6 +34,7 @@ export function ActionButton({ return html` + `, diff --git a/libs/components/src/async-actions/standalone.stories.ts b/libs/components/src/async-actions/standalone.stories.ts index 07a1e2a98a2..1ed6f6c5a59 100644 --- a/libs/components/src/async-actions/standalone.stories.ts +++ b/libs/components/src/async-actions/standalone.stories.ts @@ -13,10 +13,10 @@ import { AsyncActionsModule } from "./async-actions.module"; import { BitActionDirective } from "./bit-action.directive"; const template = /*html*/ ` - - `; + `; @Component({ template, diff --git a/libs/components/src/badge/badge.stories.ts b/libs/components/src/badge/badge.stories.ts index a151547ef6a..0e5f44cc5c0 100644 --- a/libs/components/src/badge/badge.stories.ts +++ b/libs/components/src/badge/badge.stories.ts @@ -47,7 +47,7 @@ export const Primary: Story = { link (args)}>Badge
- button + button
`, @@ -108,40 +108,40 @@ export const VariantsAndInteractionStates: Story = { props: args, template: /*html*/ ` Default - - - - - - - + + + + + + +

Hover - - - - - - - + + + + + + +

Focus Visible - - - - - - - + + + + + + +

Disabled - - - - - - - + + + + + + + `, }), }; diff --git a/libs/components/src/banner/banner.stories.ts b/libs/components/src/banner/banner.stories.ts index 8338c9240b9..a7649a28228 100644 --- a/libs/components/src/banner/banner.stories.ts +++ b/libs/components/src/banner/banner.stories.ts @@ -49,10 +49,10 @@ export const Base: Story = { render: (args) => { return { props: args, - template: ` + template: /*html*/ ` (args)}> Content Really Long Text Lorem Ipsum Ipsum Ipsum - + `, }; diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 29a9e367fcc..180f5cb4aa9 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -31,7 +31,7 @@ export const Default: Story = { render: (args) => ({ props: args, template: /*html*/ ` - + `, }), args: { @@ -58,9 +58,9 @@ export const Small: Story = { props: args, template: /*html*/ `
- - - + + +
`, }), @@ -86,15 +86,15 @@ export const Disabled: Story = { export const DisabledWithAttribute: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` @if (disabled) { - - - + + + } @else { - - - + + + } `, }), @@ -107,12 +107,12 @@ export const DisabledWithAttribute: Story = { export const Block: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` - + [block]="true" Link - + block Link `, @@ -125,16 +125,16 @@ export const Block: Story = { export const WithIcon: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
-
- @@ -149,11 +149,11 @@ export const InteractionStates: Story = { props: args, template: /*html*/ `
- - - - - + + + + +
Anchor diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index bb8b2450de2..7cf8b774a23 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -78,16 +78,17 @@ type Story = StoryObj; export const Default: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` Foobar Dialog body text goes here. - - + + - + + `, @@ -166,7 +167,7 @@ export const ScrollingContent: Story = { export const TabContent: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` @@ -176,8 +177,8 @@ export const TabContent: Story = { - - + + `, @@ -211,7 +212,7 @@ export const WithCards: Story = {

Foo

- + @@ -231,7 +232,7 @@ export const WithCards: Story = {

Bar

- + @@ -248,9 +249,10 @@ export const WithCards: Story = { - - + + - + + `, @@ -43,14 +43,14 @@ export const Default: Story = { export const CustomIcon: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` Premium Subscription Available Message Content - - + + `, @@ -60,13 +60,13 @@ export const CustomIcon: Story = { export const HideIcon: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` Premium Subscription Available Message Content - - + + `, @@ -76,7 +76,7 @@ export const HideIcon: Story = { export const ScrollingContent: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` Alert Dialog @@ -87,8 +87,8 @@ export const ScrollingContent: Story = { end of sequence! - - + + `, @@ -101,13 +101,13 @@ export const ScrollingContent: Story = { export const TextOverflow: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` Alert Dialogdialogdialogdialogdialogdialogdialogdialogdialogdialogdialogdialogdialog Message Contentcontentcontentcontentcontentcontentcontentcontentcontentcontentcontent - - + + `, diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index fc6d29c11ad..9fe8b057d4e 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -346,11 +346,11 @@ export const ButtonInputGroup: Story = { - + - - - + + + `, }), @@ -363,11 +363,11 @@ export const DisabledButtonInputGroup: Story = { template: /*html*/ ` Label - + - - - + + + `, @@ -382,9 +382,9 @@ export const PartiallyDisabledButtonInputGroup: Story = { Label - - - + + + `, }), diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index fdcda07f021..45022a55535 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -29,7 +29,7 @@ export const Default: Story = { render: (args) => ({ props: args, template: /*html*/ ` - + `, }), }; @@ -76,7 +76,7 @@ export const NavContrast: Story = { template: /*html*/ `
- +
`, }), @@ -91,7 +91,7 @@ export const Contrast: Story = { template: /*html*/ `
- +
`, }), diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index 6187266c40c..15b81f3f67e 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -66,7 +66,7 @@ export const Default: Story = { props: args, template: /*html*/ ` - - @@ -187,7 +187,7 @@ export const TextOverflowWrap: Story = { const multipleActionListTemplate = /*html*/ ` - @@ -429,7 +429,7 @@ export const WithoutBorderRadius: Story = { template: /*html*/ ` - +
-
-
- +
`, @@ -134,7 +134,7 @@ export const Inline: Story = { props: args, template: /*html*/ ` - On the internet paragraphs often contain inline links, but few know that can be used for similar purposes. + On the internet paragraphs often contain inline links, but few know that can be used for similar purposes. `, }), @@ -147,10 +147,10 @@ export const Disabled: Story = { render: (args) => ({ props: args, template: /*html*/ ` - - + +
- +
`, }), diff --git a/libs/components/src/menu/menu.stories.ts b/libs/components/src/menu/menu.stories.ts index f1f4d8df000..7c4894cbb2f 100644 --- a/libs/components/src/menu/menu.stories.ts +++ b/libs/components/src/menu/menu.stories.ts @@ -57,7 +57,7 @@ export const ClosedMenu: Story = { props: args, template: /*html*/ `
- +
diff --git a/libs/components/src/navigation/nav-item.stories.ts b/libs/components/src/navigation/nav-item.stories.ts index 94d7d33cea3..c3f7e526ecb 100644 --- a/libs/components/src/navigation/nav-item.stories.ts +++ b/libs/components/src/navigation/nav-item.stories.ts @@ -86,9 +86,10 @@ export const WithoutRoute: Story = { export const WithChildButtons: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` + `; diff --git a/libs/components/src/section/section.stories.ts b/libs/components/src/section/section.stories.ts index 53e6bc078c5..f28cca0af7b 100644 --- a/libs/components/src/section/section.stories.ts +++ b/libs/components/src/section/section.stories.ts @@ -69,7 +69,7 @@ export const HeaderVariants: Story = {

Title with icon button suffix

- + `, }), @@ -88,7 +88,7 @@ export const HeaderEndSlotVariants: Story = {

Title with end slot icon button

- + `, }), @@ -103,7 +103,7 @@ export const HeaderWithPadding: Story = {

Card as immediate sibling

- +

bit-section-header has padding

@@ -114,7 +114,7 @@ export const HeaderWithPadding: Story = {

Card nested in immediate sibling

- +
@@ -127,7 +127,7 @@ export const HeaderWithPadding: Story = {

Item as immediate sibling

- + bit-section-header has padding @@ -138,7 +138,7 @@ export const HeaderWithPadding: Story = {

Item nested in immediate sibling

- + diff --git a/libs/components/src/tabs/tabs.stories.ts b/libs/components/src/tabs/tabs.stories.ts index 0fe88c57a16..3be63b433aa 100644 --- a/libs/components/src/tabs/tabs.stories.ts +++ b/libs/components/src/tabs/tabs.stories.ts @@ -153,7 +153,7 @@ export const PreserveContentTabs: Story = { export const KeyboardNavigation: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `

@@ -174,7 +174,7 @@ export const KeyboardNavigation: Story = {

This tab has no focusable content, but the panel should still be focusable

- + `, }), }; From 804ad79877fc4d1cebb2b2e987f12dca8d1130e4 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 7 Aug 2025 08:48:46 -0400 Subject: [PATCH 11/12] Fix extra signalr connection web (#15633) * Revert "fix(SignalR): Revert "[PM-23062] Fix extra signalr connections"" This reverts commit 97ec9a633988cbed7790ced508368e37384f5ca9. * Fix first login on web --- .../app/platform/web-environment.service.ts | 13 ++- .../config/config-api.service.abstraction.ts | 2 +- .../abstractions/environment.service.ts | 11 ++- .../services/config/config-api.service.ts | 2 +- .../services/config/config.service.spec.ts | 32 +++--- .../services/config/default-config.service.ts | 99 ++++++++++++------- .../services/default-environment.service.ts | 9 +- 7 files changed, 109 insertions(+), 59 deletions(-) diff --git a/apps/web/src/app/platform/web-environment.service.ts b/apps/web/src/app/platform/web-environment.service.ts index 1df842d6b31..4c4681ff715 100644 --- a/apps/web/src/app/platform/web-environment.service.ts +++ b/apps/web/src/app/platform/web-environment.service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Router } from "@angular/router"; -import { firstValueFrom, ReplaySubject } from "rxjs"; +import { firstValueFrom, Observable, ReplaySubject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { @@ -16,6 +16,7 @@ import { SelfHostedEnvironment, } from "@bitwarden/common/platform/services/default-environment.service"; import { StateProvider } from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/user-core"; export type WebRegionConfig = RegionConfig & { key: Region | string; // strings are used for custom environments @@ -27,6 +28,8 @@ export type WebRegionConfig = RegionConfig & { * Web specific environment service. Ensures that the urls are set from the window location. */ export class WebEnvironmentService extends DefaultEnvironmentService { + private _environmentSubject: ReplaySubject; + constructor( private win: Window, stateProvider: StateProvider, @@ -60,7 +63,9 @@ export class WebEnvironmentService extends DefaultEnvironmentService { // Override the environment observable with a replay subject const subject = new ReplaySubject(1); subject.next(environment); + this._environmentSubject = subject; this.environment$ = subject.asObservable(); + this.globalEnvironment$ = subject.asObservable(); } // Web setting env means navigating to a new location @@ -100,6 +105,12 @@ export class WebEnvironmentService extends DefaultEnvironmentService { // This return shouldn't matter as we are about to leave the current window return chosenRegionConfig.urls; } + + getEnvironment$(userId: UserId): Observable { + // Web does not support account switching, and even if it did, you'd be required to be the environment of where the application + // is running. + return this._environmentSubject.asObservable(); + } } export class WebCloudEnvironment extends CloudEnvironment { diff --git a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts index 3c191f59ccc..0460e8c715f 100644 --- a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts @@ -5,5 +5,5 @@ export abstract class ConfigApiServiceAbstraction { /** * Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context. */ - abstract get(userId: UserId | undefined): Promise; + abstract get(userId: UserId | null): Promise; } diff --git a/libs/common/src/platform/abstractions/environment.service.ts b/libs/common/src/platform/abstractions/environment.service.ts index b8931656848..86a0fbea242 100644 --- a/libs/common/src/platform/abstractions/environment.service.ts +++ b/libs/common/src/platform/abstractions/environment.service.ts @@ -95,6 +95,13 @@ export interface Environment { */ export abstract class EnvironmentService { abstract environment$: Observable; + + /** + * The environment stored in global state, when a user signs in the state stored here will become + * their user environment. + */ + abstract globalEnvironment$: Observable; + abstract cloudWebVaultUrl$: Observable; /** @@ -125,12 +132,12 @@ export abstract class EnvironmentService { * @param userId - The user id to set the cloud web vault app URL for. If null or undefined the global environment is set. * @param region - The region of the cloud web vault app. */ - abstract setCloudRegion(userId: UserId, region: Region): Promise; + abstract setCloudRegion(userId: UserId | null, region: Region): Promise; /** * Get the environment from state. Useful if you need to get the environment for another user. */ - abstract getEnvironment$(userId: UserId): Observable; + abstract getEnvironment$(userId: UserId): Observable; /** * @deprecated Use {@link getEnvironment$} instead. diff --git a/libs/common/src/platform/services/config/config-api.service.ts b/libs/common/src/platform/services/config/config-api.service.ts index f283410acea..b7ecb9c8712 100644 --- a/libs/common/src/platform/services/config/config-api.service.ts +++ b/libs/common/src/platform/services/config/config-api.service.ts @@ -10,7 +10,7 @@ export class ConfigApiService implements ConfigApiServiceAbstraction { private tokenService: TokenService, ) {} - async get(userId: UserId | undefined): Promise { + async get(userId: UserId | null): Promise { // Authentication adds extra context to config responses, if the user has an access token, we want to use it // We don't particularly care about ensuring the token is valid and not expired, just that it exists const authed: boolean = diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index ea3b56a32f1..e8a1872c4c1 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -10,9 +10,9 @@ import { FakeGlobalState, FakeSingleUserState, FakeStateProvider, - awaitAsync, mockAccountServiceWith, } from "../../../../spec"; +import { Matrix } from "../../../../spec/matrix"; import { subscribeTo } from "../../../../spec/observable-tracker"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; @@ -74,7 +74,8 @@ describe("ConfigService", () => { }); beforeEach(() => { - environmentService.environment$ = environmentSubject; + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); + environmentService.globalEnvironment$ = environmentSubject; sut = new DefaultConfigService( configApiService, environmentService, @@ -98,9 +99,12 @@ describe("ConfigService", () => { : serverConfigFactory(activeApiUrl + userId, tooOld); const globalStored = configStateDescription === "missing" - ? {} + ? { + [activeApiUrl]: null, + } : { [activeApiUrl]: serverConfigFactory(activeApiUrl, tooOld), + [activeApiUrl + "0"]: serverConfigFactory(activeApiUrl + userId, tooOld), }; beforeEach(() => { @@ -108,11 +112,6 @@ describe("ConfigService", () => { userState.nextState(userStored); }); - // sanity check - test("authed and unauthorized state are different", () => { - expect(globalStored[activeApiUrl]).not.toEqual(userStored); - }); - describe("fail to fetch", () => { beforeEach(() => { configApiService.get.mockRejectedValue(new Error("Unable to fetch")); @@ -178,6 +177,7 @@ describe("ConfigService", () => { beforeEach(() => { globalState.stateSubject.next(globalStored); userState.nextState(userStored); + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); }); it("does not fetch from server", async () => { await firstValueFrom(sut.serverConfig$); @@ -189,21 +189,13 @@ describe("ConfigService", () => { const actual = await firstValueFrom(sut.serverConfig$); expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); }); - - it("does not complete after emit", async () => { - const emissions = []; - const subscription = sut.serverConfig$.subscribe((v) => emissions.push(v)); - await awaitAsync(); - expect(emissions.length).toBe(1); - expect(subscription.closed).toBe(false); - }); }); }); }); it("gets global config when there is an locked active user", async () => { await accountService.switchAccount(userId); - environmentService.environment$ = of(environmentFactory(activeApiUrl)); + environmentService.globalEnvironment$ = of(environmentFactory(activeApiUrl)); globalState.stateSubject.next({ [activeApiUrl]: serverConfigFactory(activeApiUrl + "global"), @@ -236,7 +228,8 @@ describe("ConfigService", () => { beforeEach(() => { environmentSubject = new Subject(); - environmentService.environment$ = environmentSubject; + environmentService.globalEnvironment$ = environmentSubject; + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); sut = new DefaultConfigService( configApiService, environmentService, @@ -327,7 +320,8 @@ describe("ConfigService", () => { beforeEach(async () => { const config = serverConfigFactory("existing-data", tooOld); - environmentService.environment$ = environmentSubject; + environmentService.globalEnvironment$ = environmentSubject; + Matrix.autoMockMethod(environmentService.getEnvironment$, () => environmentSubject); globalState.stateSubject.next({ [apiUrl(0)]: config }); userState.stateSubject.next({ diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index 33f86d30885..2dad227876e 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -1,17 +1,18 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { combineLatest, + distinctUntilChanged, firstValueFrom, map, mergeWith, NEVER, Observable, of, - shareReplay, + ReplaySubject, + share, Subject, switchMap, tap, + timer, } from "rxjs"; import { SemVer } from "semver"; @@ -50,11 +51,15 @@ export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record { + return previous.getApiUrl() === current.getApiUrl(); +}; + // FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it. export class DefaultConfigService implements ConfigService { - private failedFetchFallbackSubject = new Subject(); + private failedFetchFallbackSubject = new Subject(); - serverConfig$: Observable; + serverConfig$: Observable; serverSettings$: Observable; @@ -67,32 +72,61 @@ export class DefaultConfigService implements ConfigService { private stateProvider: StateProvider, private authService: AuthService, ) { - const userId$ = this.stateProvider.activeUserId$; - const authStatus$ = userId$.pipe( - switchMap((userId) => (userId == null ? of(null) : this.authService.authStatusFor$(userId))), + const globalConfig$ = this.environmentService.globalEnvironment$.pipe( + distinctUntilChanged(environmentComparer), + switchMap((environment) => + this.globalConfigFor$(environment.getApiUrl()).pipe( + map((config) => { + return [config, null as UserId | null, environment, config] as const; + }), + ), + ), ); - this.serverConfig$ = combineLatest([ - userId$, - this.environmentService.environment$, - authStatus$, - ]).pipe( - switchMap(([userId, environment, authStatus]) => { - if (userId == null || authStatus !== AuthenticationStatus.Unlocked) { - return this.globalConfigFor$(environment.getApiUrl()).pipe( - map((config) => [config, null, environment] as const), - ); + this.serverConfig$ = this.stateProvider.activeUserId$.pipe( + distinctUntilChanged(), + switchMap((userId) => { + if (userId == null) { + // Global + return globalConfig$; } - return this.userConfigFor$(userId).pipe( - map((config) => [config, userId, environment] as const), + return this.authService.authStatusFor$(userId).pipe( + map((authStatus) => authStatus === AuthenticationStatus.Unlocked), + distinctUntilChanged(), + switchMap((isUnlocked) => { + if (!isUnlocked) { + return globalConfig$; + } + + return combineLatest([ + this.environmentService + .getEnvironment$(userId) + .pipe(distinctUntilChanged(environmentComparer)), + this.userConfigFor$(userId), + ]).pipe( + switchMap(([environment, config]) => { + if (config == null) { + // If the user doesn't have any config yet, use the global config for that url as the fallback + return this.globalConfigFor$(environment.getApiUrl()).pipe( + map( + (globalConfig) => + [null as ServerConfig | null, userId, environment, globalConfig] as const, + ), + ); + } + + return of([config, userId, environment, config] as const); + }), + ); + }), ); }), tap(async (rec) => { - const [existingConfig, userId, environment] = rec; + const [existingConfig, userId, environment, fallbackConfig] = rec; // Grab new config if older retrieval interval if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) { - await this.renewConfig(existingConfig, userId, environment); + await this.renewConfig(existingConfig, userId, environment, fallbackConfig); } }), switchMap(([existingConfig]) => { @@ -106,7 +140,7 @@ export class DefaultConfigService implements ConfigService { }), // If fetch fails, we'll emit on this subject to fallback to the existing config mergeWith(this.failedFetchFallbackSubject), - shareReplay({ refCount: true, bufferSize: 1 }), + share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(1000) }), ); this.cloudRegion$ = this.serverConfig$.pipe( @@ -155,19 +189,18 @@ export class DefaultConfigService implements ConfigService { // Updates the on-disk configuration with a newly retrieved configuration private async renewConfig( - existingConfig: ServerConfig, - userId: UserId, + existingConfig: ServerConfig | null, + userId: UserId | null, environment: Environment, + fallbackConfig: ServerConfig | null, ): Promise { try { // Feature flags often have a big impact on user experience, lets ensure we return some value // somewhat quickly even though it may not be accurate, we won't cancel the HTTP request // though so that hopefully it can have finished and hydrated a more accurate value. const handle = setTimeout(() => { - this.logService.info( - "Self-host environment did not respond in time, emitting previous config.", - ); - this.failedFetchFallbackSubject.next(existingConfig); + this.logService.info("Environment did not respond in time, emitting previous config."); + this.failedFetchFallbackSubject.next(fallbackConfig); }, SLOW_EMISSION_GUARD); const response = await this.configApiService.get(userId); clearTimeout(handle); @@ -195,17 +228,17 @@ export class DefaultConfigService implements ConfigService { // mutate error to be handled by catchError this.logService.error(`Unable to fetch ServerConfig from ${environment.getApiUrl()}`, e); // Emit the existing config - this.failedFetchFallbackSubject.next(existingConfig); + this.failedFetchFallbackSubject.next(fallbackConfig); } } - private globalConfigFor$(apiUrl: string): Observable { + private globalConfigFor$(apiUrl: string): Observable { return this.stateProvider .getGlobal(GLOBAL_SERVER_CONFIGURATIONS) - .state$.pipe(map((configs) => configs?.[apiUrl])); + .state$.pipe(map((configs) => configs?.[apiUrl] ?? null)); } - private userConfigFor$(userId: UserId): Observable { + private userConfigFor$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$; } } diff --git a/libs/common/src/platform/services/default-environment.service.ts b/libs/common/src/platform/services/default-environment.service.ts index df55693ba0b..4a1af68505a 100644 --- a/libs/common/src/platform/services/default-environment.service.ts +++ b/libs/common/src/platform/services/default-environment.service.ts @@ -133,6 +133,7 @@ export class DefaultEnvironmentService implements EnvironmentService { ); environment$: Observable; + globalEnvironment$: Observable; cloudWebVaultUrl$: Observable; constructor( @@ -148,6 +149,10 @@ export class DefaultEnvironmentService implements EnvironmentService { distinctUntilChanged((oldUserId: UserId, newUserId: UserId) => oldUserId == newUserId), ); + this.globalEnvironment$ = this.stateProvider + .getGlobal(GLOBAL_ENVIRONMENT_KEY) + .state$.pipe(map((state) => this.buildEnvironment(state?.region, state?.urls))); + this.environment$ = account$.pipe( switchMap((userId) => { const t = userId @@ -263,7 +268,7 @@ export class DefaultEnvironmentService implements EnvironmentService { return new SelfHostedEnvironment(urls); } - async setCloudRegion(userId: UserId, region: CloudRegion) { + async setCloudRegion(userId: UserId | null, region: CloudRegion) { if (userId == null) { await this.globalCloudRegionState.update(() => region); } else { @@ -271,7 +276,7 @@ export class DefaultEnvironmentService implements EnvironmentService { } } - getEnvironment$(userId: UserId): Observable { + getEnvironment$(userId: UserId): Observable { return this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$.pipe( map((state) => { return this.buildEnvironment(state?.region, state?.urls); From c13f8241c6b59ba22c816088ebc7b1db544b6589 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 7 Aug 2025 14:50:52 +0200 Subject: [PATCH 12/12] Remove worker js on safari (#15914) --- apps/browser/src/safari/desktop.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj b/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj index 7642e7d1859..05e6e8be978 100644 --- a/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj +++ b/apps/browser/src/safari/desktop.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */ = {isa = PBXBuildFile; fileRef = 03100CAE291891F4008E14EF /* encrypt-worker.js */; }; 55BC93932CB4268A008CA4C6 /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 55BC93922CB4268A008CA4C6 /* assets */; }; 55E0374D2577FA6B00979016 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E0374C2577FA6B00979016 /* AppDelegate.swift */; }; 55E037502577FA6B00979016 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55E0374E2577FA6B00979016 /* Main.storyboard */; }; @@ -53,7 +52,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 03100CAE291891F4008E14EF /* encrypt-worker.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; name = "encrypt-worker.js"; path = "../../../build/encrypt-worker.js"; sourceTree = ""; }; 5508DD7926051B5900A85C58 /* libswiftAppKit.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftAppKit.tbd; path = usr/lib/swift/libswiftAppKit.tbd; sourceTree = SDKROOT; }; 55BC93922CB4268A008CA4C6 /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../../build/assets; sourceTree = ""; }; 55E037482577FA6B00979016 /* desktop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktop.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -155,7 +153,6 @@ isa = PBXGroup; children = ( 55BC93922CB4268A008CA4C6 /* assets */, - 03100CAE291891F4008E14EF /* encrypt-worker.js */, 55E037702577FA6F00979016 /* popup */, 55E037712577FA6F00979016 /* background.js */, 55E037722577FA6F00979016 /* images */, @@ -272,7 +269,6 @@ 55E037802577FA6F00979016 /* background.html in Resources */, 55E0377A2577FA6F00979016 /* background.js in Resources */, 55E037792577FA6F00979016 /* popup in Resources */, - 03100CAF291891F4008E14EF /* encrypt-worker.js in Resources */, 55BC93932CB4268A008CA4C6 /* assets in Resources */, 55E0377C2577FA6F00979016 /* notification in Resources */, 55E0377E2577FA6F00979016 /* vendor.js in Resources */,