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

Merge branch 'main' into nathan/fix-autofill-signing

# Conflicts:
#	apps/desktop/resources/entitlements.mas.plist
This commit is contained in:
Nathan Ansel
2025-02-05 16:59:32 -06:00
358 changed files with 2631 additions and 3528 deletions

13
.github/CODEOWNERS vendored
View File

@@ -29,12 +29,12 @@ libs/tools @bitwarden/team-tools-dev
bitwarden_license/bit-web/src/app/tools @bitwarden/team-tools-dev
bitwarden_license/bit-common/src/tools @bitwarden/team-tools-dev
## Localization/Crowdin (Tools team)
apps/browser/src/_locales @bitwarden/team-tools-dev
apps/browser/store/locales @bitwarden/team-tools-dev
apps/cli/src/locales @bitwarden/team-tools-dev
apps/desktop/src/locales @bitwarden/team-tools-dev
apps/web/src/locales @bitwarden/team-tools-dev
## Localization/Crowdin (Platform and Tools team)
apps/browser/src/_locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev
apps/browser/store/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev
apps/cli/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev
apps/desktop/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev
apps/web/src/locales @bitwarden/team-tools-dev @bitwarden/team-platform-dev
## Vault team files ##
apps/browser/src/vault @bitwarden/team-vault-dev
@@ -131,6 +131,7 @@ apps/web/src/app/key-management @bitwarden/team-key-management-dev
apps/browser/src/key-management @bitwarden/team-key-management-dev
apps/cli/src/key-management @bitwarden/team-key-management-dev
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

64
.github/codecov.yml vendored
View File

@@ -1,2 +1,66 @@
ignore:
- "**/*.spec.ts" # Tests
component_management:
default_rules:
statuses:
- type: project
target: auto
individual_components:
- component_id: key-management-biometrics
name: Key Management - Biometrics
paths:
- apps/browser/src/key-management/biometrics/**
- apps/cli/src/key-management/cli-biometrics-service.ts
- apps/desktop/destkop_native/core/src/biometric/**
- apps/desktop/src/key-management/biometrics/**
- apps/desktop/src/services/biometric-message-handler.service.ts
- apps/web/src/app/key-management/web-biometric.service.ts
- libs/key-management/src/biometrics/**
- component_id: key-management-lock
name: Key Management - Lock
paths:
- apps/browser/src/key-management/lock/**
- apps/desktop/src/key-management/lock/**
- apps/web/src/app/key-management/lock/**
- libs/key-management-ui/src/lock/**
- component_id: key-management-ipc
name: Key Management - IPC
paths:
- apps/browser/src/background/nativeMessaging.background.ts
- apps/desktop/src/services/native-messaging.service.ts
- component_id: key-management-key-rotation
name: Key Management - Key Rotation
paths:
- apps/web/src/app/key-management/key-rotation/**
- apps/web/src/app/key-management/migrate-encryption/**
- libs/key-management/src/user-asymmetric-key-regeneration/**
- component_id: key-management-process-reload
name: Key Management - Process Reload
paths:
- apps/web/src/app/key-management/services/web-process-reload.service.ts
- libs/common/src/key-management/services/default-process-reload.service.ts
- component_id: key-management-keys
name: Key Management - Keys
paths:
- libs/key-management/src/kdf-config.service.ts
- libs/key-management/src/key.service.ts
- libs/common/src/key-management/master-password/**
- component_id: key-management-crypto
name: Key Management - Crypto
paths:
- libs/common/src/key-management/crypto/**
- component_id: key-management
name: Key Management
paths:
- 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/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/**
- libs/common/src/key-management/**
- libs/key-management/**
- libs/key-management-ui/**

View File

@@ -103,15 +103,15 @@ jobs:
matrix:
os:
- ubuntu-22.04
- macos-latest
- windows-latest
- macos-14
- windows-2022
steps:
- name: Check Rust version
run: rustup --version
- name: Install gnome-keyring
if: ${{ matrix.os=='ubuntu-latest' }}
if: ${{ matrix.os=='ubuntu-22.04' }}
run: |
sudo apt-get update
sudo apt-get install -y gnome-keyring dbus-x11
@@ -124,7 +124,7 @@ jobs:
run: cargo build
- name: Test Ubuntu
if: ${{ matrix.os=='ubuntu-latest' }}
if: ${{ matrix.os=='ubuntu-22.04' }}
working-directory: ./apps/desktop/desktop_native
run: |
eval "$(dbus-launch --sh-syntax)"
@@ -135,11 +135,41 @@ jobs:
cargo test -- --test-threads=1
- name: Test macOS
if: ${{ matrix.os=='macos-latest' }}
if: ${{ matrix.os=='macos-14' }}
working-directory: ./apps/desktop/desktop_native
run: cargo test -- --test-threads=1
- name: Test Windows
if: ${{ matrix.os=='windows-latest'}}
if: ${{ matrix.os=='windows-2022'}}
working-directory: ./apps/desktop/desktop_native/core
run: cargo test -- --test-threads=1
rust-coverage:
name: Rust Coverage
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install rust
uses: dtolnay/rust-toolchain@a54c7afa936fefeb4456b2dd8068152669aa8203 # stable
with:
toolchain: stable
components: llvm-tools
- name: Cache cargo registry
uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
with:
workspaces: "apps/desktop/desktop_native -> target"
- name: Install cargo-llvm-cov
run: cargo install cargo-llvm-cov --version 0.6.16
- name: Generate coverage
working-directory: ./apps/desktop/desktop_native
run: cargo llvm-cov --all-features --lcov --output-path lcov.info --workspace --no-cfg-coverage
- name: Upload to codecov.io
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
with:
files: ./apps/desktop/desktop_native/lcov.info

View File

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

View File

@@ -4155,15 +4155,6 @@
"itemName": {
"message": "Item name"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"organizationIsDeactivated": {
"message": "Organization is deactivated"
},
@@ -4896,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},

View File

@@ -424,6 +424,7 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
}
await this.setupSubmitListenerOnFormlessField(formFieldElement);
return;
}
/**
@@ -439,15 +440,16 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
this.formElements.add(formElement);
formElement.addEventListener(EVENTS.SUBMIT, this.handleFormFieldSubmitEvent);
const closesSubmitButton = await this.findSubmitButton(formElement);
const closestSubmitButton = await this.findSubmitButton(formElement);
// If we cannot find a submit button within the form, check for a submit button outside the form.
if (!closesSubmitButton) {
if (!closestSubmitButton) {
await this.setupSubmitListenerOnFormlessField(formFieldElement);
return;
}
this.setupSubmitButtonEventListeners(closesSubmitButton);
this.setupSubmitButtonEventListeners(closestSubmitButton);
return;
}
}
@@ -459,9 +461,11 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
*/
private async setupSubmitListenerOnFormlessField(formFieldElement: FillableFormFieldElement) {
if (formFieldElement && !this.fieldsWithSubmitElements.has(formFieldElement)) {
const closesSubmitButton = await this.findClosestFormlessSubmitButton(formFieldElement);
this.setupSubmitButtonEventListeners(closesSubmitButton);
const closestSubmitButton = await this.findClosestFormlessSubmitButton(formFieldElement);
this.setupSubmitButtonEventListeners(closestSubmitButton);
}
return;
}
/**

View File

@@ -747,7 +747,7 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info");
jest.spyOn(cipherService, "updateLastUsedDate");
jest.spyOn(chrome.runtime, "sendMessage");
jest.spyOn(eventCollectionService, "collect");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
@@ -769,7 +769,10 @@ describe("AutofillService", () => {
);
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalled();
expect(logService.info).not.toHaveBeenCalled();
expect(cipherService.updateLastUsedDate).toHaveBeenCalledWith(autofillOptions.cipher.id);
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
cipherId: autofillOptions.cipher.id,
command: "updateLastUsedDate",
});
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
autofillOptions.pageDetails[0].tab.id,
{
@@ -890,11 +893,11 @@ describe("AutofillService", () => {
it("skips updating the cipher's last used date if the passed options indicate that we should skip the last used cipher", async () => {
autofillOptions.skipLastUsed = true;
jest.spyOn(cipherService, "updateLastUsedDate");
jest.spyOn(chrome.runtime, "sendMessage");
await autofillService.doAutoFill(autofillOptions);
expect(cipherService.updateLastUsedDate).not.toHaveBeenCalled();
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
});
it("returns early if the fillScript cannot be generated", async () => {

View File

@@ -463,8 +463,13 @@ export default class AutofillService implements AutofillServiceInterface {
fillScript.properties.delay_between_operations = 20;
didAutofill = true;
if (!options.skipLastUsed) {
await this.cipherService.updateLastUsedDate(options.cipher.id);
// In order to prevent a UI update send message to background script to update last used date
await chrome.runtime.sendMessage({
command: "updateLastUsedDate",
cipherId: options.cipher.id,
});
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.

View File

@@ -317,6 +317,7 @@ describe("CollectAutofillContentService", () => {
__form__0: {
opid: "__form__0",
htmlAction: formAction,
htmlClass: null,
htmlName: formName,
htmlID: formId,
htmlMethod: formMethod,
@@ -544,6 +545,7 @@ describe("CollectAutofillContentService", () => {
__form__0: {
opid: "__form__0",
htmlAction: formAction1,
htmlClass: null,
htmlName: formName1,
htmlID: formId1,
htmlMethod: formMethod1,
@@ -551,6 +553,7 @@ describe("CollectAutofillContentService", () => {
__form__1: {
opid: "__form__1",
htmlAction: formAction2,
htmlClass: null,
htmlName: formName2,
htmlID: formId2,
htmlMethod: formMethod2,

View File

@@ -228,6 +228,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
opid: formElement.opid,
htmlAction: this.getFormActionAttribute(formElement),
htmlName: this.getPropertyOrAttribute(formElement, "name"),
htmlClass: this.getPropertyOrAttribute(formElement, "class"),
htmlID: this.getPropertyOrAttribute(formElement, "id"),
htmlMethod: this.getPropertyOrAttribute(formElement, "method"),
});
@@ -982,8 +983,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
const queueLength = this.mutationsQueue.length;
if (!this.domQueryService.pageContainsShadowDomElements()) {
// Checking if a page contains shadowDOM elements is a heavy operation and doesn't have to be done immediately, so we can call this within an idle moment on the event loop.
requestIdleCallbackPolyfill(this.checkPageContainsShadowDom, { timeout: 500 });
this.checkPageContainsShadowDom();
}
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {

View File

@@ -56,6 +56,7 @@ export class InlineMenuFieldQualificationService
"neuer benutzer",
"neues passwort",
"neue e-mail",
"pwdcheck",
];
private updatePasswordFieldKeywords = [
"update password",

View File

@@ -75,12 +75,16 @@ import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/bill
import { ClientType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
import { FallbackBulkEncryptService } from "@bitwarden/common/key-management/crypto/services/fallback-bulk-encrypt.service";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
@@ -123,10 +127,6 @@ import { ConfigApiService } from "@bitwarden/common/platform/services/config/con
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service";
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/services/fido2/fido2-active-request-manager";
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service";
@@ -208,7 +208,7 @@ import {
ImportApiServiceAbstraction,
ImportService,
ImportServiceAbstraction,
} from "@bitwarden/importer/core";
} from "@bitwarden/importer-core";
import {
BiometricsService,
BiometricStateService,
@@ -809,7 +809,7 @@ export default class MainBackground {
this.apiService,
);
this.ssoLoginService = new SsoLoginService(this.stateProvider);
this.ssoLoginService = new SsoLoginService(this.stateProvider, this.logService);
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
@@ -1142,6 +1142,7 @@ export default class MainBackground {
this.accountService,
lockService,
this.billingAccountProfileStateService,
this.cipherService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.keyService,

View File

@@ -4,9 +4,9 @@ import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@@ -16,6 +16,7 @@ import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/m
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { BiometricsCommands } from "@bitwarden/key-management";
@@ -53,6 +54,7 @@ export default class RuntimeBackground {
private accountService: AccountService,
private readonly lockService: LockService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private cipherService: CipherService,
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
@@ -200,6 +202,9 @@ export default class RuntimeBackground {
case BiometricsCommands.GetBiometricsStatusForUser: {
return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId);
}
case "updateLastUsedDate": {
return await this.cipherService.updateLastUsedDate(msg.cipherId);
}
case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": {
return await this.configService.getFeatureFlag(
FeatureFlag.UseTreeWalkerApiForPageDetailsCollection,

View File

@@ -1,6 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { Subject } from "rxjs";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {

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

@@ -55,13 +55,13 @@ import {
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ClientType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import {
AnimationControlService,
DefaultAnimationControlService,
} from "@bitwarden/common/platform/abstractions/animation-control.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -7,4 +7,5 @@
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : null"
showAutofillButton
[primaryActionAutofill]="clickItemsToAutofillVaultView"
[groupByType]="groupByType()"
></app-vault-list-items-container>

View File

@@ -1,5 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { combineLatest, firstValueFrom, map, Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -48,6 +49,10 @@ export class AutofillVaultListItemsComponent implements OnInit {
clickItemsToAutofillVaultView = false;
protected groupByType = toSignal(
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)),
);
/**
* Observable that determines whether the empty autofill tip should be shown.
* The tip is shown when there are no login ciphers to autofill, no filter is applied, and autofill is allowed in

View File

@@ -27,7 +27,7 @@
<button type="button" bitMenuItem (click)="toggleFavorite()">
{{ favoriteText | i18n }}
</button>
<ng-container *ngIf="canEdit">
<ng-container *ngIf="canEdit && canViewPassword">
<a bitMenuItem (click)="clone()" *ngIf="canClone$ | async">
{{ "clone" | i18n }}
</a>

View File

@@ -97,6 +97,9 @@ export class ItemMoreOptionsComponent implements OnInit {
return this.cipher.edit;
}
get canViewPassword() {
return this.cipher.viewPassword;
}
/**
* Determines if the cipher can be autofilled.
*/

