1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 21:50:15 +00:00

Merge branch 'main' into km/userkey-rotation-v2

This commit is contained in:
Bernd Schoolmann
2025-02-07 11:59:40 +01:00
committed by GitHub
84 changed files with 840 additions and 1518 deletions

2
.github/CODEOWNERS vendored
View File

@@ -134,7 +134,7 @@ libs/key-management @bitwarden/team-key-management-dev
libs/key-management-ui @bitwarden/team-key-management-dev
libs/common/src/key-management @bitwarden/team-key-management-dev
apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev
apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev
apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev
apps/browser/src/background/nativeMessaging.background.ts @bitwarden/team-key-management-dev
apps/desktop/src/services/biometric-message-handler.service.ts @bitwarden/team-key-management-dev

4
.github/codecov.yml vendored
View File

@@ -56,11 +56,11 @@ component_management:
- apps/browser/src/key-management/**
- apps/browser/src/background/nativeMessaging.background.ts
- apps/cli/src/key-management/**
- apps/desktop/destkop_native/core/src/biometric/**
- apps/desktop/desktop_native/core/src/biometric/**
- apps/desktop/src/key-management/**
- apps/desktop/src/services/biometric-message-handler.service.ts
- apps/desktop/src/services/native-messaging.service.ts
- apps/web/src/app/key-managemen/**
- apps/web/src/app/key-management/**
- libs/common/src/key-management/**
- libs/key-management/**
- libs/key-management-ui/**

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
"version": "2025.1.4",
"version": "2025.1.3",
"scripts": {
"build": "npm run build:chrome",
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",

View File

@@ -52,7 +52,7 @@ foreach ($subBuildPath in $subBuildPaths) {
"--verbose",
"--force",
"--sign",
"E7C9978F6FBCE0553429185C405E61F5380BE8EB",
"4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C",
"--entitlements",
$entitlementsPath
)

View File

@@ -25,6 +25,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat
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";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
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";
@@ -32,6 +33,7 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { BrowserApi } from "../../platform/browser/browser-api";
import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window";
import { NotificationCipherData } from "../content/components/cipher/types";
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
import { AutofillService } from "../services/abstractions/autofill.service";
@@ -82,6 +84,7 @@ export default class NotificationBackground {
bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(),
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
notificationRefreshFlagValue: () => this.getNotificationFlag(),
bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
};
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
@@ -132,6 +135,40 @@ export default class NotificationBackground {
return await firstValueFrom(this.domainSettingsService.neverDomains$);
}
/**
*
* Gets the current active tab and retrieves all decrypted ciphers
* for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects.
* If no active tab or URL is found, it returns an empty array.
*
* @returns {Promise<NotificationCipherData[]>}
*/
async getNotificationCipherData(): Promise<NotificationCipherData[]> {
const [currentTab, showFavicons, env] = await Promise.all([
BrowserApi.getTabFromCurrentWindow(),
firstValueFrom(this.domainSettingsService.showFavicons$),
firstValueFrom(this.environmentService.environment$),
]);
const iconsServerUrl = env.getIconsUrl();
const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl(currentTab.url);
return decryptedCiphers.map((view) => {
const { id, name, reprompt, favorite, login } = view;
return {
id,
name,
type: CipherType.Login,
reprompt,
favorite,
icon: buildCipherIcon(iconsServerUrl, view, showFavicons),
login: login && {
username: login.username,
},
};
});
}
/**
* Gets the active user server config from the config service.
*/

View File