View File

@@ -11,11 +11,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component";
export interface NewItemInitialValues {
folderId?: string;
@@ -72,6 +72,6 @@ export class NewItemDropdownV2Component implements OnInit {
}
openFolderDialog() {
this.dialogService.open(AddEditFolderDialogComponent);
AddEditFolderDialogComponent.open(this.dialogService);
}
}

View File

@@ -6,11 +6,12 @@ import { combineLatest, map, take } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DisclosureTriggerForDirective, IconButtonModule } from "@bitwarden/components";
import {
DisclosureComponent,
DisclosureTriggerForDirective,
IconButtonModule,
} from "@bitwarden/components";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { DisclosureComponent } from "../../../../../../../../libs/components/src/disclosure/disclosure.component";
import { runInsideAngular } from "../../../../../platform/browser/run-inside-angular.operator";
import { VaultPopupListFiltersService } from "../../../../../vault/popup/services/vault-popup-list-filters.service";
import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component";

View File

@@ -1,9 +1,13 @@
<bit-section *ngIf="ciphers?.length > 0 || description" [disableMargin]="disableSectionMargin">
<bit-section
*ngIf="cipherGroups$().length > 0 || description"
[disableMargin]="disableSectionMargin"
>
<ng-container *ngIf="collapsibleKey">
<button
class="tw-group/vault-section-header hover:tw-bg-secondary-100 tw-pl-1 tw-w-full tw-border-x-0 tw-border-t-0 tw-border-b tw-border-solid focus-visible:tw-outline-none focus-visible:tw-rounded-md focus-visible:tw-ring-inset focus-visible:tw-ring-2 focus-visible:tw-ring-primary-600"
class="tw-group/vault-section-header hover:tw-bg-primary-100 tw-rounded-md tw-pl-1 tw-w-full tw-border-x-0 tw-border-t-0 tw-border-b tw-border-solid focus-visible:tw-outline-none focus-visible:tw-ring-inset focus-visible:tw-ring-2 focus-visible:tw-ring-primary-600"
[ngClass]="{
'tw-border-b-secondary-300': !sectionOpenState(),
'tw-border-b-secondary-300 tw-rounded-b-none [&:is(:hover,:focus-visible)]:tw-border-b-transparent [&:is(:hover,:focus-visible)]:tw-rounded-b-md':
!sectionOpenState(),
'tw-border-b-transparent': sectionOpenState(),
}"
type="button"
@@ -17,6 +21,7 @@
<ng-container *ngTemplateOutlet="itemGroup"></ng-container>
</bit-disclosure>
</ng-container>
<ng-container *ngIf="!collapsibleKey">
<div class="tw-pl-1">
<ng-container *ngTemplateOutlet="sectionHeader"></ng-container>
@@ -27,7 +32,7 @@
</bit-section>
<ng-template #sectionHeader>
<bit-section-header class="tw-p-0.5 -tw-mt-0.5 -tw-mx-0.5">
<bit-section-header class="tw-p-0.5 -tw-mx-0.5">
<h2 bitTypography="h6">
{{ title }}
</h2>
@@ -47,11 +52,11 @@
'tw-hidden': collapsibleKey && !sectionOpenState(),
}"
>
{{ ciphers.length }}
{{ ciphers().length }}
</span>
<span class="tw-pr-1" *ngIf="collapsibleKey">
<i
class="bwi"
class="bwi tw-text-main"
[ngClass]="{
'bwi-angle-down tw-inline-block': !sectionOpenState(),
'bwi-angle-up tw-hidden group-hover/vault-section-header:tw-inline-block group-focus-visible/vault-section-header:tw-inline-block':
@@ -72,69 +77,78 @@
<ng-template #itemGroup>
<bit-item-group>
<cdk-virtual-scroll-viewport
[itemSize]="itemHeight$ | async"
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
>
<bit-item *cdkVirtualFor="let cipher of ciphers">
<button
bit-item-content
type="button"
(click)="primaryActionOnSelect(cipher)"
(dblclick)="launchCipher(cipher)"
[appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
class="{{ itemHeightClass }}"
>
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span>
<i
*ngIf="cipher.organizationId"
slot="default-trailing"
appOrgIcon
[tierType]="cipher.organization.productTierType"
[size]="'small'"
[appA11yTitle]="orgIconTooltip(cipher)"
></i>
<i
*ngIf="cipher.hasAttachments"
class="bwi bwi-paperclip bwi-sm"
[appA11yTitle]="'attachments' | i18n"
></i>
<span slot="secondary">{{ cipher.subTitle }}</span>
</button>
<ng-container slot="end">
<bit-item-action *ngIf="!(hideAutofillButton$ | async)">
<button
type="button"
bitBadge
variant="primary"
(click)="doAutofill(cipher)"
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
>
{{ "fill" | i18n }}
</button>
</bit-item-action>
<bit-item-action *ngIf="!showAutofillButton && cipher.canLaunch">
<button
type="button"
bitIconButton="bwi-external-link"
size="small"
(click)="launchCipher(cipher)"
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
[title]="'launchWebsiteName' | i18n: cipher.name"
></button>
</bit-item-action>
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options
[cipher]="cipher"
[hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill"
></app-item-more-options>
</ng-container>
</bit-item>
</cdk-virtual-scroll-viewport>
<ng-container *ngFor="let group of cipherGroups$()">
<ng-container *ngIf="group.subHeaderKey">
<h3 class="tw-text-muted tw-text-xs tw-font-semibold tw-pl-1 tw-mb-1 bit-compact:tw-m-0">
{{ group.subHeaderKey | i18n }}
</h3>
</ng-container>
<cdk-virtual-scroll-viewport
[itemSize]="itemHeight$ | async"
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
>
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
<button
bit-item-content
type="button"
(click)="primaryActionOnSelect(cipher)"
(dblclick)="launchCipher(cipher)"
[appA11yTitle]="cipherItemTitleKey | async | i18n: cipher.name"
class="{{ itemHeightClass }}"
>
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span>
<i
*ngIf="cipher.organizationId"
slot="default-trailing"
appOrgIcon
[tierType]="cipher.organization.productTierType"
[size]="'small'"
[appA11yTitle]="orgIconTooltip(cipher)"
></i>
<i
*ngIf="cipher.hasAttachments"
class="bwi bwi-paperclip bwi-sm"
[appA11yTitle]="'attachments' | i18n"
></i>
<span slot="secondary">{{ cipher.subTitle }}</span>
</button>
<ng-container slot="end">
<bit-item-action *ngIf="!(hideAutofillButton$ | async)">
<button
type="button"
bitBadge
variant="primary"
(click)="doAutofill(cipher)"
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
>
{{ "fill" | i18n }}
</button>
</bit-item-action>
<bit-item-action *ngIf="!showAutofillButton && cipher.canLaunch">
<button
type="button"
bitIconButton="bwi-external-link"
size="small"
(click)="launchCipher(cipher)"
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
[title]="'launchWebsiteName' | i18n: cipher.name"
></button>
</bit-item-action>
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options
[cipher]="cipher"
[hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill"
></app-item-more-options>
</ng-container>
</bit-item>
</cdk-virtual-scroll-viewport>
</ng-container>
</bit-item-group>
</ng-template>

View File

@@ -9,11 +9,14 @@ import {
EventEmitter,
inject,
Input,
OnInit,
Output,
Signal,
signal,
ViewChild,
computed,
OnInit,
ChangeDetectionStrategy,
input,
} from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, Observable, map } from "rxjs";
@@ -23,6 +26,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
@@ -73,6 +77,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
selector: "app-vault-list-items-container",
templateUrl: "vault-list-items-container.component.html",
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
private compactModeService = inject(CompactModeService);
@@ -110,11 +115,51 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
*/
private viewCipherTimeout: number | null;
ciphers = input<PopupCipherView[]>([]);
/**
* The list of ciphers to display.
* If true, we will group ciphers by type (Login, Card, Identity)
* within subheadings in a single container, converted to a WritableSignal.
*/
@Input()
ciphers: PopupCipherView[] = [];
groupByType = input<boolean>(false);
/**
* Computed signal for a grouped list of ciphers with an optional header
*/
cipherGroups$ = computed<
{
subHeaderKey?: string | null;
ciphers: PopupCipherView[];
}[]
>(() => {
const groups: { [key: string]: CipherView[] } = {};
this.ciphers().forEach((cipher) => {
let groupKey;
if (this.groupByType()) {
switch (cipher.type) {
case CipherType.Card:
groupKey = "cards";
break;
case CipherType.Identity:
groupKey = "identities";
break;
}
}
if (!groups[groupKey]) {
groups[groupKey] = [];
}
groups[groupKey].push(cipher);
});
return Object.keys(groups).map((key) => ({
subHeaderKey: this.groupByType ? key : "",
ciphers: groups[key],
}));
});
/**
* Title for the vault list item section.

View File

@@ -11,10 +11,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordHistoryViewComponent } from "@bitwarden/vault";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { PasswordHistoryViewComponent } from "../../../../../../../../libs/vault/src/components/password-history-view/password-history-view.component";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";

View File

@@ -69,13 +69,13 @@
<app-autofill-vault-list-items></app-autofill-vault-list-items>
<app-vault-list-items-container
[title]="'favorites' | i18n"
[ciphers]="favoriteCiphers$ | async"
[ciphers]="(favoriteCiphers$ | async) || []"
id="favorites"
collapsibleKey="favorites"
></app-vault-list-items-container>
<app-vault-list-items-container
[title]="'allItems' | i18n"
[ciphers]="remainingCiphers$ | async"
[ciphers]="(remainingCiphers$ | async) || []"
id="allItems"
disableSectionMargin
collapsibleKey="allItems"

View File

@@ -23,6 +23,7 @@ import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -35,14 +36,8 @@ import {
SearchModule,
ToastService,
} from "@bitwarden/components";
import { CopyCipherFieldService } from "@bitwarden/vault";
import { CipherViewComponent, CopyCipherFieldService } from "@bitwarden/vault";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";

View File

@@ -21,14 +21,12 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { ToastService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service";
import {
AutoFillOptions,
AutofillService,
PageDetail,
} from "../../../autofill/services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";

View File

@@ -27,13 +27,11 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view
import { ToastService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service";
import {
AutofillService,
PageDetail,
} from "../../../autofill/services/abstractions/autofill.service";
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { closeViewVaultItemPopout, VaultPopoutType } from "../utils/vault-popout-window";

View File

@@ -17,9 +17,7 @@ import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service";
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { VaultPopupAutofillService } from "./vault-popup-autofill.service";

View File

@@ -214,6 +214,7 @@ export class VaultPopupItemsService {
map(([hasSearchText, filters]) => {
return hasSearchText || Object.values(filters).some((filter) => filter !== null);
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
/**

View File

@@ -14,17 +14,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { BadgeModule, CheckboxModule, Option } from "@bitwarden/components";
import {
BadgeModule,
CardComponent,
CheckboxModule,
FormFieldModule,
Option,
SelectModule,
} from "@bitwarden/components";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { CardComponent } from "../../../../../../libs/components/src/card/card.component";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { SelectModule } from "../../../../../../libs/components/src/select/select.module";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";

View File

@@ -14,10 +14,10 @@ import { UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { DialogService } from "@bitwarden/components";
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { AddEditFolderDialogComponent } from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component";
import { FoldersV2Component } from "./folders-v2.component";
@@ -27,8 +27,8 @@ import { FoldersV2Component } from "./folders-v2.component";
template: `<ng-content></ng-content>`,
})
class MockPopupHeaderComponent {
@Input() pageTitle: string;
@Input() backAction: () => void;
@Input() pageTitle: string = "";
@Input() backAction: () => void = () => {};
}
@Component({
@@ -37,14 +37,15 @@ class MockPopupHeaderComponent {
template: `<ng-content></ng-content>`,
})
class MockPopupFooterComponent {
@Input() pageTitle: string;
@Input() pageTitle: string = "";
}
describe("FoldersV2Component", () => {
let component: FoldersV2Component;
let fixture: ComponentFixture<FoldersV2Component>;
const folderViews$ = new BehaviorSubject<FolderView[]>([]);
const open = jest.fn();
const open = jest.spyOn(AddEditFolderDialogComponent, "open");
const mockDialogService = { open: jest.fn() };
beforeEach(async () => {
open.mockClear();
@@ -68,7 +69,7 @@ describe("FoldersV2Component", () => {
imports: [MockPopupHeaderComponent, MockPopupFooterComponent],
},
})
.overrideProvider(DialogService, { useValue: { open } })
.overrideProvider(DialogService, { useValue: mockDialogService })
.compileComponents();
fixture = TestBed.createComponent(FoldersV2Component);
@@ -101,9 +102,7 @@ describe("FoldersV2Component", () => {
editButton.triggerEventHandler("click");
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, {
data: { editFolderConfig: { folder } },
});
expect(open).toHaveBeenCalledWith(mockDialogService, { editFolderConfig: { folder } });
});
it("opens add dialog for new folder when there are no folders", () => {
@@ -114,6 +113,6 @@ describe("FoldersV2Component", () => {
addButton.triggerEventHandler("click");
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} });
expect(open).toHaveBeenCalledWith(mockDialogService, {});
});
});

View File

@@ -12,25 +12,14 @@ import {
ButtonModule,
DialogService,
IconButtonModule,
ItemModule,
NoItemsModule,
} from "@bitwarden/components";
import { VaultIcons } from "@bitwarden/vault";
import { AddEditFolderDialogComponent, VaultIcons } from "@bitwarden/vault";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { ItemModule } from "../../../../../../libs/components/src/item/item.module";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no-items.module";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import {
AddEditFolderDialogComponent,
AddEditFolderDialogData,
} from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component";
@Component({
standalone: true,
@@ -42,7 +31,6 @@ import {
PopupPageComponent,
PopupHeaderComponent,
ItemModule,
ItemGroupComponent,
NoItemsModule,
IconButtonModule,
ButtonModule,
@@ -78,8 +66,6 @@ export class FoldersV2Component {
// If a folder is provided, the edit variant should be shown
const editFolderConfig = folder ? { folder } : undefined;
this.dialogService.open<unknown, AddEditFolderDialogData>(AddEditFolderDialogComponent, {
data: { editFolderConfig },
});
AddEditFolderDialogComponent.open(this.dialogService, { editFolderConfig });
}
}

View File

@@ -24,7 +24,7 @@
"@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"],
"@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-core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
@@ -51,8 +51,8 @@
},
"include": [
"src",
"../../libs/common/src/platform/services/**/*.worker.ts",
"../../libs/common/src/autofill/constants",
"../../libs/common/custom-matchers.d.ts"
"../../libs/common/custom-matchers.d.ts",
"../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"
]
}

View File

@@ -205,7 +205,7 @@ const mainConfig = {
"./src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts",
"overlay/list":
"./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts",
"encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts",
"encrypt-worker": "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts",
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
},
optimization: {

View File

@@ -5,7 +5,7 @@ import {
OrganizationUserConfirmRequest,
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { KeyService } from "@bitwarden/key-management";

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";

View File

@@ -6,10 +6,10 @@ import { CollectionRequest } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
import { FolderExport } from "@bitwarden/common/models/export/folder.export";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
@@ -123,6 +123,9 @@ export class EditCommand {
"Item does not belong to an organization. Consider moving it first.",
);
}
if (!cipher.viewPassword) {
return Response.noEditPermission();
}
cipher.collectionIds = req;
try {

View File

@@ -13,6 +13,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CardExport } from "@bitwarden/common/models/export/card.export";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
@@ -23,7 +24,6 @@ import { LoginUriExport } from "@bitwarden/common/models/export/login-uri.export
import { LoginExport } from "@bitwarden/common/models/export/login.export";
import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";

View File

@@ -39,6 +39,10 @@ export class Response {
return Response.error("Not found.");
}
static noEditPermission(): Response {
return Response.error("You do not have permission to edit this item");
}
static badRequest(message: string): Response {
return Response.error(message);
}

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { throwError } from "rxjs";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";

View File

@@ -61,6 +61,8 @@ import {
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { ClientType } from "@bitwarden/common/enums";
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
import { FallbackBulkEncryptService } from "@bitwarden/common/key-management/crypto/services/fallback-bulk-encrypt.service";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
@@ -83,8 +85,6 @@ import { AppIdService } from "@bitwarden/common/platform/services/app-id.service
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { FallbackBulkEncryptService } from "@bitwarden/common/platform/services/cryptography/fallback-bulk-encrypt.service";
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
@@ -151,7 +151,7 @@ import {
ImportApiServiceAbstraction,
ImportService,
ImportServiceAbstraction,
} from "@bitwarden/importer/core";
} from "@bitwarden/importer-core";
import {
DefaultKdfConfigService,
KdfConfigService,

View File

@@ -11,7 +11,7 @@ import {
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer/core";
import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer-core";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/message.response";

View File

@@ -5,7 +5,7 @@ import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";

View File

@@ -5,9 +5,9 @@ import * as inquirer from "inquirer";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";

View File

@@ -11,10 +11,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
import { FolderExport } from "@bitwarden/common/models/export/folder.export";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";

View File

@@ -17,7 +17,7 @@
"@bitwarden/auth/common": ["../../libs/auth/src/common"],
"@bitwarden/auth/angular": ["../../libs/auth/src/angular"],
"@bitwarden/common/*": ["../../libs/common/src/*"],
"@bitwarden/importer/core": ["../../libs/importer/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
"@bitwarden/generator-core": ["../../libs/tools/generator/core/src"],
"@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"],

View File

@@ -3,11 +3,11 @@ import "module-alias/register";
import { v4 as uuidv4 } from "uuid";
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
// eslint-disable-next-line no-restricted-imports

View File

@@ -16,6 +16,8 @@
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
<true/>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>

View File

@@ -54,7 +54,6 @@ import { InternalFolderService } from "@bitwarden/common/vault/abstractions/fold
import { CipherType } from "@bitwarden/common/vault/enums";
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
import { DeleteAccountComponent } from "../auth/delete-account.component";
@@ -65,9 +64,7 @@ import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.compo
import { SettingsComponent } from "./accounts/settings.component";
import { ExportDesktopComponent } from "./tools/export/export-desktop.component";
import { CredentialGeneratorComponent } from "./tools/generator/credential-generator.component";
import { GeneratorComponent } from "./tools/generator.component";
import { ImportDesktopComponent } from "./tools/import/import-desktop.component";
import { PasswordGeneratorHistoryComponent } from "./tools/password-generator-history.component";
const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes
@@ -126,7 +123,6 @@ export class AppComponent implements OnInit, OnDestroy {
private broadcasterService: BroadcasterService,
private folderService: InternalFolderService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private cipherService: CipherService,
private authService: AuthService,
private router: Router,
@@ -508,41 +504,13 @@ export class AppComponent implements OnInit, OnDestroy {
}
async openGenerator() {
const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
FeatureFlag.GeneratorToolsModernization,
);
if (isGeneratorSwapEnabled) {
await this.dialogService.open(CredentialGeneratorComponent);
return;
}
this.modalService.closeAll();
[this.modal] = await this.modalService.openViewRef(
GeneratorComponent,
this.generatorModalRef,
(comp) => (comp.comingFromAddEdit = false),
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.modal.onClosed.subscribe(() => {
this.modal = null;
});
await this.dialogService.open(CredentialGeneratorComponent);
return;
}
async openGeneratorHistory() {
const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
FeatureFlag.GeneratorToolsModernization,
);
if (isGeneratorSwapEnabled) {
await this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
return;
}
await this.openModal<PasswordGeneratorHistoryComponent>(
PasswordGeneratorHistoryComponent,
this.passwordHistoryRef,
);
await this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
return;
}
private async updateAppMenu() {

View File

@@ -47,8 +47,6 @@ import { HeaderComponent } from "./layout/header.component";
import { NavComponent } from "./layout/nav.component";
import { SearchComponent } from "./layout/search/search.component";
import { SharedModule } from "./shared/shared.module";
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 { SendComponent } from "./tools/send/send.component";
@@ -80,8 +78,6 @@ import { SendComponent } from "./tools/send/send.component";
HeaderComponent,
HintComponent,
NavComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordHistoryComponent,
PremiumComponent,
RegisterComponent,

View File

@@ -7,7 +7,7 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";

View File

@@ -50,10 +50,10 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { ClientType } from "@bitwarden/common/enums";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";

View File

@@ -1,636 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="generatorTitle">
<div class="modal-dialog modal-md" role="document">
<div class="modal-content">
<div class="modal-body">
<h1 class="modal-title" id="generatorTitle">
{{ "generator" | i18n }}
</h1>
<bit-callout
type="info"
*ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'"
>
{{ "passwordGeneratorPolicyInEffect" | i18n }}
</bit-callout>
<div class="generated-block" *ngIf="type === 'password'">
<div
class="generated-wrapper"
[innerHTML]="password | colorPassword"
[appCopyText]="password"
></div>
<div class="action-buttons">
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'regeneratePassword' | i18n }}"
(click)="regenerate()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="generated-block" *ngIf="type === 'username'">
<div
class="generated-wrapper"
[innerHTML]="username | colorPassword"
[appCopyText]="username"
></div>
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'regenerateUsername' | i18n }}"
(click)="$any(form).loading ? false : regenerate()"
[attr.aria-disabled]="$any(form).loading ? 'true' : null"
>
<i
class="bwi bwi-lg bwi-generate"
[ngClass]="$any(form).loading ? 'bwi-spin' : ''"
aria-hidden="true"
></i>
</button>
</div>
</div>
<div class="box" *ngIf="!comingFromAddEdit">
<div class="box-content condensed">
<div
class="box-content-row box-content-row-radio"
role="radiogroup"
aria-labelledby="typeHeading"
>
<label id="typeHeading" class="radio-header">{{
"whatWouldYouLikeToGenerate" | i18n
}}</label>
<div
class="radio-group text-default"
appBoxRow
name="TypeOptions"
*ngFor="let o of typeOptions"
>
<input
type="radio"
class="radio"
[(ngModel)]="type"
name="Type"
id="type_{{ o.value }}"
[value]="o.value"
(change)="typeChanged()"
[checked]="type === o.value"
/>
<label class="unstyled" for="type_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<ng-container *ngIf="type === 'password'">
<div class="box">
<h2 class="box-header">
<button type="button" (click)="toggleOptions()" [attr.aria-expanded]="showOptions">
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
></i>
{{ "options" | i18n }}
</button>
</h2>
<div class="box-content condensed" [hidden]="!showOptions">
<div
class="box-content-row box-content-row-radio"
role="radiogroup"
aria-labelledby="passwordTypeHeading"
>
<label id="passwordTypeHeading" class="radio-header">{{
"passwordType" | i18n
}}</label>
<div
class="radio-group text-default"
appBoxRow
name="PassTypeOptions"
*ngFor="let o of passTypeOptions"
>
<input
type="radio"
class="radio"
[(ngModel)]="passwordOptions.type"
name="PasswordType"
id="passwordType_{{ o.value }}"
[value]="o.value"
(change)="savePasswordOptions()"
[checked]="passwordOptions.type === o.value"
/>
<label class="unstyled" for="passwordType_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<div class="box" [hidden]="!showOptions" *ngIf="passwordOptions.type === 'passphrase'">
<div class="box-content condensed">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="num-words">{{ "numWords" | i18n }}</label>
<input
id="num-words"
type="number"
min="3"
max="20"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.numWords"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input
id="word-separator"
type="text"
maxlength="1"
(input)="savePasswordOptions()"
[(ngModel)]="passwordOptions.wordSeparator"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.capitalize"
[disabled]="enforcedPasswordPolicyOptions?.capitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.includeNumber"
[disabled]="enforcedPasswordPolicyOptions?.includeNumber"
/>
</div>
</div>
</div>
<ng-container *ngIf="passwordOptions.type === 'password'">
<div class="box" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row box-content-row-slider" appBoxRow>
<label for="length">{{ "length" | i18n }}</label>
<input
id="length"
type="number"
[min]="passwordOptions.minLength"
max="128"
[(ngModel)]="passwordOptions.length"
(blur)="savePasswordOptions()"
/>
<input
id="lengthRange"
type="range"
[min]="passwordOptions.minLength"
max="128"
step="1"
[(ngModel)]="passwordOptions.length"
(change)="sliderChanged()"
(input)="sliderInput()"
attr.aria-label="{{ 'length' | i18n }}"
tabindex="-1"
/>
</div>
<div class="box-content-row" appBoxRow>
<span>{{ "passwordMinLength" | i18n }}</span>
<span class="txt-right">{{ passwordOptions.minLength }}</span>
<span
class="sr-only"
attr.aria-label="{{ 'passwordMinLength' | i18n }}"
role="status"
aria-live="polite"
>
{{ passwordOptionsMinLengthForReader$ | async }}
</span>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label>
<input
id="uppercase"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useUppercase"
[(ngModel)]="passwordOptions.uppercase"
attr.aria-label="{{ 'uppercase' | i18n }}"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label>
<input
id="lowercase"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useLowercase"
[(ngModel)]="passwordOptions.lowercase"
attr.aria-label="{{ 'lowercase' | i18n }}"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input
id="numbers"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
[ngModel]="passwordOptions.number"
(ngModelChange)="setPasswordOptionsNumber($event)"
attr.aria-label="{{ 'numbers' | i18n }}"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="special">!&#64;#$%^&*</label>
<input
id="special"
type="checkbox"
(change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
[ngModel]="passwordOptions.special"
(ngModelChange)="setPasswordOptionsSpecial($event)"
attr.aria-label="{{ 'specialCharacters' | i18n }}"
/>
</div>
</div>
</div>
<div class="box" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-number">{{ "minNumbers" | i18n }}</label>
<input
id="min-number"
type="number"
min="0"
max="9"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.minNumber"
(input)="onPasswordOptionsMinNumberInput($event)"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-special">{{ "minSpecial" | i18n }}</label>
<input
id="min-special"
type="number"
min="0"
max="9"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.minSpecial"
(input)="onPasswordOptionsMinSpecialInput($event)"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "ambiguous" | i18n }}</label>
<input
id="ambiguous"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="avoidAmbiguous"
/>
</div>
</div>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="type === 'username'">
<div class="box">
<h2 class="box-header">
<button type="button" (click)="toggleOptions()" [attr.aria-expanded]="showOptions">
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
></i>
{{ "options" | i18n }}
</button>
</h2>
<div class="box-content condensed" [hidden]="!showOptions">
<div
class="box-content-row box-content-row-radio"
role="radiogroup"
aria-labelledby="usernameTypeHeading"
>
<label id="usernameTypeHeading" class="radio-header">
{{ "usernameType" | i18n }}
<a
href="#"
appStopClick
(click)="usernameTypesLearnMore()"
appA11yTitle="{{ 'learnMore' | i18n }}"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</label>
<div
class="radio-group align-start text-default"
appBoxRow
name="UsernameTypeOptions"
*ngFor="let o of usernameTypeOptions"
>
<input
type="radio"
class="radio"
[(ngModel)]="usernameOptions.type"
name="UsernameType"
id="usernameType_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.type === o.value"
/>
<label class="unstyled" for="usernameType_{{ o.value }}">
{{ o.name }}
<small class="help-block" *ngIf="o.desc">{{ o.desc }}</small>
</label>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'forwarded'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row" role="listbox" aria-labelledby="forwardTypeHeading">
<label id="forwardTypeHeading">{{ "service" | i18n }}</label>
<select
id="ForwardTypeDropdown"
name="ForwardType"
[(ngModel)]="usernameOptions.forwardedService"
(change)="saveUsernameOptions()"
>
<option *ngFor="let o of forwardOptions" [ngValue]="o.value" role="option">
{{ o.name }}
</option>
</select>
</div>
<ng-container *ngIf="usernameOptions.forwardedService === 'simplelogin'">
<div class="box-content-row" appBoxRow>
<label for="simplelogin-apikey">{{ "apiKey" | i18n }}</label>
<input
id="simplelogin-apikey"
type="password"
name="SimpleLoginApiKey"
[(ngModel)]="usernameOptions.forwardedSimpleLoginApiKey"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="simplelogin-baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="simplelogin-baseUrl"
type="text"
name="SimpleLoginDomain"
[(ngModel)]="usernameOptions.forwardedSimpleLoginBaseUrl"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'duckduckgo'">
<div class="box-content-row" appBoxRow>
<label for="duckduckgo-apikey">{{ "apiKey" | i18n }}</label>
<input
id="duckduckgo-apikey"
type="password"
name="DuckDuckGoApiKey"
[(ngModel)]="usernameOptions.forwardedDuckDuckGoToken"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'anonaddy'">
<div class="box-content-row" appBoxRow>
<label for="anonaddy-accessToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="anonaddy-accessToken"
type="password"
name="AnonAddyAccessToken"
[(ngModel)]="usernameOptions.forwardedAnonAddyApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="anonaddy-domain">{{ "aliasDomain" | i18n }}</label>
<input
id="anonaddy-domain"
type="text"
name="AnonAddyDomain"
[(ngModel)]="usernameOptions.forwardedAnonAddyDomain"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="anonaddy-baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="anonaddy-baseUrl"
type="text"
name="AnonAddyDomain"
[(ngModel)]="usernameOptions.forwardedAnonAddyBaseUrl"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'firefoxrelay'">
<div class="box-content-row" appBoxRow>
<label for="firefox-apikey">{{ "apiAccessToken" | i18n }}</label>
<input
id="firefox-apikey"
type="password"
name="FirefoxApiKey"
[(ngModel)]="usernameOptions.forwardedFirefoxApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'fastmail'">
<div class="box-content-row" appBoxRow>
<label for="fastmail-apiToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="fastmail-apiToken"
type="password"
name="FastmailApiToken"
[(ngModel)]="usernameOptions.forwardedFastmailApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
<ng-container *ngIf="usernameOptions.forwardedService === 'forwardemail'">
<div class="box-content-row" appBoxRow>
<label for="forwardemail-accessToken">{{ "apiAccessToken" | i18n }}</label>
<input
id="forwardemail-accessToken"
type="password"
name="ForwardEmailAccessToken"
[(ngModel)]="usernameOptions.forwardedForwardEmailApiToken"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="forwardemail-domain">{{ "aliasDomain" | i18n }}</label>
<input
id="forwardemail-domain"
type="text"
name="ForwardEmailDomain"
[(ngModel)]="usernameOptions.forwardedForwardEmailDomain"
(blur)="saveUsernameOptions()"
/>
</div>
</ng-container>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'subaddress'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row" appBoxRow>
<label for="subaddress-email">{{ "emailAddress" | i18n }}</label>
<input
id="subaddress-email"
type="text"
name="SubaddressEmail"
[(ngModel)]="usernameOptions.subaddressEmail"
(blur)="saveUsernameOptions()"
/>
</div>
<div
class="box-content-row"
role="radiogroup"
aria-labelledby="subaddressTypeHeading"
*ngIf="subaddressOptions.length > 1"
>
<label id="subaddressTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of subaddressOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.subaddressType"
name="SubaddressType"
id="subaddresstype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.subaddressType === o.value"
/>
<label for="subaddresstype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
<label for="subaddress-website">{{ "website" | i18n }}</label>
<input
id="subaddress-website"
type="text"
name="SubaddressWebsite"
[value]="usernameOptions.website"
disabled
readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'catchall'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row" appBoxRow>
<label for="catchall-domain">{{ "domainName" | i18n }}</label>
<input
id="catchall-domain"
type="text"
name="CatchallDomain"
[(ngModel)]="usernameOptions.catchallDomain"
(blur)="saveUsernameOptions()"
/>
</div>
<div
class="box-content-row"
role="radiogroup"
aria-labelledby="catchallTypeHeading"
*ngIf="catchallOptions.length > 1"
>
<label id="catchallTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of catchallOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.catchallType"
name="CatchallType"
id="catchalltype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.catchallType === o.value"
/>
<label for="catchalltype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
<label for="catchall-website">{{ "website" | i18n }}</label>
<input
id="catchall-website"
type="text"
name="CatchallWebsite"
[value]="usernameOptions.website"
disabled
readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'word'" [hidden]="!showOptions">
<div class="box-content condensed">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordCapitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordIncludeNumber"
/>
</div>
</div>
</div>
</ng-container>
</div>
<div class="modal-footer">
<button
type="button"
class="primary"
*ngIf="comingFromAddEdit"
(click)="select()"
appA11yTitle="{{ 'select' | i18n }}"
>
<i class="bwi bwi-lg bwi-fw bwi-check" aria-hidden="true"></i>
</button>
<button type="button" data-dismiss="modal">
{{ (comingFromAddEdit ? "cancel" : "close") | i18n }}
</button>
</div>
</div>
</div>
</div>