@@ -6,10 +6,10 @@ import { Theme } from "@bitwarden/common/platform/enums";
import { themes, typography } from "../../../content/components/constants/styles";
import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons";
import { CipherData } from "./types";
import { NotificationCipherData } from "./types";
// @TODO support other cipher types (card, identity, notes, etc)
export function CipherInfo({ cipher, theme }: { cipher: CipherData; theme: Theme }) {
export function CipherInfo({ cipher, theme }: { cipher: NotificationCipherData; theme: Theme }) {
const { name, login } = cipher;
return html`

View File

@@ -12,7 +12,7 @@ import {
import { CipherAction } from "./cipher-action";
import { CipherIcon } from "./cipher-icon";
import { CipherInfo } from "./cipher-info";
import { CipherData } from "./types";
import { NotificationCipherData } from "./types";
const cipherIconWidth = "24px";
@@ -22,7 +22,7 @@ export function CipherItem({
notificationType,
theme = ThemeTypes.Light,
}: {
cipher: CipherData;
cipher: NotificationCipherData;
handleAction?: (e: Event) => void;
notificationType?: NotificationType;
theme: Theme;

View File

@@ -1,6 +1,4 @@
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const CipherTypes = {
export const CipherTypes = {
Login: 1,
SecureNote: 2,
Card: 3,
@@ -9,9 +7,7 @@ const CipherTypes = {
type CipherType = (typeof CipherTypes)[keyof typeof CipherTypes];
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const CipherRepromptTypes = {
export const CipherRepromptTypes = {
None: 0,
Password: 1,
} as const;
@@ -25,13 +21,16 @@ export type WebsiteIconData = {
icon: string;
};
export type CipherData = {
type BaseCipherData<CipherTypeValue> = {
id: string;
name: string;
type: CipherType;
type: CipherTypeValue;
reprompt: CipherRepromptType;
favorite: boolean;
icon: WebsiteIconData;
};
export type CipherData = BaseCipherData<CipherType> & {
accountCreationFieldType?: string;
login?: {
username: string;
@@ -46,3 +45,9 @@ export type CipherData = {
username?: string;
};
};
export type NotificationCipherData = BaseCipherData<typeof CipherTypes.Login> & {
login?: {
username: string;
};
};

View File

@@ -5,11 +5,11 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { NotificationType } from "../../../../notification/abstractions/notification-bar";
import { CipherData } from "../../cipher/types";
import { NotificationCipherData } from "../../cipher/types";
import { NotificationBody } from "../../notification/body";
type Args = {
ciphers: CipherData[];
ciphers: NotificationCipherData[];
notificationType: NotificationType;
theme: Theme;
};
@@ -38,7 +38,7 @@ export default {
fallbackImage: "https://example.com/fallback.png",
icon: "icon-class",
},
login: { username: "user@example.com", passkey: null },
login: { username: "user@example.com" },
},
],
theme: ThemeTypes.Light,

View File

@@ -5,7 +5,7 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationType } from "../../../notification/abstractions/notification-bar";
import { CipherItem } from "../cipher";
import { CipherData } from "../cipher/types";
import { NotificationCipherData } from "../cipher/types";
import { scrollbarStyles, spacing, themes, typography } from "../constants/styles";
import { ItemRow } from "../rows/item-row";
@@ -20,7 +20,7 @@ export function NotificationBody({
notificationType,
theme = ThemeTypes.Light,
}: {
ciphers: CipherData[];
ciphers: NotificationCipherData[];
customClasses?: string[];
notificationType?: NotificationType;
theme: Theme;

View File

@@ -8,8 +8,7 @@ import {
NotificationTypes,
NotificationType,
} from "../../../notification/abstractions/notification-bar";
import { createAutofillOverlayCipherDataMock } from "../../../spec/autofill-mocks";
import { CipherData } from "../cipher/types";
import { NotificationCipherData } from "../cipher/types";
import { themes, spacing } from "../constants/styles";
import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body";
@@ -24,23 +23,15 @@ export function NotificationContainer({
i18n,
theme = ThemeTypes.Light,
type,
ciphers,
}: NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void } & {
i18n: { [key: string]: string };
type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type`
ciphers: NotificationCipherData[];
}) {
const headerMessage = getHeaderMessage(i18n, type);
const showBody = true;
// @TODO remove mock ciphers for development
const ciphers = [
createAutofillOverlayCipherDataMock(1),
{ ...createAutofillOverlayCipherDataMock(2), icon: { imageEnabled: false } },
{
...createAutofillOverlayCipherDataMock(3),
icon: { imageEnabled: true, image: "https://localhost:8443/icons/webtests.dev/icon.png" },
},
] as CipherData[];
return html`
<div class=${notificationContainerStyles(theme)}>
${NotificationHeader({

View File

@@ -84,23 +84,25 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
document.body.innerHTML = "";
// Current implementations utilize a require for scss files which creates the need to remove the node.
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
const themeType = getTheme(globalThis, theme);
// There are other possible passed theme values, but for now, resolve to dark or light
const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
// @TODO use context to avoid prop drilling
return render(
NotificationContainer({
...notificationBarIframeInitData,
type: notificationBarIframeInitData.type as NotificationType,
theme: resolvedTheme,
handleCloseNotification,
i18n,
}),
document.body,
);
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => {
// @TODO use context to avoid prop drilling
return render(
NotificationContainer({
...notificationBarIframeInitData,
type: notificationBarIframeInitData.type as NotificationType,
theme: resolvedTheme,
handleCloseNotification,
i18n,
ciphers: cipherData,
}),
document.body,
);
});
}
setNotificationBarTheme();

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
"version": "2025.1.4",
"version": "2025.1.3",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
"version": "2025.1.4",
"version": "2025.1.3",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@@ -147,7 +147,9 @@ describe("Browser Utils Service", () => {
describe("isViewOpen", () => {
it("returns false if a heartbeat response is not received", async () => {
BrowserApi.sendMessageWithResponse = jest.fn().mockResolvedValueOnce(undefined);
chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => {
callback(undefined);
});
const isViewOpen = await browserPlatformUtilsService.isViewOpen();
@@ -155,16 +157,29 @@ describe("Browser Utils Service", () => {
});
it("returns true if a heartbeat response is received", async () => {
BrowserApi.sendMessageWithResponse = jest
.fn()
.mockImplementationOnce((subscriber) =>
Promise.resolve((subscriber === "checkVaultPopupHeartbeat") as any),
);
chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => {
callback(message.command === "checkVaultPopupHeartbeat");
});
const isViewOpen = await browserPlatformUtilsService.isViewOpen();
expect(isViewOpen).toBe(true);
});
it("returns false if special error is sent", async () => {
chrome.runtime.sendMessage = jest.fn().mockImplementation((message, callback) => {
chrome.runtime.lastError = new Error(
"Could not establish connection. Receiving end does not exist.",
);
callback(undefined);
});
const isViewOpen = await browserPlatformUtilsService.isViewOpen();
expect(isViewOpen).toBe(false);
chrome.runtime.lastError = null;
});
});
describe("copyToClipboard", () => {
@@ -228,6 +243,7 @@ describe("Browser Utils Service", () => {
});
it("copies the passed text using the offscreen document if the extension is using manifest v3", async () => {
BrowserApi.sendMessageWithResponse = jest.fn();
const text = "test";
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
getManifestVersionSpy.mockReturnValue(3);
@@ -302,6 +318,7 @@ describe("Browser Utils Service", () => {
});
it("reads the clipboard text using the offscreen document", async () => {
BrowserApi.sendMessageWithResponse = jest.fn();
offscreenDocumentService.offscreenApiSupported.mockReturnValue(true);
getManifestVersionSpy.mockReturnValue(3);
offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) =>

View File

@@ -169,7 +169,28 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
// Query views on safari since chrome.runtime.sendMessage does not timeout and will hang.
return BrowserApi.isPopupOpen();
}
return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat"));
return new Promise<boolean>((resolve, reject) => {
chrome.runtime.sendMessage({ command: "checkVaultPopupHeartbeat" }, (response) => {
if (chrome.runtime.lastError != null) {
// This error means that nothing was there to listen to the message,
// meaning the view is not open.
if (
chrome.runtime.lastError.message ===
"Could not establish connection. Receiving end does not exist."
) {
resolve(false);
return;
}
// All unhandled errors still reject
reject(chrome.runtime.lastError);
return;
}
resolve(Boolean(response));
});
});
}
lockTimeout(): number {

View File

@@ -4,7 +4,7 @@ import { Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
import { ImportComponent } from "@bitwarden/importer/ui";
import { ImportComponent } from "@bitwarden/importer-ui";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";

View File

@@ -25,7 +25,7 @@
"@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/importer-ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
"@bitwarden/platform": ["../../libs/platform/src"],

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.1.4",
"version": "2025.2.0",
"keywords": [
"bitwarden",
"password",

View File

@@ -42,7 +42,7 @@ async function run(context) {
if (process.env.GITHUB_ACTIONS === "true") {
if (is_mas) {
id = is_mas_dev
? "E7C9978F6FBCE0553429185C405E61F5380BE8EB"
? "4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C"
: "3rd Party Mac Developer Application: Bitwarden Inc";
} else {
id = "Developer ID Application: 8bit Solutions LLC";

View File

@@ -4,7 +4,7 @@ import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
import { ImportComponent } from "@bitwarden/importer/ui";
import { ImportComponent } from "@bitwarden/importer-ui";
@Component({
templateUrl: "import-desktop.component.html",

View File

@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
"version": "2025.1.4",
"version": "2025.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2025.1.4",
"version": "2025.2.0",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi"

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.1.4",
"version": "2025.2.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@@ -25,7 +25,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui";
import { SshKeyPasswordPromptComponent } from "@bitwarden/importer-ui";
import { PasswordRepromptService } from "@bitwarden/vault";
const BroadcasterSubscriptionId = "AddEditComponent";

View File

@@ -23,7 +23,7 @@
"@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/importer-ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
"@bitwarden/node/*": ["../../libs/node/src/*"],

View File

@@ -121,6 +121,22 @@
</app-side-nav>
<ng-container *ngIf="organization$ | async as organization">
<bit-banner
*ngIf="showAccountDeprovisioningBanner$ | async"
(onClose)="bannerService.hideBanner(organization)"
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
>
{{ "accountDeprovisioningNotification" | i18n }}
<a
href="https://bitwarden.com/help/claimed-accounts"
bitLink
linkType="contrast"
target="_blank"
rel="noreferrer"
>
{{ "learnMore" | i18n }}
</a>
</bit-banner>
<bit-banner
*ngIf="organization.isProviderUser"
[showClose]="false"

View File

@@ -3,7 +3,7 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { combineLatest, filter, firstValueFrom, map, Observable, switchMap } from "rxjs";
import { combineLatest, filter, map, Observable, switchMap, withLatestFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
@@ -22,6 +22,7 @@ import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/
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 { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -32,6 +33,8 @@ import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher
import { WebLayoutModule } from "../../../layouts/web-layout.module";
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
import { AccountDeprovisioningBannerService } from "./services/account-deprovisioning-banner.service";
@Component({
selector: "app-organization-layout",
templateUrl: "organization-layout.component.html",
@@ -61,6 +64,8 @@ export class OrganizationLayoutComponent implements OnInit {
organizationIsUnmanaged$: Observable<boolean>;
enterpriseOrganization$: Observable<boolean>;
showAccountDeprovisioningBanner$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
@@ -68,19 +73,36 @@ export class OrganizationLayoutComponent implements OnInit {
private configService: ConfigService,
private policyService: PolicyService,
private providerService: ProviderService,
protected bannerService: AccountDeprovisioningBannerService,
private accountService: AccountService,
) {}
async ngOnInit() {
document.body.classList.remove("layout_frontend");
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.organization$ = this.route.params.pipe(
map((p) => p.organizationId),
switchMap((id) => this.organizationService.organizations$(userId).pipe(getById(id))),
withLatestFrom(this.accountService.activeAccount$.pipe(getUserId)),
switchMap(([orgId, userId]) =>
this.organizationService.organizations$(userId).pipe(getById(orgId)),
),
filter((org) => org != null),
);
this.showAccountDeprovisioningBanner$ = combineLatest([
this.bannerService.showBanner$,
this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioningBanner),
this.organization$,
]).pipe(
map(
([dismissedOrgs, featureFlagEnabled, organization]) =>
organization.productTierType === ProductTierType.Enterprise &&
organization.isAdmin &&
!dismissedOrgs?.includes(organization.id) &&
featureFlagEnabled,
),
);
this.canAccessExport$ = this.organization$.pipe(map((org) => org.canAccessExport));
this.showPaymentAndHistory$ = this.organization$.pipe(

View File

@@ -0,0 +1,75 @@
import { firstValueFrom } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { AccountDeprovisioningBannerService } from "./account-deprovisioning-banner.service";
describe("Account Deprovisioning Banner Service", () => {
const userId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
let bannerService: AccountDeprovisioningBannerService;
beforeEach(async () => {
accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
bannerService = new AccountDeprovisioningBannerService(stateProvider);
});
it("updates state with single org", async () => {
const fakeOrg = new Organization();
fakeOrg.id = "123";
await bannerService.hideBanner(fakeOrg);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toEqual([fakeOrg.id]);
});
it("updates state with multiple orgs", async () => {
const fakeOrg1 = new Organization();
fakeOrg1.id = "123";
const fakeOrg2 = new Organization();
fakeOrg2.id = "234";
const fakeOrg3 = new Organization();
fakeOrg3.id = "987";
await bannerService.hideBanner(fakeOrg1);
await bannerService.hideBanner(fakeOrg2);
await bannerService.hideBanner(fakeOrg3);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toContain(fakeOrg1.id);
expect(state).toContain(fakeOrg2.id);
expect(state).toContain(fakeOrg3.id);
});
it("does not add the same org id multiple times", async () => {
const fakeOrg = new Organization();
fakeOrg.id = "123";
await bannerService.hideBanner(fakeOrg);
await bannerService.hideBanner(fakeOrg);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toEqual([fakeOrg.id]);
});
it("does not add null to the state", async () => {
await bannerService.hideBanner(null as unknown as Organization);
await bannerService.hideBanner(undefined as unknown as Organization);
const state = await firstValueFrom(bannerService.showBanner$);
expect(state).toBeNull();
});
});

View File

@@ -0,0 +1,40 @@
import { Injectable } from "@angular/core";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import {
ACCOUNT_DEPROVISIONING_BANNER_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
export const SHOW_BANNER_KEY = new UserKeyDefinition<string[]>(
ACCOUNT_DEPROVISIONING_BANNER_DISK,
"accountDeprovisioningBanner",
{
deserializer: (b) => b,
clearOn: [],
},
);
@Injectable({ providedIn: "root" })
export class AccountDeprovisioningBannerService {
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);
showBanner$ = this._showBanner.state$;
constructor(private stateProvider: StateProvider) {}
async hideBanner(organization: Organization) {
await this._showBanner.update((state) => {
if (!organization) {
return state;
}
if (!state) {
return [organization.id];
} else if (!state.includes(organization.id)) {
return [...state, organization.id];
}
return state;
});
}
}

View File

@@ -2,12 +2,17 @@
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
import { DeleteManagedMemberWarningService } from "../../services/delete-managed-member/delete-managed-member-warning.service";
import { BulkUserDetails } from "./bulk-status.component";
type BulkDeleteDialogParams = {
@@ -31,12 +36,20 @@ export class BulkDeleteDialogComponent {
@Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams,
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
) {
this.organizationId = dialogParams.organizationId;
this.users = dialogParams.users;
}
async submit() {
if (
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning))
) {
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
}
try {
this.loading = true;
this.error = null;

View File

@@ -26,61 +26,47 @@
{{ "nonCompliantMembersError" | i18n }}
</bit-callout>
<ng-template #userDisplay let-user>
<div class="tw-flex tw-items-center">
<div class="tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<ng-container *ngIf="user.name; else emailOnly">
<div class="tw-truncate" [title]="user.name + ' ' + user.email">
{{ user.name }}
<small class="tw-block tw-text-muted tw-truncate">
{{ user.email }}
</small>
</div>
</ng-container>
<ng-template #emailOnly>
<div class="tw-truncate" [title]="user.email">{{ user.email }}</div>
</ng-template>
</div>
</ng-template>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeUsersWarning" | i18n }}</p>
</bit-callout>
<bit-table *ngIf="accountDeprovisioning.enabled">
<bit-table>
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
<th bitCell>{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }}</th>
<th bitCell class="tw-w-1/2" *ngIf="this.showNoMasterPasswordWarning">
{{ "details" | i18n }}
</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
<td bitCell class="tw-max-w-0">
<ng-container
*ngTemplateOutlet="userDisplay; context: { $implicit: user }"
></ng-container>
</td>
<td bitCell *ngIf="this.showNoMasterPasswordWarning">
<span class="tw-block tw-lowercase tw-text-muted">
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
<ng-container *ngIf="user.hasMasterPassword === false">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
{{ "noMasterPassword" | i18n }}
</ng-container>
</span>
</td>
</tr>
</ng-template>
</bit-table>
<bit-table *ngIf="!accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell colspan="2">{{ "user" | i18n }}</th>
<th bitCell *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</td>
<td bitCell>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</td>
<td bitCell *ngIf="this.showNoMasterPasswordWarning">
<td bitCell class="tw-w-1/2" *ngIf="this.showNoMasterPasswordWarning">
<span class="tw-block tw-lowercase tw-text-muted">
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
<ng-container *ngIf="user.hasMasterPassword === false">
@@ -95,55 +81,21 @@
</ng-container>
<ng-container *ngIf="done">
<bit-table *ngIf="accountDeprovisioning.enabled">
<bit-table>
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell>{{ "status" | i18n }}</th>
<th bitCell class="tw-w-1/2">
{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }}
</th>
<th bitCell class="tw-w-1/2">{{ "status" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
</td>
<td bitCell *ngIf="statuses.has(user.id)">
{{ statuses.get(user.id) }}
</td>
<td bitCell *ngIf="!statuses.has(user.id)">
{{ "bulkFilteredMessage" | i18n }}
</td>
</tr>
</ng-template>
</bit-table>
<bit-table *ngIf="!accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell>{{ "status" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
<td bitCell class="tw-max-w-0">
<ng-container
*ngTemplateOutlet="userDisplay; context: { $implicit: user }"
></ng-container>
</td>
<td bitCell *ngIf="statuses.has(user.id)">
{{ statuses.get(user.id) }}

View File

@@ -53,6 +53,7 @@ import {
convertToSelectionView,
PermissionMode,
} from "../../../shared/components/access-selector";
import { DeleteManagedMemberWarningService } from "../../services/delete-managed-member/delete-managed-member-warning.service";
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
import { inputEmailLimitValidator } from "./validators/input-email-limit.validator";
@@ -176,6 +177,7 @@ export class MemberDialogComponent implements OnDestroy {
organizationService: OrganizationService,
private toastService: ToastService,
private configService: ConfigService,
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
) {
this.organization$ = accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -639,6 +641,27 @@ export class MemberDialogComponent implements OnDestroy {
return;
}
const showWarningDialog = combineLatest([
this.organization$,
this.deleteManagedMemberWarningService.warningAcknowledged(this.params.organizationId),
this.accountDeprovisioningEnabled$,
]).pipe(
map(
([organization, acknowledged, featureFlagEnabled]) =>
featureFlagEnabled &&
organization.isOwner &&
organization.productTierType === ProductTierType.Enterprise &&
!acknowledged,
),
);
if (await firstValueFrom(showWarningDialog)) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return;
}
}
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
@@ -667,6 +690,10 @@ export class MemberDialogComponent implements OnDestroy {
title: null,
message: this.i18nService.t("organizationUserDeleted", this.params.name),
});
if (await firstValueFrom(this.accountDeprovisioningEnabled$)) {
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
}
this.close(MemberDialogResult.Deleted);
};

View File

@@ -81,6 +81,7 @@ import {
ResetPasswordComponent,
ResetPasswordDialogResult,
} from "./components/reset-password.component";
import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service";
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
protected statusType = OrganizationUserStatusType;
@@ -138,6 +139,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
private collectionService: CollectionService,
private billingApiService: BillingApiServiceAbstraction,
private configService: ConfigService,
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
) {
super(
apiService,
@@ -585,6 +587,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
async bulkDelete() {
if (this.accountDeprovisioningEnabled) {
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
if (
!warningAcknowledged &&
this.organization.isOwner &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return;
}
}
}
if (this.actionPromise != null) {
return;
}
@@ -774,6 +793,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
async deleteUser(user: OrganizationUserView) {
if (this.accountDeprovisioningEnabled) {
const warningAcknowledged = await firstValueFrom(
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
);
if (
!warningAcknowledged &&
this.organization.isOwner &&
this.organization.productTierType === ProductTierType.Enterprise
) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) {
return false;
}
}
}
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
@@ -792,6 +828,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return false;
}
if (this.accountDeprovisioningEnabled) {
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
}
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id,
user.id,

View File

@@ -0,0 +1,51 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
FakeAccountService,
FakeStateProvider,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { DialogService } from "@bitwarden/components";
import { DeleteManagedMemberWarningService } from "./delete-managed-member-warning.service";
describe("Delete managed member warning service", () => {
const userId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
let dialogService: MockProxy<DialogService>;
let warningService: DeleteManagedMemberWarningService;
beforeEach(() => {
accountService = mockAccountServiceWith(userId);
stateProvider = new FakeStateProvider(accountService);
dialogService = mock();
warningService = new DeleteManagedMemberWarningService(stateProvider, dialogService);
});
it("warningAcknowledged returns false for ids that have not acknowledged the warning", async () => {
const id = Utils.newGuid();
const acknowledged = await firstValueFrom(warningService.warningAcknowledged(id));
expect(acknowledged).toEqual(false);
});
it("warningAcknowledged returns true for ids that have acknowledged the warning", async () => {
const id1 = Utils.newGuid();
const id2 = Utils.newGuid();
const id3 = Utils.newGuid();
await warningService.acknowledgeWarning(id1);
await warningService.acknowledgeWarning(id3);
const acknowledged1 = await firstValueFrom(warningService.warningAcknowledged(id1));
const acknowledged2 = await firstValueFrom(warningService.warningAcknowledged(id2));
const acknowledged3 = await firstValueFrom(warningService.warningAcknowledged(id3));
expect(acknowledged1).toEqual(true);
expect(acknowledged2).toEqual(false);
expect(acknowledged3).toEqual(true);
});
});

View File

@@ -0,0 +1,70 @@
import { Injectable } from "@angular/core";
import { map } from "rxjs";
import {
DELETE_MANAGED_USER_WARNING,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { DialogService } from "@bitwarden/components";
export const SHOW_WARNING_KEY = new UserKeyDefinition<string[]>(
DELETE_MANAGED_USER_WARNING,
"showDeleteManagedUserWarning",
{
deserializer: (b) => b,
clearOn: [],
},
);
@Injectable({ providedIn: "root" })
export class DeleteManagedMemberWarningService {
private _acknowledged = this.stateProvider.getActive(SHOW_WARNING_KEY);
private acknowledgedState$ = this._acknowledged.state$;
constructor(
private stateProvider: StateProvider,
private dialogService: DialogService,
) {}
async acknowledgeWarning(organizationId: string) {
await this._acknowledged.update((state) => {
if (!organizationId) {
return state;
}
if (!state) {
return [organizationId];
} else if (!state.includes(organizationId)) {
return [...state, organizationId];
}
return state;
});
}
async showWarning() {
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteManagedUserWarning",
},
content: {
key: "deleteManagedUserWarningDesc",
},
type: "danger",
icon: "bwi-exclamation-circle",
acceptButtonText: { key: "continue" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
return confirmed;
}
warningAcknowledged(organizationId: string) {
return this.acknowledgedState$.pipe(
map((acknowledgedIds) => acknowledgedIds?.includes(organizationId) ?? false),
);
}
}

View File

@@ -11,8 +11,8 @@ import {
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ImportComponent } from "@bitwarden/importer/ui";
import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core";
import { ImportComponent } from "@bitwarden/importer-ui";
import { LooseComponentsModule, SharedModule } from "../../../shared";
import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service";

View File

@@ -9,7 +9,7 @@
</div>
<app-danger-zone>
<ng-container *ngIf="showSetNewDeviceLoginProtection$ | async">
<ng-container>
<button
*ngIf="verifyNewDeviceLogin"
type="button"

View File

@@ -34,7 +34,6 @@ export class AccountComponent implements OnInit, OnDestroy {
showChangeEmail$: Observable<boolean> = new Observable();
showPurgeVault$: Observable<boolean> = new Observable();
showDeleteAccount$: Observable<boolean> = new Observable();
showSetNewDeviceLoginProtection$: Observable<boolean> = new Observable();
verifyNewDeviceLogin: boolean = true;
constructor(
@@ -48,9 +47,6 @@ export class AccountComponent implements OnInit, OnDestroy {
async ngOnInit() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.showSetNewDeviceLoginProtection$ = this.configService.getFeatureFlag$(
FeatureFlag.NewDeviceVerification,
);
const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);

View File

@@ -11,6 +11,7 @@ import { ProviderService } from "@bitwarden/common/admin-console/abstractions/pr
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
@@ -24,6 +25,7 @@ describe("ProductSwitcherService", () => {
let organizationService: MockProxy<OrganizationService>;
let providerService: MockProxy<ProviderService>;
let accountService: FakeAccountService;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let activeRouteParams = convertToParamMap({ organizationId: "1234" });
const getLastSync = jest.fn().mockResolvedValue(new Date("2024-05-14"));
const userId = Utils.newGuid() as UserId;
@@ -43,11 +45,13 @@ describe("ProductSwitcherService", () => {
organizationService = mock<OrganizationService>();
providerService = mock<ProviderService>();
accountService = mockAccountServiceWith(userId);
platformUtilsService = mock<PlatformUtilsService>();
router.url = "/";
router.events = of({});
organizationService.organizations$.mockReturnValue(of([{}] as Organization[]));
providerService.getAll.mockResolvedValue([] as Provider[]);
platformUtilsService.isSelfHost.mockReturnValue(false);
TestBed.configureTestingModule({
providers: [
@@ -55,6 +59,7 @@ describe("ProductSwitcherService", () => {
{ provide: OrganizationService, useValue: organizationService },
{ provide: ProviderService, useValue: providerService },
{ provide: AccountService, useValue: accountService },
{ provide: PlatformUtilsService, useValue: platformUtilsService },
{
provide: ActivatedRoute,
useValue: {
@@ -138,11 +143,31 @@ describe("ProductSwitcherService", () => {
});
describe("Admin/Organizations", () => {
it("includes Organizations in other when there are organizations", async () => {
it("includes Organizations with the internal route in other when there are organizations on cloud", async () => {
initiateService();
const products = await firstValueFrom(service.products$);
const organizations = products.other.find((p) => p.name === "Organizations");
expect(organizations).toBeDefined();
expect(organizations.marketingRoute.route).toBe("/create-organization");
expect(organizations.marketingRoute.external).toBe(false);
expect(products.other.find((p) => p.name === "Organizations")).toBeDefined();
expect(products.bento.find((p) => p.name === "Admin Console")).toBeUndefined();
});
it("includes Organizations with the external route in other when there are organizations on Self-Host", async () => {
platformUtilsService.isSelfHost.mockReturnValue(true);
initiateService();
const products = await firstValueFrom(service.products$);
const organizations = products.other.find((p) => p.name === "Organizations");
expect(organizations).toBeDefined();
expect(organizations.marketingRoute.route).toBe("https://bitwarden.com/products/business/");
expect(organizations.marketingRoute.external).toBe(true);
expect(products.other.find((p) => p.name === "Organizations")).toBeDefined();
expect(products.bento.find((p) => p.name === "Admin Console")).toBeUndefined();
});

View File

@@ -21,6 +21,7 @@ import {
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
export type ProductSwitcherItem = {
@@ -101,6 +102,7 @@ export class ProductSwitcherService {
private i18n: I18nPipe,
private syncService: SyncService,
private accountService: AccountService,
private platformUtilsService: PlatformUtilsService,
) {
this.pollUntilSynced();
}
@@ -151,6 +153,16 @@ export class ProductSwitcherService {
// TODO: This should be migrated to an Observable provided by the provider service and moved to the combineLatest above. See AC-2092.
const providers = await this.providerService.getAll();
const orgsMarketingRoute = this.platformUtilsService.isSelfHost()
? {
route: "https://bitwarden.com/products/business/",
external: true,
}
: {
route: "/create-organization",
external: false,
};
const products = {
pm: {
name: "Password Manager",
@@ -197,10 +209,7 @@ export class ProductSwitcherService {
orgs: {
name: "Organizations",
icon: "bwi-business",
marketingRoute: {
route: "https://bitwarden.com/products/business/",
external: true,
},
marketingRoute: orgsMarketingRoute,
otherProductOverrides: {
name: "Share your passwords",
supportingText: this.i18n.transform("protectYourFamilyOrBusiness"),

View File

@@ -12,7 +12,6 @@ import {
activeAuthGuard,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap";
import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
@@ -90,7 +89,6 @@ import { SMLandingComponent } from "./secrets-manager/secrets-manager-landing/sm
import { DomainRulesComponent } from "./settings/domain-rules.component";
import { PreferencesComponent } from "./settings/preferences.component";
import { CredentialGeneratorComponent } from "./tools/credential-generator/credential-generator.component";
import { GeneratorComponent } from "./tools/generator.component";
import { ReportsModule } from "./tools/reports";
import { AccessComponent } from "./tools/send/access.component";
import { SendAccessExplainerComponent } from "./tools/send/send-access-explainer.component";
@@ -831,10 +829,11 @@ const routes: Routes = [
titleId: "exportVault",
} satisfies RouteDataProperties,
},
...generatorSwap(GeneratorComponent, CredentialGeneratorComponent, {
{
path: "generator",
component: CredentialGeneratorComponent,
data: { titleId: "generator" } satisfies RouteDataProperties,
}),
},
],
},
{

View File

@@ -66,8 +66,6 @@ import { ProductSwitcherModule } from "../layouts/product-switcher/product-switc
import { UserLayoutComponent } from "../layouts/user-layout.component";
import { DomainRulesComponent } from "../settings/domain-rules.component";
import { PreferencesComponent } from "../settings/preferences.component";
import { GeneratorComponent } from "../tools/generator.component";
import { PasswordGeneratorHistoryComponent } from "../tools/password-generator-history.component";
import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component";
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
import { AddEditCustomFieldsComponent } from "../vault/individual-vault/add-edit-custom-fields.component";
@@ -139,8 +137,6 @@ import { SharedModule } from "./shared.module";
OrgUnsecuredWebsitesReportComponent,
OrgUserConfirmComponent,
OrgWeakPasswordsReportComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PreferencesComponent,
PremiumBadgeComponent,
ProfileComponent,
@@ -206,8 +202,6 @@ import { SharedModule } from "./shared.module";
OrgUnsecuredWebsitesReportComponent,
OrgUserConfirmComponent,
OrgWeakPasswordsReportComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PreferencesComponent,
PremiumBadgeComponent,
ProfileComponent,

View File

@@ -1,479 +0,0 @@
<app-header></app-header>
<bit-container>
<app-callout type="info" *ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'">
{{ "passwordGeneratorPolicyInEffect" | i18n }}
</app-callout>
<div class="card card-generated bg-light my-4">
<div class="card-body">
<bit-color-password
[password]="type === 'password' ? password : username"
[appCopyText]="type === 'password' ? password : username"
></bit-color-password>
</div>
</div>
<div class="form-group" role="radiogroup" aria-labelledby="typeHeading">
<label id="typeHeading" class="d-block">{{ "whatWouldYouLikeToGenerate" | i18n }}</label>
<div class="form-check form-check-inline" *ngFor="let o of typeOptions">
<input
class="form-check-input"
type="radio"
[(ngModel)]="type"
name="Type"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="type === o.value"
/>
<label class="form-check-label" for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<ng-container *ngIf="type === 'password'">
<div aria-labelledby="passwordTypeHeading" class="form-group" role="radiogroup">
<label id="passwordTypeHeading" class="d-block">{{ "passwordType" | i18n }}</label>
<div class="form-check form-check-inline" *ngFor="let o of passTypeOptions">
<input
class="form-check-input"
type="radio"
[(ngModel)]="passwordOptions.type"
name="PasswordType"
id="passwordType_{{ o.value }}"
[value]="o.value"
(change)="savePasswordOptions()"
[checked]="passwordOptions.type === o.value"
/>
<label class="form-check-label" for="passwordType_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<ng-container *ngIf="passwordOptions.type === 'passphrase'">
<div class="row">
<div class="form-group col-4">
<label for="num-words">{{ "numWords" | i18n }}</label>
<input
id="num-words"
class="form-control"
type="number"
min="3"
max="20"
[(ngModel)]="passwordOptions.numWords"
(blur)="savePasswordOptions()"
/>
</div>
<div class="form-group col-4">
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input
id="word-separator"
class="form-control"
type="text"
maxlength="1"
[(ngModel)]="passwordOptions.wordSeparator"
(blur)="savePasswordOptions()"
/>
</div>
</div>
<label class="d-block">{{ "options" | i18n }}</label>
<div class="form-group">
<div class="form-check">
<input
id="capitalize"
class="form-check-input"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.capitalize"
[disabled]="enforcedPasswordPolicyOptions?.capitalize"
/>
<label for="capitalize" class="form-check-label">{{ "capitalize" | i18n }}</label>
</div>
<div class="form-check">
<input
id="include-number"
class="form-check-input"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.includeNumber"
[disabled]="enforcedPasswordPolicyOptions?.includeNumber"
/>
<label for="include-number" class="form-check-label">{{ "includeNumber" | i18n }}</label>
</div>
</div>
</ng-container>
<ng-container *ngIf="passwordOptions.type === 'password'">
<div class="row">
<div class="form-group col-4">
<label for="length">{{ "length" | i18n }}</label>
<input
id="length"
class="form-control"
type="number"
[min]="passwordOptions.minLength"
max="128"
[(ngModel)]="passwordOptions.length"
(blur)="savePasswordOptions()"
(change)="lengthChanged()"
/>
</div>
<div class="form-group col-4">
<label for="min-length">{{ "passwordMinLength" | i18n }}</label>
<input
id="min-length"
class="form-control"
type="text"
readonly="true"
[value]="passwordOptions.length"
/>
<span
class="tw-sr-only"
attr.aria-label="{{ 'passwordMinLength' | i18n }}"
role="status"
aria-live="polite"
>
{{ passwordOptionsMinLengthForReader$ | async }}
</span>
</div>
<div class="form-group col-4">
<label for="min-number">{{ "minNumbers" | i18n }}</label>
<input
id="min-number"
class="form-control"
type="number"
min="0"
max="9"
[(ngModel)]="passwordOptions.minNumber"
(input)="onPasswordOptionsMinNumberInput($event)"
(change)="minNumberChanged()"
/>
</div>
<div class="form-group col-4">
<label for="min-special">{{ "minSpecial" | i18n }}</label>
<input
id="min-special"
class="form-control"
type="number"
min="0"
max="9"
[(ngModel)]="passwordOptions.minSpecial"
(input)="onPasswordOptionsMinSpecialInput($event)"
(change)="minSpecialChanged()"
/>
</div>
</div>
<label class="d-block">{{ "options" | i18n }}</label>
<div class="form-group">
<div class="form-check">
<input
id="uppercase"
class="form-check-input"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.uppercase"
[disabled]="enforcedPasswordPolicyOptions?.useUppercase"
attr.aria-label="{{ 'uppercase' | i18n }}"
/>
<label for="uppercase" class="form-check-label">A-Z</label>
</div>
<div class="form-check">
<input
id="lowercase"
class="form-check-input"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.lowercase"
[disabled]="enforcedPasswordPolicyOptions?.useLowercase"
attr.aria-label="{{ 'lowercase' | i18n }}"
/>
<label for="lowercase" class="form-check-label">a-z</label>
</div>
<div class="form-check">
<input
id="numbers"
class="form-check-input"
type="checkbox"
(change)="savePasswordOptions()"
[ngModel]="passwordOptions.number"
(ngModelChange)="setPasswordOptionsNumber($event)"
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
attr.aria-label="{{ 'numbers' | i18n }}"
/>
<label for="numbers" class="form-check-label">0-9</label>
</div>
<div class="form-check">
<input
id="special"
class="form-check-input"
type="checkbox"
[ngModel]="passwordOptions.special"
(ngModelChange)="setPasswordOptionsSpecial($event)"
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
attr.aria-label="{{ 'specialCharacters' | i18n }}"
/>
<label for="special" class="form-check-label">!&#64;#$%^&amp;*</label>
</div>
<div class="form-check">
<input
id="ambiguous"
class="form-check-input"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="avoidAmbiguous"
/>
<label for="ambiguous" class="form-check-label">{{ "ambiguous" | i18n }}</label>
</div>
</div>
</ng-container>
<div class="d-flex">
<div>
<button type="button" class="btn btn-primary" (click)="regenerate()">
{{ "regeneratePassword" | i18n }}
</button>
<button type="button" class="btn btn-outline-secondary" (click)="copy()">
{{ "copyPassword" | i18n }}
</button>
</div>
<div class="ml-auto">
<button
type="button"
class="btn btn-outline-secondary"
(click)="history()"
appA11yTitle="{{ 'passwordHistory' | i18n }}"
>
<i class="bwi bwi-clock bwi-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="type === 'username'">
<div aria-labelledby="usernameTypeHeading" class="form-group" role="radiogroup">
<div class="d-block">
<label id="usernameTypeHeading">{{ "usernameType" | i18n }}</label>
<a
class="ml-auto"
href="https://bitwarden.com/help/generator/#username-types"
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</div>
<div class="form-check" *ngFor="let o of usernameTypeOptions">
<input
class="form-check-input"
type="radio"
[(ngModel)]="usernameOptions.type"
name="UsernameType"
id="usernameType_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.type === o.value"
/>
<label class="form-check-label" for="usernameType_{{ o.value }}">
{{ o.name }}
<div class="small text-muted">{{ o.desc }}</div>
</label>
</div>
</div>
<ng-container *ngIf="usernameOptions.type === 'forwarded'">
<div class="form-group" role="listbox">
<label class="d-block">{{ "service" | i18n }}</label>
<select
id="ForwardTypeDropdown"
name="ForwardType"
[(ngModel)]="usernameOptions.forwardedService"
(change)="saveUsernameOptions()"
class="form-control w-auto"
>
<option *ngFor="let o of forwardOptions" [ngValue]="o.value" role="option">
{{ o.name }}
</option>
</select>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'simplelogin'">
<div class="form-group col-4">
<label for="simplelogin-apikey">{{ "apiKey" | i18n }}</label>
<input
id="simplelogin-apikey"
class="form-control"
type="password"
[(ngModel)]="usernameOptions.forwardedSimpleLoginApiKey"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="form-group col-4" *ngIf="isSelfHosted">
<label for="simplelogin-baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="simplelogin-baseUrl"
class="form-control"
type="text"
name="SimpleLoginDomain"
[(ngModel)]="usernameOptions.forwardedSimpleLoginBaseUrl"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'duckduckgo'">
<div class="form-group col-4">
<label for="duckduckgo-apikey">{{ "apiKey" | i18n }}</label>
<input
id="duckduckgo-apikey"
class="form-control"
type="password"
name="DuckDuckGoApiKey"
[(ngModel)]="usernameOptions.forwardedDuckDuckGoToken"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="form-group col-4">
<label for="anonaddy-apikey">{{ "apiAccessToken" | i18n }}</label>
<input
id="anonaddy-apikey"
class="form-control"
type="password"
[(ngModel)]="usernameOptions.forwardedAnonAddyApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="form-group col-4">
<label for="anonaddy-domain">{{ "aliasDomain" | i18n }}</label>
<input
id="anonaddy-domain"
class="form-control"
type="text"
[(ngModel)]="usernameOptions.forwardedAnonAddyDomain"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="form-group col-4" *ngIf="isSelfHosted">
<label for="anonaddy-baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="anonaddy-baseUrl"
class="form-control"
type="text"
name="AnonAddyDomain"
[(ngModel)]="usernameOptions.forwardedAnonAddyBaseUrl"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'firefoxrelay'">
<div class="form-group col-4">
<label for="firefox-apikey">{{ "apiAccessToken" | i18n }}</label>
<input
id="firefox-apikey"
class="form-control"
type="password"
[(ngModel)]="usernameOptions.forwardedFirefoxApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'fastmail'">
<div class="form-group col-4">
<label for="fastmail-apiToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="fastmail-apiToken"
class="form-control"
type="password"
[(ngModel)]="usernameOptions.forwardedFastmailApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.forwardedService === 'forwardemail'">
<div class="form-group col-4">
<label for="forwardemail-apikey">{{ "apiAccessToken" | i18n }}</label>
<input
id="forwardemail-apikey"
class="form-control"
type="password"
[(ngModel)]="usernameOptions.forwardedForwardEmailApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="form-group col-4">
<label for="forwardemail-domain">{{ "aliasDomain" | i18n }}</label>
<input
id="forwardemail-domain"
class="form-control"
type="text"
[(ngModel)]="usernameOptions.forwardedForwardEmailDomain"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
</ng-container>
<div class="row" *ngIf="usernameOptions.type === 'subaddress'">
<div class="form-group col-4">
<label for="subaddress-email">{{ "emailAddress" | i18n }}</label>
<input
id="subaddress-email"
class="form-control"
type="text"
[(ngModel)]="usernameOptions.subaddressEmail"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<div class="row" *ngIf="usernameOptions.type === 'catchall'">
<div class="form-group col-4">
<label for="catchall-domain">{{ "domainName" | i18n }}</label>
<input
id="catchall-domain"
class="form-control"
type="text"
[(ngModel)]="usernameOptions.catchallDomain"
(blur)="saveUsernameOptions()"
/>
</div>
</div>
<ng-container *ngIf="usernameOptions.type === 'word'">
<label class="d-block">{{ "options" | i18n }}</label>
<div class="row">
<div class="form-group">
<div class="form-check">
<input
id="capitalizeUsername"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordCapitalize"
/>
<label for="capitalizeUsername" class="form-check-label">{{
"capitalize" | i18n
}}</label>
</div>
<div class="form-check">
<input
id="includeNumberUsername"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordIncludeNumber"
/>
<label for="includeNumberUsername" class="form-check-label">{{
"includeNumber" | i18n
}}</label>
</div>
</div>
</div>
</ng-container>
<div #form [appApiAction]="usernameGeneratingPromise">
<button
type="button"
class="btn btn-submit btn-primary"
(click)="$any(form).loading ? false : regenerate()"
[attr.aria-disabled]="$any(form).loading ? 'true' : null"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "regenerateUsername" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="copy()">
{{ "copyUsername" | i18n }}
</button>
</div>
</ng-container>
<ng-template #historyTemplate></ng-template>
</bit-container>

View File

@@ -1,73 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, NgZone } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
import {
PasswordGenerationServiceAbstraction,
UsernameGenerationServiceAbstraction,
} from "@bitwarden/generator-legacy";
import { PasswordGeneratorHistoryComponent } from "./password-generator-history.component";
@Component({
selector: "app-generator",
templateUrl: "generator.component.html",
})
export class GeneratorComponent extends BaseGeneratorComponent {
constructor(
passwordGenerationService: PasswordGenerationServiceAbstraction,
usernameGenerationService: UsernameGenerationServiceAbstraction,
accountService: AccountService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
logService: LogService,
route: ActivatedRoute,
ngZone: NgZone,
private dialogService: DialogService,
toastService: ToastService,
) {
super(
passwordGenerationService,
usernameGenerationService,
platformUtilsService,
accountService,
i18nService,
logService,
route,
ngZone,
window,
toastService,
);
if (platformUtilsService.isSelfHost()) {
// Allow only valid email forwarders for self host
this.forwardOptions = this.forwardOptions.filter((forwarder) => forwarder.validForSelfHosted);
}
}
get isSelfHosted(): boolean {
return this.platformUtilsService.isSelfHost();
}
async history() {
this.dialogService.open(PasswordGeneratorHistoryComponent);
}
lengthChanged() {
document.getElementById("length").focus();
}
minNumberChanged() {
document.getElementById("min-number").focus();
}
minSpecialChanged() {
document.getElementById("min-special").focus();
}
}

View File

@@ -1,7 +1,7 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { ImportComponent } from "@bitwarden/importer/ui";
import { ImportComponent } from "@bitwarden/importer-ui";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";

View File

@@ -1,47 +0,0 @@
<bit-dialog>
<span bitDialogTitle>
{{ "passwordHistory" | i18n }}
</span>
<span bitDialogContent>
<bit-table *ngIf="history.length">
<ng-template body>
<tr bitRow *ngFor="let h of history">
<td bitCell>
<bit-color-password
[password]="h.password"
class="tw-block tw-font-mono"
[appCopyText]="h.password"
></bit-color-password>
<small bitTypography="body2" class="tw-text-muted">
{{ h.date | date: "medium" }}
</small>
</td>
<td bitCell class="tw-w-0">
<button
type="button"
bitIconButton="bwi-clone"
(click)="copy(h.password)"
[appA11yTitle]="'copyPassword' | i18n"
></button>
</td>
</tr>
</ng-template>
</bit-table>
<div *ngIf="!history.length">
{{ "noPasswordsInList" | i18n }}
</div>
</span>
<ng-container bitDialogFooter>
<button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
<button
type="button"
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
title="{{ 'clear' | i18n }}"
[bitAction]="clear"
></button>
</ng-container>
</bit-dialog>

View File

@@ -1,22 +0,0 @@
import { Component } from "@angular/core";
import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@Component({
selector: "app-password-generator-history",
templateUrl: "password-generator-history.component.html",
})
export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent {
constructor(
passwordGenerationService: PasswordGenerationServiceAbstraction,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
toastService: ToastService,
) {
super(passwordGenerationService, platformUtilsService, i18nService, window, toastService);
}
}

View File

@@ -366,6 +366,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
const cipherData = new CipherData(cipherResponse);
cipher = new Cipher(cipherData);
// Update organizationUseTotp from server response
this.cipher.organizationUseTotp = cipher.organizationUseTotp;
}
// Store the updated cipher so any following edits use the most up to date cipher

View File

@@ -1718,9 +1718,6 @@
"message": "Avoid ambiguous characters",
"description": "Label for the avoid ambiguous characters checkbox."
},
"regeneratePassword": {
"message": "Regenerate password"
},
"length": {
"message": "Length"
},
@@ -4773,9 +4770,6 @@
"passwordGeneratorPolicyDesc": {
"message": "Set requirements for password generator."
},
"passwordGeneratorPolicyInEffect": {
"message": "One or more organization policies are affecting your generator settings."
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
},
@@ -6744,15 +6738,6 @@
"message": "Generator",
"description": "Short for 'credential generator'."
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
},
"passwordType": {
"message": "Password type"
},
"regenerateUsername": {
"message": "Regenerate username"
},
"generateUsername": {
"message": "Generate username"
},
@@ -6793,9 +6778,6 @@
}
}
},
"usernameType": {
"message": "Username type"
},
"plusAddressedEmail": {
"message": "Plus addressed email",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -7015,9 +6997,6 @@
"message": "Hostname",
"description": "Part of a URL."
},
"apiAccessToken": {
"message": "API access token"
},
"deviceVerification": {
"message": "Device verification"
},
@@ -8728,9 +8707,6 @@
"message": "Self-host server URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"aliasDomain": {
"message": "Alias domain"
},
"alreadyHaveAccount": {
"message": "Already have an account?"
},
@@ -10346,6 +10322,15 @@
}
}
},
"accountDeprovisioningNotification" : {
"message": "Administrators now have the ability to delete member accounts that belong to a claimed domain."
},
"deleteManagedUserWarningDesc": {
"message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action."
},
"deleteManagedUserWarning": {
"message": "Delete is a new action!"
},
"seatsRemaining": {
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
"placeholders": {

View File

@@ -1,37 +1,3 @@
app-generator {
#lengthRange {
width: 100%;
}
.card-generated {
.card-body {
@include themify($themes) {
background: themed("foregroundColor");
}
align-items: center;
display: flex;
flex-wrap: wrap;
font-family: $font-family-monospace;
font-size: $font-size-lg;
justify-content: center;
text-align: center;
}
}
}
app-password-generator-history {
.list-group-item {
line-height: 1;
@include themify($themes) {
background: themed("backgroundColor");
}
.password {
font-family: $font-family-monospace;
}
}
}
tools-import {
textarea {
height: 150px;

View File

@@ -19,7 +19,7 @@
"@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/importer-ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
"@bitwarden/platform": ["../../libs/platform/src"],

View File

@@ -37,25 +37,5 @@
></bit-nav-item>
</app-side-nav>
<bit-banner
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
(onClose)="(true)"
*ngIf="
(showProviderClientVaultPrivacyWarningBanner$ | async) &&
(providerClientVaultPrivacyBannerService.showBanner$ | async) != false
"
(onClose)="providerClientVaultPrivacyBannerService.hideBanner()"
>
{{ "providerClientVaultPrivacyNotification" | i18n }}
<a
href="https://bitwarden.com/contact/"
bitLink
linkType="secondary"
target="_blank"
rel="noreferrer"
>
{{ "contactBitwardenSupport" | i18n }} </a
>.
</bit-banner>
<router-outlet></router-outlet>
</app-layout>

View File

@@ -10,27 +10,15 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { BannerModule, IconModule, LinkModule } from "@bitwarden/components";
import { IconModule } from "@bitwarden/components";
import { ProviderPortalLogo } from "@bitwarden/web-vault/app/admin-console/icons/provider-portal-logo";
import { WebLayoutModule } from "@bitwarden/web-vault/app/layouts/web-layout.module";
import { ProviderClientVaultPrivacyBannerService } from "./services/provider-client-vault-privacy-banner.service";
@Component({
selector: "providers-layout",
templateUrl: "providers-layout.component.html",
standalone: true,
imports: [
CommonModule,
RouterModule,
JslibModule,
WebLayoutModule,
IconModule,
LinkModule,
BannerModule,
],
imports: [CommonModule, RouterModule, JslibModule, WebLayoutModule, IconModule],
})
export class ProvidersLayoutComponent implements OnInit, OnDestroy {
protected readonly logo = ProviderPortalLogo;
@@ -41,15 +29,9 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
protected isBillable: Observable<boolean>;
protected canAccessBilling$: Observable<boolean>;
protected showProviderClientVaultPrivacyWarningBanner$ = this.configService.getFeatureFlag$(
FeatureFlag.ProviderClientVaultPrivacyBanner,
);
constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
private configService: ConfigService,
protected providerClientVaultPrivacyBannerService: ProviderClientVaultPrivacyBannerService,
) {}
ngOnInit() {

View File

@@ -1,31 +0,0 @@
import { Injectable } from "@angular/core";
import {
StateProvider,
AC_BANNERS_DISMISSED_DISK,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>(
AC_BANNERS_DISMISSED_DISK,
"showProviderClientVaultPrivacyBanner",
{
deserializer: (b) => b,
clearOn: [],
},
);
/** Displays a banner warning provider users that client organization vaults
* will soon become inaccessible directly. */
@Injectable({ providedIn: "root" })
export class ProviderClientVaultPrivacyBannerService {
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);
showBanner$ = this._showBanner.state$;
constructor(private stateProvider: StateProvider) {}
async hideBanner() {
await this._showBanner.update(() => false);
}
}

View File

@@ -56,7 +56,7 @@ describe("DefaultAdminTaskService", () => {
expect(apiService.send).toHaveBeenCalledWith(
"POST",
`/tasks/${organizationId}/bulk-create`,
tasks,
{ tasks },
true,
true,
);

View File

@@ -43,6 +43,12 @@ export class DefaultAdminTaskService implements AdminTaskService {
organizationId: OrganizationId,
tasks: CreateTasksRequest[],
): Promise<void> {
await this.apiService.send("POST", `/tasks/${organizationId}/bulk-create`, tasks, true, true);
await this.apiService.send(
"POST",
`/tasks/${organizationId}/bulk-create`,
{ tasks },
true,
true,
);
}
}

View File

@@ -22,7 +22,7 @@
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/importer-ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
"@bitwarden/platform": ["../../libs/platform/src"],

View File

@@ -1,7 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
@@ -28,7 +27,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@@ -57,7 +55,6 @@ export class SsoComponent implements OnInit {
protected redirectUri: string;
protected state: string;
protected codeChallenge: string;
protected activeUserId: UserId;
constructor(
protected ssoLoginService: SsoLoginServiceAbstraction,
@@ -77,11 +74,7 @@ export class SsoComponent implements OnInit {
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected accountService: AccountService,
protected toastService: ToastService,
) {
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
this.activeUserId = account?.id;
});
}
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs/no-async-subscribe
@@ -233,10 +226,8 @@ export class SsoComponent implements OnInit {
// - TDE login decryption options component
// - Browser SSO on extension open
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
orgSsoIdentifier,
this.activeUserId,
);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId);
// Users enrolled in admin acct recovery can be forced to set a new password after
// having the admin set a temp password for them (affects TDE & standard users)

View File

@@ -2,7 +2,6 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router";
import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs";
@@ -32,7 +31,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import {
AsyncActionsModule,
ButtonModule,
@@ -128,7 +126,6 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
protected changePasswordRoute = "set-password";
protected forcePasswordResetRoute = "update-temp-password";
protected successRoute = "vault";
protected activeUserId: UserId;
constructor(
protected loginStrategyService: LoginStrategyServiceAbstraction,
@@ -151,10 +148,6 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService, toastService);
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
this.activeUserId = account?.id;
});
}
async ngOnInit() {
@@ -269,10 +262,8 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
// Save off the OrgSsoIdentifier for use in the TDE flows
// - TDE login decryption options component
// - Browser SSO on extension open
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
this.orgIdentifier,
this.activeUserId,
);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId);
this.loginEmailService.clearValues();
// note: this flow affects both TDE & standard users

View File

@@ -35,7 +35,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
@@ -74,8 +73,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected successRoute = "vault";
protected twoFactorTimeoutRoute = "authentication-timeout";
protected activeUserId: UserId;
get isDuoProvider(): boolean {
return (
this.selectedProviderType === TwoFactorProviderType.Duo ||
@@ -108,10 +105,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
this.activeUserId = account?.id;
});
// Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired
this.loginStrategyService.authenticationSessionTimeout$
.pipe(takeUntilDestroyed())
@@ -295,10 +288,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
// Save off the OrgSsoIdentifier for use in the TDE flows
// - TDE login decryption options component
// - Browser SSO on extension open
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
this.orgIdentifier,
this.activeUserId,
);
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId);
this.loginEmailService.clearValues();
// note: this flow affects both TDE & standard users

View File

@@ -1,389 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs";
import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators";
import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import {
GeneratorType,
DefaultPasswordBoundaries as DefaultBoundaries,
} from "@bitwarden/generator-core";
import {
PasswordGenerationServiceAbstraction,
UsernameGenerationServiceAbstraction,
UsernameGeneratorOptions,
PasswordGeneratorOptions,
} from "@bitwarden/generator-legacy";
export class EmailForwarderOptions {
name: string;
value: string;
validForSelfHosted: boolean;
}
@Directive()
export class GeneratorComponent implements OnInit, OnDestroy {
@Input() comingFromAddEdit = false;
@Input() type: GeneratorType | "";
@Output() onSelected = new EventEmitter<string>();
usernameGeneratingPromise: Promise<string>;
typeOptions: any[];
usernameTypeOptions: any[];
subaddressOptions: any[];
catchallOptions: any[];
forwardOptions: EmailForwarderOptions[];
usernameOptions: UsernameGeneratorOptions = { website: null };
passwordOptions: PasswordGeneratorOptions = {};
username = "-";
password = "-";
showOptions = false;
avoidAmbiguous = false;
enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
usernameWebsite: string = null;
get passTypeOptions() {
return this._passTypeOptions.filter((o) => !o.disabled);
}
private _passTypeOptions: { name: string; value: GeneratorType; disabled: boolean }[];
private destroy$ = new Subject<void>();
private isInitialized$ = new BehaviorSubject(false);
// update screen reader minimum password length with 500ms debounce
// so that the user isn't flooded with status updates
private _passwordOptionsMinLengthForReader = new BehaviorSubject<number>(
DefaultBoundaries.length.min,
);
protected passwordOptionsMinLengthForReader$ = this._passwordOptionsMinLengthForReader.pipe(
map((val) => val || DefaultBoundaries.length.min),
debounceTime(500),
);
private _password = new BehaviorSubject<string>("-");
constructor(
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected usernameGenerationService: UsernameGenerationServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected accountService: AccountService,
protected i18nService: I18nService,
protected logService: LogService,
protected route: ActivatedRoute,
protected ngZone: NgZone,
private win: Window,
protected toastService: ToastService,
) {
this.typeOptions = [
{ name: i18nService.t("password"), value: "password" },
{ name: i18nService.t("username"), value: "username" },
];
this._passTypeOptions = [
{ name: i18nService.t("password"), value: "password", disabled: false },
{ name: i18nService.t("passphrase"), value: "passphrase", disabled: false },
];
this.usernameTypeOptions = [
{
name: i18nService.t("plusAddressedEmail"),
value: "subaddress",
desc: i18nService.t("plusAddressedEmailDesc"),
},
{
name: i18nService.t("catchallEmail"),
value: "catchall",
desc: i18nService.t("catchallEmailDesc"),
},
{
name: i18nService.t("forwardedEmail"),
value: "forwarded",
desc: i18nService.t("forwardedEmailDesc"),
},
{ name: i18nService.t("randomWord"), value: "word" },
];
this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
this.forwardOptions = [
{ name: "", value: "", validForSelfHosted: false },
{ name: "addy.io", value: "anonaddy", validForSelfHosted: true },
{ name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
{ name: "Fastmail", value: "fastmail", validForSelfHosted: true },
{ name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
{ name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
{ name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
].sort((a, b) => a.name.localeCompare(b.name));
this._password.pipe(debounceTime(250)).subscribe((password) => {
ngZone.run(() => {
this.password = password;
});
this.passwordGenerationService.addHistory(this.password).catch((e) => {
this.logService.error(e);
});
});
}
cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) {
this.avoidAmbiguous = !this.passwordOptions.ambiguous;
if (!this.type) {
if (navigationType) {
this.type = navigationType;
} else {
this.type = this.passwordOptions.type === "username" ? "username" : "password";
}
}
this.passwordOptions.type =
this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
const overrideType = this.enforcedPasswordPolicyOptions.overridePasswordType ?? "";
const isDisabled = overrideType.length
? (value: string, policyValue: string) => value !== policyValue
: (_value: string, _policyValue: string) => false;
for (const option of this._passTypeOptions) {
option.disabled = isDisabled(option.value, overrideType);
}
if (this.usernameOptions.type == null) {
this.usernameOptions.type = "word";
}
if (
this.usernameOptions.subaddressEmail == null ||
this.usernameOptions.subaddressEmail === ""
) {
this.usernameOptions.subaddressEmail = accountEmail;
}
if (this.usernameWebsite == null) {
this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
} else {
this.usernameOptions.website = this.usernameWebsite;
}
}
async ngOnInit() {
combineLatest([
this.route.queryParams.pipe(first()),
this.accountService.activeAccount$.pipe(first()),
this.passwordGenerationService.getOptions$(),
this.usernameGenerationService.getOptions$(),
])
.pipe(
map(([qParams, account, [passwordOptions, passwordPolicy], usernameOptions]) => ({
navigationType: qParams.type as GeneratorType,
accountEmail: account.email,
passwordOptions,
passwordPolicy,
usernameOptions,
})),
takeUntil(this.destroy$),
)
.subscribe((options) => {
this.passwordOptions = options.passwordOptions;
this.enforcedPasswordPolicyOptions = options.passwordPolicy;
this.usernameOptions = options.usernameOptions;
this.cascadeOptions(options.navigationType, options.accountEmail);
this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
if (this.regenerateWithoutButtonPress()) {
this.regenerate().catch((e) => {
this.logService.error(e);
});
}
this.isInitialized$.next(true);
});
// once initialization is complete, `ngOnInit` should return.
//
// FIXME(#6944): if a sync is in progress, wait to complete until after
// the sync completes.
await firstValueFrom(
this.isInitialized$.pipe(
skipWhile((initialized) => !initialized),
takeUntil(this.destroy$),
),
);
if (this.usernameWebsite !== null) {
const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
this.subaddressOptions.push(websiteOption);
this.catchallOptions.push(websiteOption);
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.isInitialized$.complete();
this._passwordOptionsMinLengthForReader.complete();
}
async typeChanged() {
await this.savePasswordOptions();
}
async regenerate() {
if (this.type === "password") {
await this.regeneratePassword();
} else if (this.type === "username") {
await this.regenerateUsername();
}
}
async sliderChanged() {
// 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.savePasswordOptions();
await this.passwordGenerationService.addHistory(this.password);
}
async onPasswordOptionsMinNumberInput($event: Event) {
// `savePasswordOptions()` replaces the null
this.passwordOptions.number = null;
await this.savePasswordOptions();
// fixes UI desync that occurs when minNumber has a fixed value
// that is reset through normalization
($event.target as HTMLInputElement).value = `${this.passwordOptions.minNumber}`;
}
async setPasswordOptionsNumber($event: boolean) {
this.passwordOptions.number = $event;
// `savePasswordOptions()` replaces the null
this.passwordOptions.minNumber = null;
await this.savePasswordOptions();
}
async onPasswordOptionsMinSpecialInput($event: Event) {
// `savePasswordOptions()` replaces the null
this.passwordOptions.special = null;
await this.savePasswordOptions();
// fixes UI desync that occurs when minSpecial has a fixed value
// that is reset through normalization
($event.target as HTMLInputElement).value = `${this.passwordOptions.minSpecial}`;
}
async setPasswordOptionsSpecial($event: boolean) {
this.passwordOptions.special = $event;
// `savePasswordOptions()` replaces the null
this.passwordOptions.minSpecial = null;
await this.savePasswordOptions();
}
async sliderInput() {
await this.normalizePasswordOptions();
}
async savePasswordOptions() {
// map navigation state into generator type
const restoreType = this.passwordOptions.type;
if (this.type === "username") {
this.passwordOptions.type = this.type;
}
// save options
await this.normalizePasswordOptions();
await this.passwordGenerationService.saveOptions(this.passwordOptions);
// restore the original format
this.passwordOptions.type = restoreType;
}
async saveUsernameOptions() {
await this.usernameGenerationService.saveOptions(this.usernameOptions);
if (this.usernameOptions.type === "forwarded") {
this.username = "-";
}
}
async regeneratePassword() {
this._password.next(
await this.passwordGenerationService.generatePassword(this.passwordOptions),
);
}
regenerateUsername() {
return this.generateUsername();
}
async generateUsername() {
try {
this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername(
this.usernameOptions,
);
this.username = await this.usernameGeneratingPromise;
if (this.username === "" || this.username === null) {
this.username = "-";
}
} catch (e) {
this.logService.error(e);
}
}
copy() {
const password = this.type === "password";
const copyOptions = this.win != null ? { window: this.win } : null;
this.platformUtilsService.copyToClipboard(
password ? this.password : this.username,
copyOptions,
);
this.toastService.showToast({
variant: "info",
title: null,
message: this.i18nService.t(
"valueCopied",
this.i18nService.t(password ? "password" : "username"),
),
});
}
select() {
this.onSelected.emit(this.type === "password" ? this.password : this.username);
}
toggleOptions() {
this.showOptions = !this.showOptions;
}
regenerateWithoutButtonPress() {
return this.type !== "username" || this.usernameOptions.type !== "forwarded";
}
private async normalizePasswordOptions() {
// Application level normalize options dependent on class variables
this.passwordOptions.ambiguous = !this.avoidAmbiguous;
if (
!this.passwordOptions.uppercase &&
!this.passwordOptions.lowercase &&
!this.passwordOptions.number &&
!this.passwordOptions.special
) {
this.passwordOptions.lowercase = true;
if (this.win != null) {
const lowercase = this.win.document.querySelector("#lowercase") as HTMLInputElement;
if (lowercase) {
lowercase.checked = true;
}
}
}
await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
this.passwordOptions,
);
}
}

View File

@@ -1,40 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { GeneratedPasswordHistory } from "@bitwarden/generator-history";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@Directive()
export class PasswordGeneratorHistoryComponent implements OnInit {
history: GeneratedPasswordHistory[] = [];
constructor(
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService,
private win: Window,
protected toastService: ToastService,
) {}
async ngOnInit() {
this.history = await this.passwordGenerationService.getHistory();
}
clear = async () => {
this.history = await this.passwordGenerationService.clear();
};
copy(password: string) {
const copyOptions = this.win != null ? { window: this.win } : null;
this.platformUtilsService.copyToClipboard(password, copyOptions);
this.toastService.showToast({
variant: "info",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
});
}
}

View File

@@ -1,32 +0,0 @@
import { Type, inject } from "@angular/core";
import { Route, Routes } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { componentRouteSwap } from "../../utils/component-route-swap";
/**
* Helper function to swap between two components based on the GeneratorToolsModernization feature flag.
* @param defaultComponent - The current non-refreshed component to render.
* @param refreshedComponent - The new refreshed component to render.
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
* @param altOptions - The alt route options to apply to the alt component.
*/
export function generatorSwap(
defaultComponent: Type<any>,
refreshedComponent: Type<any>,
options: Route,
altOptions?: Route,
): Routes {
return componentRouteSwap(
defaultComponent,
refreshedComponent,
async () => {
const configService = inject(ConfigService);
return configService.getFeatureFlag(FeatureFlag.GeneratorToolsModernization);
},
options,
altOptions,
);
}

View File

@@ -11,7 +11,7 @@ import {
OnInit,
Output,
} from "@angular/core";
import { filter, firstValueFrom, map, Observable } from "rxjs";
import { filter, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -81,6 +81,7 @@ export class ViewComponent implements OnDestroy, OnInit {
private passwordReprompted = false;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
private destroyed$ = new Subject<void>();
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
@@ -146,12 +147,15 @@ export class ViewComponent implements OnDestroy, OnInit {
const activeUserId = await firstValueFrom(this.activeUserId$);
// Grab individual cipher from `cipherViews$` for the most up-to-date information
this.cipher = await firstValueFrom(
this.cipherService.cipherViews$.pipe(
map((ciphers) => ciphers.find((c) => c.id === this.cipherId)),
this.cipherService.cipherViews$
.pipe(
map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)),
filter((cipher) => !!cipher),
),
);
takeUntil(this.destroyed$),
)
.subscribe((cipher) => {
this.cipher = cipher;
});
this.canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
@@ -524,6 +528,7 @@ export class ViewComponent implements OnDestroy, OnInit {
this.showCardNumber = false;
this.showCardCode = false;
this.passwordReprompted = false;
this.destroyed$.next();
if (this.totpInterval) {
clearInterval(this.totpInterval);
}

View File

@@ -36,7 +36,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid";
import {
AsyncActionsModule,
ButtonModule,
@@ -90,7 +89,6 @@ export class SsoComponent implements OnInit {
protected state: string | undefined;
protected codeChallenge: string | undefined;
protected clientId: SsoClientType | undefined;
protected activeUserId: UserId | undefined;
formPromise: Promise<AuthResult> | undefined;
initiateSsoFormPromise: Promise<SsoPreValidateResponse> | undefined;
@@ -132,8 +130,6 @@ export class SsoComponent implements OnInit {
}
async ngOnInit() {
this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const qParams: QueryParams = await firstValueFrom(this.route.queryParams);
// This if statement will pass on the second portion of the SSO flow
@@ -388,10 +384,10 @@ export class SsoComponent implements OnInit {
// - TDE login decryption options component
// - Browser SSO on extension open
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
orgSsoIdentifier,
this.activeUserId,
);
// Grabbing the active user id right before making the state set to ensure it exists.
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId);
// Users enrolled in admin acct recovery can be forced to set a new password after
// having the admin set a temp password for them (affects TDE & standard users)

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { UserId } from "@bitwarden/common/types/guid";
export abstract class SsoLoginServiceAbstraction {
@@ -13,7 +11,7 @@ export abstract class SsoLoginServiceAbstraction {
* @see https://datatracker.ietf.org/doc/html/rfc7636
* @returns The code verifier used for SSO.
*/
getCodeVerifier: () => Promise<string>;
abstract getCodeVerifier: () => Promise<string>;
/**
* Sets the code verifier used for SSO.
*
@@ -23,7 +21,7 @@ export abstract class SsoLoginServiceAbstraction {
* and verify it matches the one sent in the request for the `authorization_code`.
* @see https://datatracker.ietf.org/doc/html/rfc7636
*/
setCodeVerifier: (codeVerifier: string) => Promise<void>;
abstract setCodeVerifier: (codeVerifier: string) => Promise<void>;
/**
* Gets the value of the SSO state.
*
@@ -33,7 +31,7 @@ export abstract class SsoLoginServiceAbstraction {
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
* @returns The SSO state.
*/
getSsoState: () => Promise<string>;
abstract getSsoState: () => Promise<string>;
/**
* Sets the value of the SSO state.
*
@@ -42,7 +40,7 @@ export abstract class SsoLoginServiceAbstraction {
* returns the `state` in the callback and the client verifies that the value returned matches the value sent.
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
*/
setSsoState: (ssoState: string) => Promise<void>;
abstract setSsoState: (ssoState: string) => Promise<void>;
/**
* Gets the value of the user's organization sso identifier.
*
@@ -50,20 +48,20 @@ export abstract class SsoLoginServiceAbstraction {
* Do not use this value outside of the SSO login flow.
* @returns The user's organization identifier.
*/
getOrganizationSsoIdentifier: () => Promise<string>;
abstract getOrganizationSsoIdentifier: () => Promise<string>;
/**
* Sets the value of the user's organization sso identifier.
*
* This should only be used during the SSO flow to identify the organization that the user is attempting to log in to.
* Do not use this value outside of the SSO login flow.
*/
setOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise<void>;
abstract setOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise<void>;
/**
* Gets the user's email.
* Note: This should only be used during the SSO flow to identify the user that is attempting to log in.
* @returns The user's email.
*/
getSsoEmail: () => Promise<string>;
abstract getSsoEmail: () => Promise<string>;
/**
* Sets the user's email.
* Note: This should only be used during the SSO flow to identify the user that is attempting to log in.
@@ -71,20 +69,20 @@ export abstract class SsoLoginServiceAbstraction {
* @returns A promise that resolves when the email has been set.
*
*/
setSsoEmail: (email: string) => Promise<void>;
abstract setSsoEmail: (email: string) => Promise<void>;
/**
* Gets the value of the active user's organization sso identifier.
*
* This should only be used post successful SSO login once the user is initialized.
* @param userId The user id for retrieving the org identifier state.
*/
getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise<string>;
abstract getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise<string | null>;
/**
* Sets the value of the active user's organization sso identifier.
*
* This should only be used post successful SSO login once the user is initialized.
*/
setActiveUserOrganizationSsoIdentifier: (
abstract setActiveUserOrganizationSsoIdentifier: (
organizationIdentifier: string,
userId: UserId | undefined,
) => Promise<void>;

View File

@@ -87,7 +87,7 @@ describe("SSOLoginService ", () => {
const orgIdentifier = "test-active-org-identifier";
await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, undefined);
expect(mockLogService.warning).toHaveBeenCalledWith(
expect(mockLogService.error).toHaveBeenCalledWith(
"Tried to set a user organization sso identifier with an undefined user id.",
);
});

View File

@@ -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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -107,7 +105,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
await this.ssoEmailState.update((_) => email);
}
getActiveUserOrganizationSsoIdentifier(userId: UserId): Promise<string> {
getActiveUserOrganizationSsoIdentifier(userId: UserId): Promise<string | null> {
return firstValueFrom(this.userOrgSsoIdentifierState(userId).state$);
}
@@ -116,7 +114,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
userId: UserId | undefined,
): Promise<void> {
if (userId === undefined) {
this.logService.warning(
this.logService.error(
"Tried to set a user organization sso identifier with an undefined user id.",
);
return;

View File

@@ -26,7 +26,6 @@ export enum FeatureFlag {
/* Tools */
ItemShare = "item-share",
GeneratorToolsModernization = "generator-tools-modernization",
CriticalApps = "pm-14466-risk-insights-critical-application",
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
@@ -50,6 +49,7 @@ export enum FeatureFlag {
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner",
NewDeviceVerification = "new-device-verification",
PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal",
}
@@ -88,7 +88,6 @@ export const DefaultFeatureFlagValue = {
/* Tools */
[FeatureFlag.ItemShare]: FALSE,
[FeatureFlag.GeneratorToolsModernization]: FALSE,
[FeatureFlag.CriticalApps]: FALSE,
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
@@ -112,6 +111,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
[FeatureFlag.AccountDeprovisioningBanner]: FALSE,
[FeatureFlag.NewDeviceVerification]: FALSE,
[FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;

View File

@@ -29,9 +29,20 @@ export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition(
web: "disk-local",
},
);
export const AC_BANNERS_DISMISSED_DISK = new StateDefinition("acBannersDismissed", "disk", {
web: "disk-local",
});
export const ACCOUNT_DEPROVISIONING_BANNER_DISK = new StateDefinition(
"showAccountDeprovisioningBanner",
"disk",
{
web: "disk-local",
},
);
export const DELETE_MANAGED_USER_WARNING = new StateDefinition(
"showDeleteManagedUserWarning",
"disk",
{
web: "disk-local",
},
);
// Billing
export const BILLING_DISK = new StateDefinition("billing", "disk");

View File

@@ -68,12 +68,13 @@ import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-una
import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date";
import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3;
export const CURRENT_VERSION = 69;
export const CURRENT_VERSION = 70;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -144,7 +145,8 @@ export function createMigrationBuilder() {
.with(MoveFinalDesktopSettingsMigrator, 65, 66)
.with(RemoveUnassignedItemsBannerDismissed, 66, 67)
.with(MoveLastSyncDate, 67, 68)
.with(MigrateIncorrectFolderKey, 68, CURRENT_VERSION);
.with(MigrateIncorrectFolderKey, 68, 69)
.with(RemoveAcBannersDismissed, 69, CURRENT_VERSION);
}
export async function currentVersion(

View File

@@ -0,0 +1,50 @@
import { runMigrator } from "../migration-helper.spec";
import { IRREVERSIBLE } from "../migrator";
import { RemoveAcBannersDismissed } from "./70-remove-ac-banner-dismissed";
describe("RemoveAcBannersDismissed", () => {
const sut = new RemoveAcBannersDismissed(69, 70);
describe("migrate", () => {
it("deletes ac banner from all users", async () => {
const output = await runMigrator(sut, {
global_account_accounts: {
user1: {
email: "user1@email.com",
name: "User 1",
emailVerified: true,
},
user2: {
email: "user2@email.com",
name: "User 2",
emailVerified: true,
},
},
user_user1_showProviderClientVaultPrivacyBanner_acBannersDismissed: true,
user_user2_showProviderClientVaultPrivacyBanner_acBannersDismissed: true,
});
expect(output).toEqual({
global_account_accounts: {
user1: {
email: "user1@email.com",
name: "User 1",
emailVerified: true,
},
user2: {
email: "user2@email.com",
name: "User 2",
emailVerified: true,
},
},
});
});
});
describe("rollback", () => {
it("is irreversible", async () => {
await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE);
});
});
});

View File

@@ -0,0 +1,23 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { IRREVERSIBLE, Migrator } from "../migrator";
export const SHOW_BANNER_KEY: KeyDefinitionLike = {
key: "acBannersDismissed",
stateDefinition: { name: "showProviderClientVaultPrivacyBanner" },
};
export class RemoveAcBannersDismissed extends Migrator<69, 70> {
async migrate(helper: MigrationHelper): Promise<void> {
await Promise.all(
(await helper.getAccounts()).map(async ({ userId }) => {
if (helper.getFromUser(userId, SHOW_BANNER_KEY) != null) {
await helper.removeFromUser(userId, SHOW_BANNER_KEY);
}
}),
);
}
async rollback(helper: MigrationHelper): Promise<void> {
throw IRREVERSIBLE;
}
}

View File

@@ -63,7 +63,8 @@ export class DialogComponent {
@Input() loading = false;
@HostBinding("class") get classes() {
return ["tw-flex", "tw-flex-col", "tw-max-h-screen", "tw-w-screen", "tw-p-4"].concat(
// `tw-max-h-[90vh]` is needed to prevent dialogs from overlapping the desktop header
return ["tw-flex", "tw-flex-col", "tw-w-screen", "tw-p-4", "tw-max-h-[90vh]"].concat(
this.width,
);
}

View File

@@ -94,6 +94,8 @@
<p bitTypography="body1">
{{ "seeDetailedInstructions" | i18n }}
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://bitwarden.com/help/import-from-lastpass/"
@@ -169,13 +171,21 @@
The process is exactly the same as importing from Google Chrome.
</span>
See detailed instructions on our help site at
<a target="_blank" rel="noreferrer" href="https://bitwarden.com/help/import-from-chrome/">
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://bitwarden.com/help/import-from-chrome/"
>
https://bitwarden.com/help/import-from-chrome/</a
>
</ng-container>
<ng-container *ngIf="format === 'firefoxcsv'">
See detailed instructions on our help site at
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://bitwarden.com/help/import-from-firefox/"
@@ -185,7 +195,13 @@
</ng-container>
<ng-container *ngIf="format === 'safaricsv'">
See detailed instructions on our help site at
<a target="_blank" rel="noreferrer" href="https://bitwarden.com/help/import-from-safari/">
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://bitwarden.com/help/import-from-safari/"
>
https://bitwarden.com/help/import-from-safari/</a
>.
</ng-container>
@@ -199,6 +215,8 @@
>
See detailed instructions on our help site at
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://bitwarden.com/help/import-from-1password/"
@@ -289,7 +307,12 @@
</ng-container>
<ng-container *ngIf="format === 'gnomejson'">
Make sure you have python-keyring and python-gnomekeyring installed. Save the
<a target="_blank" rel="noreferrer" href="https://bit.ly/2GpOMTg"
<a
bitLink
linkType="primary"
target="_blank"
rel="noreferrer"
href="https://github.com/spookylukey/gnome-keyring-import-export"
>GNOME Keyring Import/Export</a
>
python script to your desktop as <code>pw_helper.py</code>. Open terminal and run

View File

@@ -16,7 +16,7 @@
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["../importer/src"],
"@bitwarden/importer/ui": ["../importer/src/components"],
"@bitwarden/importer-ui": ["../importer/src/components"],
"@bitwarden/key-management": ["../key-management/src"],
"@bitwarden/key-management-ui": ["../key-management-ui/src/index.ts"],
"@bitwarden/node/*": ["../node/src/*"],

View File

@@ -14,11 +14,5 @@
>
{{ "clearHistory" | i18n }}
</button>
<!-- FIXME: Remove the close button once the dialog doesn't overlap electron's
drag area.
-->
<button bitButton type="submit" buttonType="secondary" (click)="close()">
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -1,6 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@@ -35,7 +34,6 @@ export class CredentialGeneratorHistoryDialogComponent {
private accountService: AccountService,
private history: GeneratorHistoryService,
private dialogService: DialogService,
private dialogRef: DialogRef,
) {
this.accountService.activeAccount$
.pipe(
@@ -54,11 +52,6 @@ export class CredentialGeneratorHistoryDialogComponent {
.subscribe(this.hasHistory$);
}
/** closes the dialog */
protected close() {
this.dialogRef.close();
}
/** Launches clear history flow */
protected async clear() {
const confirmed = await this.dialogService.openSimpleDialog({

View File

@@ -127,8 +127,9 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
this.destroyed$ = inject(DestroyRef);
this.cipherFormContainer.registerChildForm("customFields", this.customFieldsForm);
this.customFieldsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((values) => {
this.updateCipher(values.fields);
this.customFieldsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe(() => {
// getRawValue ensures disabled fields are included
this.updateCipher(this.fields.getRawValue());
});
}
@@ -151,7 +152,7 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
const prefillCipher = this.cipherFormContainer.getInitialCipherView();
// When available, populate the form with the existing fields
prefillCipher.fields?.forEach((field) => {
prefillCipher?.fields?.forEach((field) => {
let value: string | boolean = field.value;
if (field.type === FieldType.Boolean) {

View File

@@ -158,7 +158,7 @@ export class ItemDetailsSectionComponent implements OnInit {
get allowOwnershipChange() {
// Do not allow ownership change in edit mode and the cipher is owned by an organization
if (this.config.mode === "edit" && this.originalCipherView.organizationId != null) {
if (this.config.mode === "edit" && this.originalCipherView?.organizationId != null) {
return false;
}

97
package-lock.json generated
View File

@@ -36,12 +36,12 @@
"argon2-browser": "1.18.0",
"big-integer": "1.6.52",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.43.0",
"braintree-web-drop-in": "1.44.0",
"buffer": "6.0.3",
"bufferutil": "4.0.9",
"chalk": "4.1.2",
"commander": "11.1.0",
"core-js": "3.39.0",
"core-js": "3.40.0",
"form-data": "4.0.1",
"https-proxy-agent": "7.0.5",
"inquirer": "8.2.6",
@@ -156,7 +156,7 @@
"lint-staged": "15.4.1",
"mini-css-extract-plugin": "2.9.2",
"node-ipc": "9.2.1",
"postcss": "8.4.49",
"postcss": "8.5.1",
"postcss-loader": "8.1.1",
"prettier": "3.4.2",
"prettier-plugin-tailwindcss": "0.6.10",
@@ -169,7 +169,7 @@
"style-loader": "4.0.0",
"tailwindcss": "3.4.17",
"ts-jest": "29.2.2",
"ts-loader": "9.5.1",
"ts-loader": "9.5.2",
"tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "2.19.0",
"typescript": "5.4.2",
@@ -190,7 +190,7 @@
},
"apps/browser": {
"name": "@bitwarden/browser",
"version": "2025.1.4"
"version": "2025.1.3"
},
"apps/cli": {
"name": "@bitwarden/cli",
@@ -230,7 +230,7 @@
},
"apps/desktop": {
"name": "@bitwarden/desktop",
"version": "2025.1.4",
"version": "2025.2.0",
"hasInstallScript": true,
"license": "GPL-3.0"
},
@@ -7921,6 +7921,36 @@
"license": "MIT",
"optional": true
},
"node_modules/@paypal/accelerated-checkout-loader": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@paypal/accelerated-checkout-loader/-/accelerated-checkout-loader-1.1.0.tgz",
"integrity": "sha512-S2KkIpq15VnxYyI0tycvfYiNsqdsg2a92El2huYUVLsWnBbubl8toYK8khaP5nnxZ0MGl9mEB9Y9axmfOw2Yvg==",
"license": "MIT",
"dependencies": {
"@braintree/asset-loader": "2.0.0",
"envify": "^4.1.0",
"typescript": "^4.6.4"
}
},
"node_modules/@paypal/accelerated-checkout-loader/node_modules/@braintree/asset-loader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@braintree/asset-loader/-/asset-loader-2.0.0.tgz",
"integrity": "sha512-7Zs3/g3lPTfkdtWr7cKh3tk1pDruXR++TXwGKkx7BPuTjjLNFul2JSfI+ScHzNU4u/gZNPNQagsSTlYxIhBgMA==",
"license": "MIT"
},
"node_modules/@paypal/accelerated-checkout-loader/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
@@ -12870,9 +12900,9 @@
}
},
"node_modules/braintree-web": {
"version": "3.103.0",
"resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.103.0.tgz",
"integrity": "sha512-gwmC5LSUP5VUC2HmUyaFnEyLjRRAo1iKKHS5eD9KIAZHB7cAQ2il1V1q2f5zdz7+7EE11eSHXznj6n/Qm6jp6w==",
"version": "3.113.0",
"resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.113.0.tgz",
"integrity": "sha512-qykYxZyld4X1tRNgXZQ3ZGzmhDGTBTRQ6Q24KaG9PuYqo+P2TVDEDOVC6tRbkx2RUIdXLv2M6WpkG7oLqEia9Q==",
"license": "MIT",
"dependencies": {
"@braintree/asset-loader": "2.0.1",
@@ -12883,6 +12913,7 @@
"@braintree/sanitize-url": "7.0.4",
"@braintree/uuid": "1.0.0",
"@braintree/wrap-promise": "2.1.0",
"@paypal/accelerated-checkout-loader": "1.1.0",
"card-validator": "10.0.0",
"credit-card-type": "10.0.1",
"framebus": "6.0.0",
@@ -12892,9 +12923,9 @@
}
},
"node_modules/braintree-web-drop-in": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.43.0.tgz",
"integrity": "sha512-lkUpQfYXR0CGtR7mPRR17AnZoYkHjhycxVnMGIPcWT6JPagEZcG/7tYyy34iWjYZeGa2wsquLBDV2Xeita962Q==",
"version": "1.44.0",
"resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.44.0.tgz",
"integrity": "sha512-maOq9SwiXztIzixJhOras7K44x4UIqqnkyQMYAJqxQ8WkADv9AkflCu2j3IeVYCus/Th9gWWFHcBugn3C4sZGw==",
"license": "MIT",
"dependencies": {
"@braintree/asset-loader": "2.0.1",
@@ -12902,7 +12933,7 @@
"@braintree/event-emitter": "0.4.1",
"@braintree/uuid": "1.0.0",
"@braintree/wrap-promise": "2.1.0",
"braintree-web": "3.103.0"
"braintree-web": "3.113.0"
}
},
"node_modules/browser-assert": {
@@ -14451,9 +14482,9 @@
}
},
"node_modules/core-js": {
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
@@ -15996,6 +16027,19 @@
"node": ">=6"
}
},
"node_modules/envify": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/envify/-/envify-4.1.0.tgz",
"integrity": "sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw==",
"license": "MIT",
"dependencies": {
"esprima": "^4.0.0",
"through": "~2.3.4"
},
"bin": {
"envify": "bin/envify"
}
},
"node_modules/envinfo": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
@@ -16853,7 +16897,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
@@ -24966,9 +25009,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@@ -27088,9 +27131,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
"dev": true,
"funding": [
{
@@ -27108,7 +27151,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -31181,9 +31224,9 @@
}
},
"node_modules/ts-loader": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz",
"integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==",
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz",
"integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -117,7 +117,7 @@
"lint-staged": "15.4.1",
"mini-css-extract-plugin": "2.9.2",
"node-ipc": "9.2.1",
"postcss": "8.4.49",
"postcss": "8.5.1",
"postcss-loader": "8.1.1",
"prettier": "3.4.2",
"prettier-plugin-tailwindcss": "0.6.10",
@@ -130,7 +130,7 @@
"style-loader": "4.0.0",
"tailwindcss": "3.4.17",
"ts-jest": "29.2.2",
"ts-loader": "9.5.1",
"ts-loader": "9.5.2",
"tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "2.19.0",
"typescript": "5.4.2",
@@ -166,12 +166,12 @@
"argon2-browser": "1.18.0",
"big-integer": "1.6.52",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.43.0",
"braintree-web-drop-in": "1.44.0",
"buffer": "6.0.3",
"bufferutil": "4.0.9",
"chalk": "4.1.2",
"commander": "11.1.0",
"core-js": "3.39.0",
"core-js": "3.40.0",
"form-data": "4.0.1",
"https-proxy-agent": "7.0.5",
"inquirer": "8.2.6",

View File

@@ -29,7 +29,7 @@
"@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["./libs/importer/src"],
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
"@bitwarden/importer-ui": ["./libs/importer/src/components"],
"@bitwarden/key-management": ["./libs/key-management/src"],
"@bitwarden/key-management-ui": ["./libs/key-management-ui/src/index,ts"],
"@bitwarden/node/*": ["./libs/node/src/*"],

View File

@@ -30,7 +30,7 @@
"@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["./libs/importer/src"],
"@bitwarden/importer/ui": ["./libs/importer/src/components"],
"@bitwarden/importer-ui": ["./libs/importer/src/components"],
"@bitwarden/key-management": ["./libs/key-management/src"],
"@bitwarden/key-management-ui": ["./libs/key-management-ui/src"],
"@bitwarden/node/*": ["./libs/node/src/*"],