View File

@@ -1,90 +0,0 @@
import { NO_ERRORS_SCHEMA } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { ToastService } from "@bitwarden/components";
import {
PasswordGenerationServiceAbstraction,
UsernameGenerationServiceAbstraction,
} from "@bitwarden/generator-legacy";
import { GeneratorComponent } from "./generator.component";
describe("GeneratorComponent", () => {
let component: GeneratorComponent;
let fixture: ComponentFixture<GeneratorComponent>;
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
beforeEach(() => {
platformUtilsServiceMock = mock<PlatformUtilsService>();
// 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
TestBed.configureTestingModule({
declarations: [GeneratorComponent, I18nPipe],
providers: [
{
provide: PasswordGenerationServiceAbstraction,
useValue: mock<PasswordGenerationServiceAbstraction>(),
},
{
provide: UsernameGenerationServiceAbstraction,
useValue: mock<UsernameGenerationServiceAbstraction>(),
},
{
provide: PlatformUtilsService,
useValue: platformUtilsServiceMock,
},
{
provide: I18nService,
useValue: mock<I18nService>(),
},
{
provide: ActivatedRoute,
useValue: mock<ActivatedRoute>(),
},
{
provide: LogService,
useValue: mock<LogService>(),
},
{
provide: CipherService,
useValue: mock<CipherService>(),
},
{
provide: AccountService,
useValue: mock<AccountService>(),
},
{
provide: ToastService,
useValue: mock<ToastService>(),
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(GeneratorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
describe("usernameTypesLearnMore()", () => {
it("should call platformUtilsService.launchUri() once", () => {
component.usernameTypesLearnMore();
expect(platformUtilsServiceMock.launchUri).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -1,48 +0,0 @@
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 { ToastService } from "@bitwarden/components";
import {
PasswordGenerationServiceAbstraction,
UsernameGenerationServiceAbstraction,
} from "@bitwarden/generator-legacy";
@Component({
selector: "app-generator",
templateUrl: "generator.component.html",
})
export class GeneratorComponent extends BaseGeneratorComponent {
constructor(
passwordGenerationService: PasswordGenerationServiceAbstraction,
usernameGenerationService: UsernameGenerationServiceAbstraction,
accountService: AccountService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
route: ActivatedRoute,
ngZone: NgZone,
logService: LogService,
toastService: ToastService,
) {
super(
passwordGenerationService,
usernameGenerationService,
platformUtilsService,
accountService,
i18nService,
logService,
route,
ngZone,
window,
toastService,
);
}
usernameTypesLearnMore() {
this.platformUtilsService.launchUri("https://bitwarden.com/help/generator/#username-types");
}
}

View File

@@ -1,52 +0,0 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="passwordGenHistoryTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="box">
<h1 class="box-header" id="passwordGenHistoryTitle">
{{ "passwordHistory" | i18n }}
</h1>
<div class="box-content condensed">
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main">
<div
class="password-wrapper monospaced"
[appCopyText]="h.password"
[innerHTML]="h.password | colorPassword"
></div>
<span class="detail">{{ h.date | date: "medium" }}</span>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(h.password)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row" *ngIf="!history.length">
{{ "noPasswordsInList" | i18n }}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
<div class="right">
<button
type="button"
(click)="clear()"
class="danger"
appA11yTitle="{{ 'clear' | i18n }}"
>
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
</div>

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

@@ -11,8 +11,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@@ -3,8 +3,8 @@
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";

View File

@@ -66,9 +66,9 @@ a {
}
}
input,
input:not(bit-form-field input),
select,
textarea {
textarea:not(bit-form-field textarea) {
@include themify($themes) {
color: themed("textColor");
background-color: themed("inputBackgroundColor");

View File

@@ -188,44 +188,6 @@ p.lead {
}
}
.generated-block {
font-size: $font-size-large;
font-family: $font-family-monospace;
padding: 8px 10px 8px 0;
display: flex;
border-radius: $border-radius;
border: 1px solid;
@include themify($themes) {
background-color: transparent;
border-color: themed("borderColorAlt");
}
.modal-body & {
margin-top: 10px;
}
.generated-wrapper {
text-align: left;
width: 100%;
min-width: 0;
white-space: pre-wrap;
word-break: break-all;
padding: 15px;
}
.action-buttons {
display: flex;
align-self: center;
height: 100%;
margin-left: 10px;
button {
margin-left: 10px;
}
}
}
.password-wrapper {
overflow-wrap: break-word;
white-space: pre-wrap;

View File

@@ -5,8 +5,8 @@ import { of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";

View File

@@ -6,8 +6,8 @@ import { combineLatest, concatMap, firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";

View File

@@ -4,8 +4,8 @@ import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { NativeMessagingVersion } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";

View File

@@ -4,7 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";

View File

@@ -21,6 +21,7 @@
type="checkbox"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
[disabled]="!cipher.canAssignToCollections"
/>
</div>
</div>

View File

@@ -39,8 +39,8 @@
(onCancelled)="cancelledAddEdit($event)"
(onShareCipher)="shareCipher($event)"
(onEditCollections)="cipherCollections($event)"
(onGeneratePassword)="openGenerator(true, true)"
(onGenerateUsername)="openGenerator(true, false)"
(onGeneratePassword)="openGenerator(true)"
(onGenerateUsername)="openGenerator(false)"
>
</app-vault-add-edit>
<div

View File

@@ -21,7 +21,6 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -39,7 +38,6 @@ import { DialogService, ToastService } from "@bitwarden/components";
import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault";
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
import { GeneratorComponent } from "../../../app/tools/generator.component";
import { invokeMenu, RendererMenuItem } from "../../../utils";
import { AddEditComponent } from "./add-edit.component";
@@ -666,65 +664,21 @@ export class VaultComponent implements OnInit, OnDestroy {
return "searchVault";
}
async openGenerator(comingFromAddEdit: boolean, passwordType = true) {
const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
FeatureFlag.GeneratorToolsModernization,
);
if (isGeneratorSwapEnabled) {
CredentialGeneratorDialogComponent.open(this.dialogService, {
onCredentialGenerated: (value?: string) => {
if (this.addEditComponent != null) {
this.addEditComponent.markPasswordAsDirty();
if (passwordType) {
this.addEditComponent.cipher.login.password = value ?? "";
} else {
this.addEditComponent.cipher.login.username = value ?? "";
}
}
},
type: passwordType ? "password" : "username",
});
return;
}
// TODO: Legacy code below, remove once the new generator is fully implemented
// https://bitwarden.atlassian.net/browse/PM-7121
const cipher = this.addEditComponent?.cipher;
const loginType = cipher != null && cipher.type === CipherType.Login && cipher.login != null;
const [modal, childComponent] = await this.modalService.openViewRef(
GeneratorComponent,
this.generatorModalRef,
(comp) => {
comp.comingFromAddEdit = comingFromAddEdit;
if (comingFromAddEdit) {
comp.type = passwordType ? "password" : "username";
if (loginType && cipher.login.hasUris && cipher.login.uris[0].hostname != null) {
comp.usernameWebsite = cipher.login.uris[0].hostname;
async openGenerator(passwordType = true) {
CredentialGeneratorDialogComponent.open(this.dialogService, {
onCredentialGenerated: (value?: string) => {
if (this.addEditComponent != null) {
this.addEditComponent.markPasswordAsDirty();
if (passwordType) {
this.addEditComponent.cipher.login.password = value ?? "";
} else {
this.addEditComponent.cipher.login.username = value ?? "";
}
}
},
);
this.modal = modal;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
childComponent.onSelected.subscribe((value: string) => {
this.modal.close();
if (loginType) {
this.addEditComponent.markPasswordAsDirty();
if (passwordType) {
this.addEditComponent.cipher.login.password = value;
} else {
this.addEditComponent.cipher.login.username = value;
}
}
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.modal.onClosed.subscribe(() => {
this.modal = null;
type: passwordType ? "password" : "username",
});
return;
}
async addFolder() {

View File

@@ -17,8 +17,8 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";

View File

@@ -22,7 +22,7 @@
"@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"],
"@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-core": ["../../libs/importer/src"],
"@bitwarden/importer/ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
@@ -48,5 +48,5 @@
"strictTemplates": true,
"preserveWhitespaces": true
},
"include": ["src", "../../libs/common/src/platform/services/**/*.worker.ts"]
"include": ["src", "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts"]
}

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

@@ -8,8 +8,8 @@ import {
} from "@bitwarden/admin-console/common";
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";

View File

@@ -14,8 +14,8 @@ import {
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { StateProvider } from "@bitwarden/common/platform/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

@@ -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

@@ -46,8 +46,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -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,7 +11,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";

View File

@@ -10,7 +10,7 @@ import {
} from "@bitwarden/admin-console/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";

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 { ImportCollectionServiceAbstraction } from "@bitwarden/importer/core";
import { ImportComponent } from "@bitwarden/importer/ui";
import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core";
import { LooseComponentsModule, SharedModule } from "../../../shared";
import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service";

View File

@@ -1,7 +1,7 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { KeyService } from "@bitwarden/key-management";

View File

@@ -1,7 +1,7 @@
import { inject, Injectable } from "@angular/core";
import { RotateableKeySet } from "@bitwarden/auth/common";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { KeyService } from "@bitwarden/key-management";

View File

@@ -4,11 +4,11 @@ import { MockProxy } from "jest-mock-extended";
import mock from "jest-mock-extended/lib/Mock";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response";
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";

View File

@@ -6,9 +6,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";

View File

@@ -12,7 +12,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { FakeGlobalState } from "@bitwarden/common/spec/fake-state";

View File

@@ -16,7 +16,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";

View File

@@ -1,158 +0,0 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable tailwindcss/no-custom-classname -->
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
class="tw-container tw-mx-auto"
[formGroup]="formGroup"
>
<div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
<input
id="register-form_input_email"
bitInput
type="email"
formControlName="email"
[attr.readonly]="queryParamFromOrgInvite ? true : null"
/>
<bit-hint>{{ "emailAddressDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input id="register-form_input_name" bitInput type="text" formControlName="name" />
<bit-hint>{{ "yourNameDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3">
<auth-password-callout [policy]="enforcedPolicyOptions" *ngIf="enforcedPolicyOptions">
</auth-password-callout>
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
id="register-form_input_master-password"
bitInput
type="password"
formControlName="masterPassword"
/>
<button
type="button"
bitSuffix
bitIconButton
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
<bit-hint>
<span class="tw-font-semibold">{{ "important" | i18n }}</span>
{{ "masterPassImportant" | i18n }} {{ characterMinimumMessage }}
</bit-hint>
</bit-form-field>
<app-password-strength
[password]="formGroup.get('masterPassword')?.value"
[email]="formGroup.get('email')?.value"
[name]="formGroup.get('name')?.value"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
<input
id="register-form_input_confirm-master-password"
bitInput
type="password"
formControlName="confirmMasterPassword"
/>
<button
type="button"
bitSuffix
bitIconButton
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
<input id="register-form_input_hint" bitInput type="text" formControlName="hint" />
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
<div class="tw-mb-4 tw-flex tw-items-start">
<input
class="mt-1"
type="checkbox"
bitCheckbox
id="checkForBreaches"
name="CheckBreach"
formControlName="checkForBreaches"
/>
<bit-label for="checkForBreaches"> {{ "checkForBreaches" | i18n }}</bit-label>
</div>
<div class="tw-mb-3 tw-flex tw-items-start" *ngIf="showTerms">
<input
class="mt-1"
id="register-form-input-accept-policies"
bitCheckbox
type="checkbox"
formControlName="acceptPolicies"
/>
<bit-label for="register-form-input-accept-policies">
{{ "acceptPolicies" | i18n }}<br />
<a bitLink href="https://bitwarden.com/terms/" target="_blank" rel="noreferrer">{{
"termsOfService" | i18n
}}</a
>,
<a bitLink href="https://bitwarden.com/privacy/" target="_blank" rel="noreferrer">{{
"privacyPolicy" | i18n
}}</a>
</bit-label>
</div>
<div class="tw-space-x-2 tw-pt-2">
<ng-container *ngIf="!accountCreated">
<button
[block]="true"
type="submit"
buttonType="primary"
bitButton
[loading]="form.loading"
>
{{ "createAccount" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="accountCreated">
<button
[block]="true"
type="submit"
buttonType="primary"
bitButton
[loading]="form.loading"
>
{{ "logIn" | i18n }}
</button>
</ng-container>
</div>
<p class="tw-m-0 tw-mt-5 tw-text-sm">
{{ "alreadyHaveAccount" | i18n }}
<a bitLink routerLink="/login">{{ "logIn" | i18n }}</a>
</p>
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
</div>
</form>

View File

@@ -1,115 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input, OnInit } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { KeyService } from "@bitwarden/key-management";
import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service";
@Component({
selector: "app-register-form",
templateUrl: "./register-form.component.html",
})
export class RegisterFormComponent extends BaseRegisterComponent implements OnInit {
@Input() queryParamEmail: string;
@Input() queryParamFromOrgInvite: boolean;
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
@Input() referenceDataValue: ReferenceEventRequest;
showErrorSummary = false;
characterMinimumMessage: string;
constructor(
formValidationErrorService: FormValidationErrorsService,
formBuilder: UntypedFormBuilder,
loginStrategyService: LoginStrategyServiceAbstraction,
router: Router,
i18nService: I18nService,
keyService: KeyService,
apiService: ApiService,
platformUtilsService: PlatformUtilsService,
private policyService: PolicyService,
environmentService: EnvironmentService,
logService: LogService,
auditService: AuditService,
dialogService: DialogService,
acceptOrgInviteService: AcceptOrganizationInviteService,
toastService: ToastService,
) {
super(
formValidationErrorService,
formBuilder,
loginStrategyService,
router,
i18nService,
keyService,
apiService,
platformUtilsService,
environmentService,
logService,
auditService,
dialogService,
toastService,
);
this.modifyRegisterRequest = async (request: RegisterRequest) => {
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
// Org user id and token are included here only for validation and two factor purposes.
const orgInvite = await acceptOrgInviteService.getOrganizationInvite();
if (orgInvite != null) {
request.organizationUserId = orgInvite.organizationUserId;
request.token = orgInvite.token;
}
// Invite is accepted after login (on deep link redirect).
};
}
async ngOnInit() {
await super.ngOnInit();
this.referenceData = this.referenceDataValue;
if (this.queryParamEmail) {
this.formGroup.get("email")?.setValue(this.queryParamEmail);
}
if (this.enforcedPolicyOptions != null && this.enforcedPolicyOptions.minLength > 0) {
this.characterMinimumMessage = "";
} else {
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
}
}
async submit() {
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
this.passwordStrengthResult.score,
this.formGroup.value.masterPassword,
this.enforcedPolicyOptions,
)
) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
});
return;
}
await super.submit(false);
}
}

View File

@@ -1,14 +0,0 @@
import { NgModule } from "@angular/core";
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
import { SharedModule } from "../../shared";
import { RegisterFormComponent } from "./register-form.component";
@NgModule({
imports: [SharedModule, PasswordCalloutComponent],
declarations: [RegisterFormComponent],
exports: [RegisterFormComponent],
})
export class RegisterFormModule {}

View File

@@ -4,7 +4,7 @@ import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/ang
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";

View File

@@ -118,7 +118,13 @@
) | currency: "$"
}}
</b>
<span class="tw-text-xs tw-px-0"> /{{ "monthPerMember" | i18n }}</span>
<span class="tw-text-xs tw-px-0">
/{{
selectableProduct.productTier === productTypes.Families
? "month"
: ("monthPerMember" | i18n)
}}</span
>
<b class="tw-text-sm tw-font-semibold">
<ng-container
*ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption"

Some files were not shown because too many files have changed in this diff Show More