mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 13:40:06 +00:00
Merge remote-tracking branch 'origin/sdk-encrypt-service' into km/cose
This commit is contained in:
20
.github/CODEOWNERS
vendored
20
.github/CODEOWNERS
vendored
@@ -90,7 +90,9 @@ apps/web/src/app/core @bitwarden/team-platform-dev
|
||||
apps/web/src/app/shared @bitwarden/team-platform-dev
|
||||
apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
|
||||
# Workflows
|
||||
.github/workflows/brew-bump-desktop.yml @bitwarden/team-platform-dev
|
||||
# Any changes here should also be reflected in Renovate configuration
|
||||
.github/workflows/automatic-issue-responses.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/automatic-pull-request-responses.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/build-browser-target.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/build-browser.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/build-cli-target.yml @bitwarden/team-platform-dev
|
||||
@@ -100,10 +102,13 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
|
||||
.github/workflows/build-web-target.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/build-web.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/chromatic.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/crowdin-pull.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/enforce-labels.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/lint.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/locales-lint.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/repository-management.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/scan.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/stale-bot.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev
|
||||
# ESLint custom rules
|
||||
@@ -152,6 +157,7 @@ apps/desktop/src/locales/en/messages.json
|
||||
apps/web/src/locales/en/messages.json
|
||||
|
||||
## BRE team owns these workflows ##
|
||||
# Any changes here should also be reflected in Renovate configuration ##
|
||||
.github/workflows/brew-bump-desktop.yml @bitwarden/dept-bre
|
||||
.github/workflows/deploy-web.yml @bitwarden/dept-bre
|
||||
.github/workflows/publish-cli.yml @bitwarden/dept-bre
|
||||
@@ -159,13 +165,11 @@ apps/web/src/locales/en/messages.json
|
||||
.github/workflows/publish-web.yml @bitwarden/dept-bre
|
||||
.github/workflows/retrieve-current-desktop-rollout.yml @bitwarden/dept-bre
|
||||
.github/workflows/staged-rollout-desktop.yml @bitwarden/dept-bre
|
||||
|
||||
## Shared ownership workflows ##
|
||||
.github/workflows/release-browser.yml
|
||||
.github/workflows/release-cli.yml
|
||||
.github/workflows/release-desktop-beta.yml
|
||||
.github/workflows/release-desktop.yml
|
||||
.github/workflows/release-web.yml
|
||||
.github/workflows/release-browser.yml @bitwarden/dept-bre
|
||||
.github/workflows/release-cli.yml @bitwarden/dept-bre
|
||||
.github/workflows/release-desktop-beta.yml @bitwarden/dept-bre
|
||||
.github/workflows/release-desktop.yml @bitwarden/dept-bre
|
||||
.github/workflows/release-web.yml @bitwarden/dept-bre
|
||||
|
||||
## Docker files have shared ownership ##
|
||||
**/Dockerfile
|
||||
|
||||
61
.github/renovate.json5
vendored
61
.github/renovate.json5
vendored
@@ -1,28 +1,65 @@
|
||||
{
|
||||
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||
extends: ["github>bitwarden/renovate-config"], // Extends our base config for pinned dependencies
|
||||
extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies
|
||||
enabledManagers: ["cargo", "github-actions", "npm"],
|
||||
packageRules: [
|
||||
{
|
||||
// Group all build/test/lint workflows for GitHub Actions together for Platform
|
||||
// Since they are code owners we don't need to assign a review team in Renovate
|
||||
// Any changes here should also be reflected in CODEOWNERS
|
||||
groupName: "github-action minor",
|
||||
matchManagers: ["github-actions"],
|
||||
matchUpdateTypes: ["minor"],
|
||||
},
|
||||
{
|
||||
matchManagers: ["cargo"],
|
||||
matchFileNames: [
|
||||
"./github/workflows/automatic-issue-responses.yml",
|
||||
"./github/workflows/automatic-pull-request-responses.yml",
|
||||
"./github/workflows/build-browser.yml",
|
||||
"./github/workflows/build-cli.yml",
|
||||
"./github/workflows/build-desktop.yml",
|
||||
"./github/workflows/build-web.yml",
|
||||
"./github/workflows/chromatic.yml",
|
||||
"./github/workflows/crowdin-pull.yml",
|
||||
"./github/workflows/enforce-labels.yml",
|
||||
"./github/workflows/lint.yml",
|
||||
"./github/workflows/locales-lint.yml",
|
||||
"./github/workflows/repository-management.yml",
|
||||
"./github/workflows/scan.yml",
|
||||
"./github/workflows/stale-bot.yml",
|
||||
"./github/workflows/test.yml",
|
||||
"./github/workflows/version-auto-bump.yml",
|
||||
],
|
||||
commitMessagePrefix: "[deps] Platform:",
|
||||
},
|
||||
{
|
||||
groupName: "napi",
|
||||
matchPackageNames: ["napi", "napi-build", "napi-derive"],
|
||||
// Group all release-related workflows for GitHub Actions together for BRE
|
||||
// Since they are code owners we don't need to assign a review team in Renovate
|
||||
// Any changes here should also be reflected in CODEOWNERS
|
||||
groupName: "github-action minor",
|
||||
matchManagers: ["github-actions"],
|
||||
matchFileNames: [
|
||||
"./github/workflows/brew-bump-desktop.yml",
|
||||
"./github/workflows/deploy-web.yml",
|
||||
"./github/workflows/publish-cli.yml",
|
||||
"./github/workflows/publish-desktop.yml",
|
||||
"./github/workflows/publish-web.yml",
|
||||
"./github/workflows/retrieve-current-desktop-rollout.yml",
|
||||
"./github/workflows/staged-rollout-desktop.yml",
|
||||
"./github/workflows/release-cli.yml",
|
||||
"./github/workflows/release-desktop-beta.yml",
|
||||
"./github/workflows/release-desktop.yml",
|
||||
"./github/workflows/release-web.yml",
|
||||
],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
},
|
||||
{
|
||||
// Disable major and minor updates for TypeScript and Zone.js because they are managed by Angular
|
||||
matchPackageNames: ["typescript", "zone.js"],
|
||||
matchUpdateTypes: ["major", "minor"],
|
||||
description: "Determined by Angular",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
// Disable major updates for core Angular dependencies because they are managed through ng update
|
||||
// when we decide to upgrade.
|
||||
matchSourceUrls: [
|
||||
"https://github.com/angular-eslint/angular-eslint",
|
||||
"https://github.com/angular/angular-cli",
|
||||
@@ -35,19 +72,27 @@
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
// Renovate should manage patch updates for TypeScript and Zone.js, despite ignoring major and minor
|
||||
matchPackageNames: ["typescript", "zone.js"],
|
||||
matchUpdateTypes: "patch",
|
||||
},
|
||||
{
|
||||
// We want to update all the Jest-related packages together, to reduce PR noise
|
||||
groupName: "jest",
|
||||
matchPackageNames: ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
|
||||
matchUpdateTypes: "major",
|
||||
},
|
||||
{
|
||||
// We need to group all napi-related packages together to avoid build errors caused by version incompatibilities
|
||||
groupName: "napi",
|
||||
matchPackageNames: ["napi", "napi-build", "napi-derive"],
|
||||
},
|
||||
{
|
||||
// We need to group all macOS/iOS binding-related packages together to avoid build errors caused by version incompatibilities
|
||||
groupName: "macOS/iOS bindings",
|
||||
matchPackageNames: ["core-foundation", "security-framework", "security-framework-sys"],
|
||||
},
|
||||
{
|
||||
// We need to group all zbus-related packages together to avoid build errors caused by version incompatibilities
|
||||
groupName: "zbus",
|
||||
matchPackageNames: ["zbus", "zbus_polkit"],
|
||||
},
|
||||
|
||||
21
.github/workflows/test.yml
vendored
21
.github/workflows/test.yml
vendored
@@ -11,28 +11,10 @@ on:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
check-test-secrets:
|
||||
name: Check for test secrets
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check
|
||||
id: check-test-secrets
|
||||
run: |
|
||||
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
testing:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-test-secrets
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
@@ -77,7 +59,7 @@ jobs:
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "junit.xml"
|
||||
@@ -89,7 +71,6 @@ jobs:
|
||||
|
||||
- name: Upload results to codecov.io
|
||||
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
@@ -669,8 +669,8 @@
|
||||
"browserNotSupportClipboard": {
|
||||
"message": "Your web browser does not support easy clipboard copying. Copy it manually instead."
|
||||
},
|
||||
"verifyIdentity": {
|
||||
"message": "Verify identity"
|
||||
"verifyYourIdentity": {
|
||||
"message": "Verify your identity"
|
||||
},
|
||||
"weDontRecognizeThisDevice": {
|
||||
"message": "We don't recognize this device. Enter the code sent to your email to verify your identity."
|
||||
@@ -1088,6 +1088,38 @@
|
||||
},
|
||||
"description": "Shown to user after login is updated."
|
||||
},
|
||||
"saveAsNewLoginAction": {
|
||||
"message": "Save as new login",
|
||||
"description": "Button text for saving login details as a new entry."
|
||||
},
|
||||
"updateLoginAction": {
|
||||
"message": "Update login",
|
||||
"description": "Button text for updating an existing login entry."
|
||||
},
|
||||
"saveLoginPrompt": {
|
||||
"message": "Save login?",
|
||||
"description": "Prompt asking the user if they want to save their login details."
|
||||
},
|
||||
"updateLoginPrompt": {
|
||||
"message": "Update existing login?",
|
||||
"description": "Prompt asking the user if they want to update an existing login entry."
|
||||
},
|
||||
"loginSaveSuccess": {
|
||||
"message": "Login saved",
|
||||
"description": "Message displayed when login details are successfully saved."
|
||||
},
|
||||
"loginUpdateSuccess": {
|
||||
"message": "Login updated",
|
||||
"description": "Message displayed when login details are successfully updated."
|
||||
},
|
||||
"saveFailure": {
|
||||
"message": "Error saving",
|
||||
"description": "Error message shown when the system fails to save login details."
|
||||
},
|
||||
"saveFailureDetails": {
|
||||
"message": "Oh no! We couldn't save this. Try entering the details manually.",
|
||||
"description": "Detailed error message shown when saving login details fails."
|
||||
},
|
||||
"enableChangedPasswordNotification": {
|
||||
"message": "Ask to update existing login"
|
||||
},
|
||||
@@ -5131,5 +5163,8 @@
|
||||
},
|
||||
"updateDesktopAppOrDisableFingerprintDialogMessage": {
|
||||
"message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings."
|
||||
},
|
||||
"changeAtRiskPassword": {
|
||||
"message": "Change at-risk password"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,10 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
|
||||
if (flag) {
|
||||
const qParams = await firstValueFrom(this.route.queryParams);
|
||||
|
||||
const uniqueQueryParams = {
|
||||
...this.route.queryParams,
|
||||
...qParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(),
|
||||
};
|
||||
|
||||
@@ -845,6 +845,7 @@ async function loadNotificationBar() {
|
||||
theme: typeData.theme,
|
||||
removeIndividualVault: typeData.removeIndividualVault,
|
||||
importType: typeData.importType,
|
||||
launchTimestamp: typeData.launchTimestamp,
|
||||
};
|
||||
const notificationBarUrl = "notification/bar.html";
|
||||
|
||||
@@ -873,11 +874,32 @@ async function loadNotificationBar() {
|
||||
const barPageUrl: string = chrome.runtime.getURL(barPage);
|
||||
|
||||
function getIframeStyle(useComponentBar: boolean): string {
|
||||
return (
|
||||
(useComponentBar
|
||||
? "height: calc(276px + 25px); width: 450px; right: 0;"
|
||||
: "height: 42px; width: 100%;") + " border: 0; min-height: initial;"
|
||||
);
|
||||
const isNotificationFresh =
|
||||
notificationBarInitData.launchTimestamp &&
|
||||
Date.now() - notificationBarInitData.launchTimestamp < 500;
|
||||
|
||||
const baseStyle = useComponentBar
|
||||
? isNotificationFresh
|
||||
? "height: calc(276px + 25px); width: 450px; right: 0; transform:translateX(100%); opacity:0;"
|
||||
: "height: calc(276px + 25px); width: 450px; right: 0; transform:translateX(0%); opacity:1;"
|
||||
: "height: 42px; width: 100%;";
|
||||
|
||||
const transitionStyle =
|
||||
isNotificationFresh && useComponentBar
|
||||
? "transition: transform 0.15s ease-in, opacity 0.15s ease; transform:translateX(0%); opacity:1; transition-delay: 0.25s;"
|
||||
: "";
|
||||
|
||||
notificationBarIframe.style.cssText = baseStyle + " border: 0; min-height: initial;";
|
||||
|
||||
if (isNotificationFresh && useComponentBar) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
notificationBarIframe.style.cssText += transitionStyle;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return baseStyle + " border: 0; min-height: initial;";
|
||||
}
|
||||
|
||||
notificationBarIframe = document.createElement("iframe");
|
||||
|
||||
@@ -53,30 +53,28 @@ function getI18n() {
|
||||
return {
|
||||
appName: chrome.i18n.getMessage("appName"),
|
||||
close: chrome.i18n.getMessage("close"),
|
||||
never: chrome.i18n.getMessage("never"),
|
||||
folder: chrome.i18n.getMessage("folder"),
|
||||
loginSaveSuccess: chrome.i18n.getMessage("loginSaveSuccess"),
|
||||
loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"),
|
||||
loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"),
|
||||
loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"),
|
||||
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
|
||||
newItem: chrome.i18n.getMessage("newItem"),
|
||||
never: chrome.i18n.getMessage("never"),
|
||||
notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"),
|
||||
notificationEdit: chrome.i18n.getMessage("edit"),
|
||||
notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"),
|
||||
notificationAddSave: chrome.i18n.getMessage("notificationAddSave"),
|
||||
notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"),
|
||||
notificationChangeSave: chrome.i18n.getMessage("notificationChangeSave"),
|
||||
notificationEdit: chrome.i18n.getMessage("edit"),
|
||||
notificationUnlock: chrome.i18n.getMessage("notificationUnlock"),
|
||||
notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"),
|
||||
|
||||
// @TODO move values to message catalog
|
||||
saveAction: "Save",
|
||||
saveAsNewLoginAction: "Save as new login",
|
||||
updateLoginAction: "Update login",
|
||||
saveLoginPrompt: "Save login?",
|
||||
updateLoginPrompt: "Update existing login?",
|
||||
loginSaveSuccess: "Login saved",
|
||||
loginUpdateSuccess: "Login updated",
|
||||
saveFailure: "Error saving",
|
||||
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details as a New item",
|
||||
newItem: "New item",
|
||||
view: "View",
|
||||
saveAction: chrome.i18n.getMessage("notificationAddSave"),
|
||||
saveAsNewLoginAction: chrome.i18n.getMessage("saveAsNewLoginAction"),
|
||||
saveFailure: chrome.i18n.getMessage("saveFailure"),
|
||||
saveFailureDetails: chrome.i18n.getMessage("saveFailureDetails"),
|
||||
saveLoginPrompt: chrome.i18n.getMessage("saveLoginPrompt"),
|
||||
updateLoginAction: chrome.i18n.getMessage("updateLoginAction"),
|
||||
updateLoginPrompt: chrome.i18n.getMessage("updateLoginPrompt"),
|
||||
view: chrome.i18n.getMessage("view"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1330,6 +1330,8 @@ export default class MainBackground {
|
||||
this.syncServiceListener?.listener$().subscribe();
|
||||
await this.autoSubmitLoginBackground.init();
|
||||
|
||||
this.configService.broadcastConfigChangesTo(this.encryptService, this.bulkEncryptService);
|
||||
|
||||
if (
|
||||
BrowserApi.isManifestVersion(2) &&
|
||||
(await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService))
|
||||
|
||||
@@ -171,7 +171,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
elevation: 1,
|
||||
pageTitle: {
|
||||
key: "verifyIdentity",
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
showBackButton: true,
|
||||
} satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData,
|
||||
@@ -248,7 +248,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
pageIcon: DeviceVerificationIcon,
|
||||
pageTitle: {
|
||||
key: "verifyIdentity",
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "weDontRecognizeThisDevice",
|
||||
|
||||
@@ -3,6 +3,8 @@ import { inject, Inject, Injectable } from "@angular/core";
|
||||
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -27,6 +29,8 @@ export class InitService {
|
||||
private themingService: AbstractThemingService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private viewCacheService: PopupViewCacheService,
|
||||
private encryptService: EncryptService,
|
||||
private configService: ConfigService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -58,6 +62,8 @@ export class InitService {
|
||||
this.logService.info("Force redraw is on");
|
||||
}
|
||||
|
||||
this.configService.broadcastConfigChangesTo(this.encryptService);
|
||||
|
||||
this.setupVaultPopupHeartbeat();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -164,7 +164,15 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
|
||||
break
|
||||
}
|
||||
|
||||
guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else {
|
||||
var flags: SecAccessControlCreateFlags = [.privateKeyUsage];
|
||||
// https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/biometryany
|
||||
if #available(macOS 10.13.4, *) {
|
||||
flags.insert(.biometryAny)
|
||||
} else {
|
||||
flags.insert(.touchIDAny)
|
||||
}
|
||||
|
||||
guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, nil) else {
|
||||
let messageId = message?["messageId"] as? Int
|
||||
response.userInfo = [
|
||||
SFExtensionMessageKey: [
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
slot="header"
|
||||
[backAction]="close"
|
||||
showBackButton
|
||||
[pageTitle]="title"
|
||||
[pageTitle]="titleKey | i18n"
|
||||
></popup-header>
|
||||
|
||||
<vault-cipher-form-generator
|
||||
[type]="params.type"
|
||||
[uri]="uri"
|
||||
(valueGenerated)="onValueGenerated($event)"
|
||||
(algorithmSelected)="onAlgorithmSelected($event)"
|
||||
></vault-cipher-form-generator>
|
||||
|
||||
<popup-footer slot="footer">
|
||||
@@ -19,6 +20,7 @@
|
||||
buttonType="primary"
|
||||
(click)="selectValue()"
|
||||
data-testid="select-button"
|
||||
[disabled]="!(selectButtonText && generatedValue)"
|
||||
>
|
||||
{{ selectButtonText }}
|
||||
</button>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
|
||||
import {
|
||||
GeneratorDialogAction,
|
||||
GeneratorDialogParams,
|
||||
GeneratorDialogResult,
|
||||
VaultGeneratorDialogComponent,
|
||||
@@ -21,8 +24,9 @@ import {
|
||||
standalone: true,
|
||||
})
|
||||
class MockCipherFormGenerator {
|
||||
@Input() type: "password" | "username";
|
||||
@Input() uri: string;
|
||||
@Input() type: "password" | "username" = "password";
|
||||
@Output() algorithmSelected: EventEmitter<AlgorithmInfo> = new EventEmitter();
|
||||
@Input() uri: string = "";
|
||||
@Output() valueGenerated = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
@@ -53,34 +57,87 @@ describe("VaultGeneratorDialogComponent", () => {
|
||||
|
||||
fixture = TestBed.createComponent(VaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
fixture.detectChanges();
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should use the appropriate text based on generator type", () => {
|
||||
expect(component["title"]).toBe("passwordGenerator");
|
||||
expect(component["selectButtonText"]).toBe("useThisPassword");
|
||||
|
||||
dialogData.type = "username";
|
||||
|
||||
fixture = TestBed.createComponent(VaultGeneratorDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
expect(component["title"]).toBe("usernameGenerator");
|
||||
expect(component["selectButtonText"]).toBe("useThisUsername");
|
||||
it("should show password generator title", () => {
|
||||
const header = fixture.debugElement.query(By.css("popup-header")).componentInstance;
|
||||
expect(header.pageTitle).toBe("passwordGenerator");
|
||||
});
|
||||
|
||||
it("should close the dialog with the generated value when the user selects it", () => {
|
||||
component["generatedValue"] = "generated-value";
|
||||
it("should pass type to cipher form generator", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
expect(generator.type).toBe("password");
|
||||
});
|
||||
|
||||
fixture.nativeElement.querySelector("button[data-testid='select-button']").click();
|
||||
it("should enable select button when value is generated", () => {
|
||||
component.onAlgorithmSelected({ useGeneratedValue: "Test" } as any);
|
||||
component.onValueGenerated("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should disable the button if no value has been generated", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable the button if no algorithm is selected", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should update button text when algorithm is selected", () => {
|
||||
component.onAlgorithmSelected({ useGeneratedValue: "Use This Password" } as any);
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.textContent.trim()).toBe("Use This Password");
|
||||
});
|
||||
|
||||
it("should close with generated value when selected", () => {
|
||||
component.onAlgorithmSelected({ useGeneratedValue: "Test" } as any);
|
||||
component.onValueGenerated("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.debugElement.query(By.css("[data-testid='select-button']")).nativeElement.click();
|
||||
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
action: "selected",
|
||||
generatedValue: "generated-value",
|
||||
action: GeneratorDialogAction.Selected,
|
||||
generatedValue: "test-password",
|
||||
});
|
||||
});
|
||||
|
||||
it("should close with canceled action when dismissed", () => {
|
||||
fixture.debugElement.query(By.css("popup-header")).componentInstance.backAction();
|
||||
expect(mockDialogRef.close).toHaveBeenCalledWith({
|
||||
action: GeneratorDialogAction.Canceled,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogService } from "@bitwarden/components";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
@@ -39,13 +41,12 @@ export enum GeneratorDialogAction {
|
||||
CommonModule,
|
||||
CipherFormGeneratorComponent,
|
||||
ButtonModule,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class VaultGeneratorDialogComponent {
|
||||
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
|
||||
protected selectButtonText = this.i18nService.t(
|
||||
this.isPassword ? "useThisPassword" : "useThisUsername",
|
||||
);
|
||||
protected selectButtonText: string | undefined;
|
||||
protected titleKey = this.isPassword ? "passwordGenerator" : "usernameGenerator";
|
||||
|
||||
/**
|
||||
* Whether the dialog is generating a password/passphrase. If false, it is generating a username.
|
||||
@@ -92,6 +93,16 @@ export class VaultGeneratorDialogComponent {
|
||||
this.generatedValue = value;
|
||||
}
|
||||
|
||||
onAlgorithmSelected = (selected?: AlgorithmInfo) => {
|
||||
if (selected) {
|
||||
this.selectButtonText = selected.useGeneratedValue;
|
||||
} else {
|
||||
// default to email
|
||||
this.selectButtonText = this.i18nService.t("useThisEmail");
|
||||
}
|
||||
this.generatedValue = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the vault generator dialog in a full screen dialog.
|
||||
*/
|
||||
|
||||
@@ -37,8 +37,16 @@ import {
|
||||
IconButtonModule,
|
||||
SearchModule,
|
||||
ToastService,
|
||||
CalloutModule,
|
||||
} from "@bitwarden/components";
|
||||
import { CipherViewComponent, CopyCipherFieldService } from "@bitwarden/vault";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
CopyCipherFieldService,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
@@ -82,10 +90,13 @@ type LoadAction =
|
||||
CipherViewComponent,
|
||||
AsyncActionsModule,
|
||||
PopOutComponent,
|
||||
CalloutModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
export class ViewV2Component {
|
||||
|
||||
@@ -28,7 +28,7 @@ impl BitwardenDesktopAgent {
|
||||
show_ui_request_tx: auth_request_tx,
|
||||
get_ui_response_rx: auth_response_rx,
|
||||
request_id: Arc::new(AtomicU32::new(0)),
|
||||
needs_unlock: Arc::new(AtomicBool::new(false)),
|
||||
needs_unlock: Arc::new(AtomicBool::new(true)),
|
||||
is_running: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
let cloned_agent_state = agent.clone();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.2.2",
|
||||
"version": "2025.2.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -97,7 +97,7 @@ const routes: Routes = [
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyIdentity",
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
@@ -126,7 +126,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
pageIcon: DeviceVerificationIcon,
|
||||
pageTitle: {
|
||||
key: "verifyIdentity",
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "weDontRecognizeThisDevice",
|
||||
|
||||
@@ -21,8 +21,8 @@ import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { TwoFactorOptionsComponentV1 } from "../auth/two-factor-options-v1.component";
|
||||
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { SshAgentService } from "../platform/services/ssh-agent.service";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/app/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/app/vault/attachments.component";
|
||||
|
||||
@@ -22,8 +22,8 @@ import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService as KeyServiceAbstraction } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { SshAgentService } from "../../autofill/services/ssh-agent.service";
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { SshAgentService } from "../../platform/services/ssh-agent.service";
|
||||
import { VersionService } from "../../platform/services/version.service";
|
||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil, tap } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
@@ -143,10 +143,11 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
|
||||
if (flag) {
|
||||
const qParams = await firstValueFrom(this.route.queryParams);
|
||||
|
||||
const uniqueQueryParams = {
|
||||
...this.route.queryParams,
|
||||
...qParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(),
|
||||
};
|
||||
@@ -156,7 +157,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
takeUntil(this.componentDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -34,9 +34,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ApproveSshRequestComponent } from "../components/approve-ssh-request";
|
||||
|
||||
import { DesktopSettingsService } from "./desktop-settings.service";
|
||||
import { ApproveSshRequestComponent } from "../../platform/components/approve-ssh-request";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
@@ -130,7 +129,7 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
throw error;
|
||||
}),
|
||||
map(() => message),
|
||||
map(() => [message, account.id]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -916,7 +916,7 @@
|
||||
"message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.",
|
||||
"description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated."
|
||||
},
|
||||
"verifyIdentity": {
|
||||
"verifyYourIdentity": {
|
||||
"message": "Verify your Identity"
|
||||
},
|
||||
"weDontRecognizeThisDevice": {
|
||||
@@ -3604,5 +3604,8 @@
|
||||
},
|
||||
"updateBrowserOrDisableFingerprintDialogMessage": {
|
||||
"message": "The browser extension you are using is out of date. Please update it or disable browser integration fingerprint validation in the desktop app settings."
|
||||
},
|
||||
"changeAtRiskPassword": {
|
||||
"message": "Change at-risk password"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@
|
||||
import { DefaultBiometricStateService } from "@bitwarden/key-management";
|
||||
/* eslint-enable import/no-restricted-paths */
|
||||
|
||||
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
|
||||
import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopBiometricsService } from "./key-management/biometrics/desktop.biometrics.service";
|
||||
import { MainBiometricsIPCListener } from "./key-management/biometrics/main-biometrics-ipc.listener";
|
||||
@@ -45,7 +46,6 @@ import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.mai
|
||||
import { ClipboardMain } from "./platform/main/clipboard.main";
|
||||
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
|
||||
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
|
||||
import { MainSshAgentService } from "./platform/main/main-ssh-agent.service";
|
||||
import { VersionMain } from "./platform/main/version.main";
|
||||
import { DesktopSettingsService } from "./platform/services/desktop-settings.service";
|
||||
import { ElectronLogMainService } from "./platform/services/electron-log.main.service";
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.2.2",
|
||||
"version": "2025.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.2.2",
|
||||
"version": "2025.2.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.2.2",
|
||||
"version": "2025.2.1",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<vault-cipher-form-generator
|
||||
[type]="data.type"
|
||||
(valueGenerated)="onCredentialGenerated($event)"
|
||||
[onAlgorithmSelected]="onAlgorithmSelected"
|
||||
(algorithmSelected)="onAlgorithmSelected($event)"
|
||||
/>
|
||||
<bit-item>
|
||||
<button
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
@@ -44,16 +45,17 @@ export class CredentialGeneratorDialogComponent {
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: CredentialGeneratorParams,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
onAlgorithmSelected = (selected?: AlgorithmInfo) => {
|
||||
if (selected) {
|
||||
this.buttonLabel = selected.useGeneratedValue;
|
||||
} else {
|
||||
// clear the credential value when the user is
|
||||
// selecting the credential generation algorithm
|
||||
this.credentialValue = undefined;
|
||||
// default to email
|
||||
this.buttonLabel = this.i18nService.t("useThisEmail");
|
||||
}
|
||||
this.credentialValue = undefined;
|
||||
};
|
||||
|
||||
applyCredentials = () => {
|
||||
|
||||
@@ -20,7 +20,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { GroupApiService, GroupView } from "../../../admin-console/organizations/core";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { GroupApiService, GroupView } from "../../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
@@ -30,8 +31,7 @@ import {
|
||||
mapGroupToAccessItemView,
|
||||
mapUserToAccessItemView,
|
||||
PermissionMode,
|
||||
} from "../../../admin-console/organizations/shared/components/access-selector";
|
||||
import { SharedModule } from "../../../shared";
|
||||
} from "../../shared/components/access-selector";
|
||||
|
||||
export interface BulkCollectionsDialogParams {
|
||||
organizationId: string;
|
||||
@@ -2,8 +2,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
import { CollectionDialogTabType } from "../components/collection-dialog";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { CollectionDialogTabType } from "../../../vault/components/collection-dialog";
|
||||
|
||||
const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="10 -10 120 140" fill="none">
|
||||
<rect class="tw-stroke-secondary-600" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { PipesModule } from "../../individual-vault/pipes/pipes.module";
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "./collection-name.badge.component";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { PipesModule } from "../../individual-vault/pipes/pipes.module";
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { GroupNameBadgeComponent } from "./group-name-badge.component";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Component, Input, OnChanges } from "@angular/core";
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { GroupView } from "../../core";
|
||||
|
||||
@Component({
|
||||
selector: "app-group-badge",
|
||||
@@ -12,18 +12,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../individual-vault/vault-filter/components/vault-filter.component"; //../../vault/vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import {
|
||||
VaultFilterList,
|
||||
VaultFilterSection,
|
||||
VaultFilterType,
|
||||
} from "../../individual-vault/vault-filter/shared/models/vault-filter-section.type";
|
||||
import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
} from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter-section.type";
|
||||
import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-vault-filter",
|
||||
templateUrl: "../../individual-vault/vault-filter/components/vault-filter.component.html",
|
||||
templateUrl:
|
||||
"../../../../vault/individual-vault/vault-filter/components/vault-filter.component.html",
|
||||
})
|
||||
export class VaultFilterComponent
|
||||
extends BaseVaultFilterComponent
|
||||
@@ -2,8 +2,8 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilterSharedModule } from "../../../../vault/individual-vault/vault-filter/shared/vault-filter-shared.module";
|
||||
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
@@ -11,8 +11,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { VaultFilterService as BaseVaultFilterService } from "../../individual-vault/vault-filter/services/vault-filter.service";
|
||||
import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
import { VaultFilterService as BaseVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/vault-filter.service";
|
||||
import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService extends BaseVaultFilterService implements OnDestroy {
|
||||
@@ -1,7 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// FIXME: rename output bindings and then remove this line
|
||||
/* eslint-disable @angular-eslint/no-output-on-prefix */
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -13,7 +15,6 @@ import {
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
@@ -25,13 +26,13 @@ import {
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { CollectionDialogTabType } from "../../components/collection-dialog";
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { CollectionDialogTabType } from "../../../../vault/components/collection-dialog";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
} from "../../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -47,7 +48,7 @@ import {
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class VaultHeaderComponent implements OnInit {
|
||||
export class VaultHeaderComponent {
|
||||
protected All = All;
|
||||
protected Unassigned = Unassigned;
|
||||
|
||||
@@ -97,11 +98,8 @@ export class VaultHeaderComponent implements OnInit {
|
||||
private dialogService: DialogService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {}
|
||||
|
||||
get title() {
|
||||
const headerType = this.i18nService.t("collections").toLowerCase();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessVaultTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { VaultComponent } from "./vault.component";
|
||||
const routes: Routes = [
|
||||
@@ -76,55 +76,55 @@ import {
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { GroupApiService, GroupView } from "../../admin-console/organizations/core";
|
||||
import { openEntityEventsDialog } from "../../admin-console/organizations/manage/entity-events.component";
|
||||
import {
|
||||
ResellerWarning,
|
||||
ResellerWarningService,
|
||||
} from "../../billing/services/reseller-warning.service";
|
||||
import { TrialFlowService } from "../../billing/services/trial-flow.service";
|
||||
import { FreeTrial } from "../../billing/types/free-trial";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { VaultFilterService } from "../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilter } from "../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
||||
import { AssignCollectionsWebComponent } from "../components/assign-collections";
|
||||
} from "../../../billing/services/reseller-warning.service";
|
||||
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
|
||||
import { FreeTrial } from "../../../billing/types/free-trial";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
|
||||
import {
|
||||
CollectionDialogAction,
|
||||
CollectionDialogTabType,
|
||||
openCollectionDialog,
|
||||
} from "../components/collection-dialog";
|
||||
} from "../../../vault/components/collection-dialog";
|
||||
import {
|
||||
VaultItemDialogComponent,
|
||||
VaultItemDialogMode,
|
||||
VaultItemDialogResult,
|
||||
} from "../components/vault-item-dialog/vault-item-dialog.component";
|
||||
import { VaultItemEvent } from "../components/vault-items/vault-item-event";
|
||||
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
|
||||
} from "../../../vault/components/vault-item-dialog/vault-item-dialog.component";
|
||||
import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event";
|
||||
import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module";
|
||||
import {
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
} from "../individual-vault/attachments-v2.component";
|
||||
} from "../../../vault/individual-vault/attachments-v2.component";
|
||||
import {
|
||||
BulkDeleteDialogResult,
|
||||
openBulkDeleteDialog,
|
||||
} from "../individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
|
||||
import { RoutedVaultFilterBridgeService } from "../individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { createFilterFunction } from "../individual-vault/vault-filter/shared/models/filter-function";
|
||||
} from "../../../vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
|
||||
import { VaultFilterService } from "../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { RoutedVaultFilterBridgeService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
|
||||
import { RoutedVaultFilterService } from "../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
import { createFilterFunction } from "../../../vault/individual-vault/vault-filter/shared/models/filter-function";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component";
|
||||
import { getNestedCollectionTree } from "../utils/collection-utils";
|
||||
} from "../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
import { VaultFilter } from "../../../vault/individual-vault/vault-filter/shared/models/vault-filter.model";
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
import { getNestedCollectionTree } from "../../../vault/utils/collection-utils";
|
||||
import { GroupApiService, GroupView } from "../core";
|
||||
import { openEntityEventsDialog } from "../manage/entity-events.component";
|
||||
|
||||
import {
|
||||
BulkCollectionsDialogComponent,
|
||||
BulkCollectionsDialogResult,
|
||||
} from "./bulk-collections-dialog";
|
||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||
import { AdminConsoleCipherFormConfigService } from "./services/admin-console-cipher-form-config.service";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||
const SearchTextDebounceInterval = 200;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { CollectionDialogModule } from "../components/collection-dialog";
|
||||
import { ViewComponent } from "../individual-vault/view.component";
|
||||
import { LooseComponentsModule } from "../../../shared/loose-components.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { CollectionDialogModule } from "../../../vault/components/collection-dialog";
|
||||
import { OrganizationBadgeModule } from "../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { ViewComponent } from "../../../vault/individual-vault/view.component";
|
||||
|
||||
import { CollectionBadgeModule } from "./collection-badge/collection-badge.module";
|
||||
import { GroupBadgeModule } from "./group-badge/group-badge.module";
|
||||
@@ -14,14 +14,14 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
|
||||
import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component";
|
||||
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
|
||||
import { VaultModule } from "../../vault/org-vault/vault.module";
|
||||
|
||||
import { VaultModule } from "./collections/vault.module";
|
||||
import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard";
|
||||
import { organizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "./guards/org-redirect.guard";
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component";
|
||||
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
||||
import { GroupsComponent } from "./manage/groups.component";
|
||||
|
||||
const routes: Routes = [
|
||||
|
||||
@@ -7,7 +7,9 @@ import { mock } from "jest-mock-extended";
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -15,6 +17,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { ChangeLoginPasswordService, TaskService } from "@bitwarden/vault";
|
||||
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
@@ -52,7 +55,34 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideComponent(EmergencyViewDialogComponent, {
|
||||
remove: {
|
||||
providers: [
|
||||
{ provide: PlatformUtilsService, useValue: PlatformUtilsService },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
useValue: ChangeLoginPasswordService,
|
||||
},
|
||||
{ provide: ConfigService, useValue: ConfigService },
|
||||
],
|
||||
},
|
||||
add: {
|
||||
providers: [
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mock<TaskService>(),
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
useValue: mock<ChangeLoginPasswordService>(),
|
||||
},
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EmergencyViewDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { CipherViewComponent } from "@bitwarden/vault";
|
||||
import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
|
||||
|
||||
@@ -33,6 +33,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
],
|
||||
})
|
||||
export class EmergencyViewDialogComponent {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
interface EnterpriseOrgStatus {
|
||||
isFreeFamilyPolicyEnabled: boolean;
|
||||
@@ -27,19 +26,8 @@ export class FreeFamiliesPolicyService {
|
||||
private policyService: PolicyService,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
canManageSponsorships$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => {
|
||||
if (account?.id) {
|
||||
return this.organizationService.canManageSponsorships$(account?.id);
|
||||
} else {
|
||||
return of();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
organizations$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) => {
|
||||
if (account?.id) {
|
||||
@@ -57,22 +45,21 @@ export class FreeFamiliesPolicyService {
|
||||
private getFreeFamiliesVisibility$(): Observable<boolean> {
|
||||
return combineLatest([
|
||||
this.checkEnterpriseOrganizationsAndFetchPolicy(),
|
||||
this.canManageSponsorships$,
|
||||
this.organizations$,
|
||||
]).pipe(
|
||||
map(([orgStatus, canManageSponsorships]) =>
|
||||
this.shouldShowFreeFamilyLink(orgStatus, canManageSponsorships),
|
||||
),
|
||||
map(([orgStatus, organizations]) => this.shouldShowFreeFamilyLink(orgStatus, organizations)),
|
||||
);
|
||||
}
|
||||
|
||||
private shouldShowFreeFamilyLink(
|
||||
orgStatus: EnterpriseOrgStatus | null,
|
||||
canManageSponsorships: boolean,
|
||||
organizations: Organization[],
|
||||
): boolean {
|
||||
if (!orgStatus) {
|
||||
return false;
|
||||
}
|
||||
const { belongToOneEnterpriseOrgs, isFreeFamilyPolicyEnabled } = orgStatus;
|
||||
const canManageSponsorships = organizations.filter((org) => org.canManageSponsorships);
|
||||
return canManageSponsorships && !(belongToOneEnterpriseOrgs && isFreeFamilyPolicyEnabled);
|
||||
}
|
||||
|
||||
|
||||
@@ -522,7 +522,7 @@ const routes: Routes = [
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyIdentity",
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
@@ -542,7 +542,7 @@ const routes: Routes = [
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyIdentity",
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
@@ -619,7 +619,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
pageIcon: DeviceVerificationIcon,
|
||||
pageTitle: {
|
||||
key: "verifyIdentity",
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "weDontRecognizeThisDevice",
|
||||
|
||||
@@ -67,7 +67,6 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f
|
||||
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../tools/reports/pages/organizations/unsecured-websites-report.component";
|
||||
import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../tools/reports/pages/organizations/weak-passwords-report.component";
|
||||
/* eslint no-restricted-imports: "error" */
|
||||
import { AddEditComponent as SendAddEditComponent } from "../tools/send/add-edit.component";
|
||||
import { PremiumBadgeComponent } from "../vault/components/premium-badge.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/individual-vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/individual-vault/add-edit.component";
|
||||
@@ -148,7 +147,6 @@ import { SharedModule } from "./shared.module";
|
||||
SecurityComponent,
|
||||
SecurityKeysComponent,
|
||||
SelectableAvatarComponent,
|
||||
SendAddEditComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
@@ -212,7 +210,6 @@ import { SharedModule } from "./shared.module";
|
||||
SecurityComponent,
|
||||
SecurityKeysComponent,
|
||||
SelectableAvatarComponent,
|
||||
SendAddEditComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
<form
|
||||
[formGroup]="formGroup"
|
||||
[bitSubmit]="submitAndClose"
|
||||
[appApiAction]="formPromise"
|
||||
autocomplete="off"
|
||||
>
|
||||
<bit-dialog dialogSize="large">
|
||||
<span bitDialogTitle>
|
||||
{{ title }}
|
||||
</span>
|
||||
<span bitDialogContent *ngIf="send">
|
||||
<bit-callout *ngIf="disableSend">
|
||||
{{ "sendDisabledWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<bit-callout *ngIf="!disableSend && disableHideEmail">
|
||||
{{ "sendOptionsPolicyInEffect" | i18n }}
|
||||
<ul class="tw-mb-0">
|
||||
<li>{{ "sendDisableHideEmailInEffect" | i18n }}</li>
|
||||
</ul>
|
||||
</bit-callout>
|
||||
<bit-form-field class="tw-w-1/2">
|
||||
<bit-label for="name">{{ "name" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="name" />
|
||||
<bit-hint>{{ "sendNameDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<div class="tw-flex" *ngIf="!editMode">
|
||||
<bit-radio-group formControlName="type">
|
||||
<bit-label>{{ "whatTypeOfSend" | i18n }}</bit-label>
|
||||
|
||||
<bit-radio-button
|
||||
*ngFor="let o of typeOptions"
|
||||
id="type_{{ o.value }}"
|
||||
class="tw-block"
|
||||
[value]="o.value"
|
||||
[disabled]="!canAccessPremium && o.premium"
|
||||
>
|
||||
<bit-label>
|
||||
{{ o.name }}
|
||||
<app-premium-badge
|
||||
class="tw-mx-1"
|
||||
*ngIf="!canAccessPremium && o.premium"
|
||||
slot="end"
|
||||
></app-premium-badge>
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<!-- Text -->
|
||||
<ng-container *ngIf="type === sendType.Text">
|
||||
<bit-form-field>
|
||||
<bit-label for="text">{{ "sendTypeText" | i18n }}</bit-label>
|
||||
<textarea bitInput id="text" rows="6" formControlName="text"></textarea>
|
||||
<bit-hint>{{ "sendTextDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox type="checkbox" formControlName="textHidden" />
|
||||
<bit-label>{{ "textHiddenByDefault" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</ng-container>
|
||||
<!-- File -->
|
||||
<ng-container *ngIf="type === sendType.File">
|
||||
<div class="tw-flex">
|
||||
<div *ngIf="editMode">
|
||||
<bit-label>{{ "file" | i18n }}</bit-label>
|
||||
<p bitTypography="body1" class="tw-mb-0">
|
||||
{{ send.file.fileName }} ({{ send.file.sizeName }})
|
||||
</p>
|
||||
</div>
|
||||
<bit-form-field *ngIf="!editMode">
|
||||
<bit-label>{{ "file" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
id="file"
|
||||
name="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($event)"
|
||||
hidden
|
||||
class="tw-hidden"
|
||||
/>
|
||||
<bit-hint>{{ "sendFileDesc" | i18n }} {{ "maxFileSize" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h4 bitTypography="h4" class="tw-mt-5">{{ "share" | i18n }}</h4>
|
||||
|
||||
<bit-form-field *ngIf="link">
|
||||
<bit-label for="link">{{ "sendLinkLabel" | i18n }}</bit-label>
|
||||
<input bitInput type="text" readonly formControlName="link" />
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<input bitCheckbox type="checkbox" formControlName="copyLink" />
|
||||
<bit-label>{{ "copySendLinkOnSave" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<div class="tw-mt-5 tw-flex" (click)="toggleOptions()">
|
||||
<h4 bitTypography="h4" class="tw-mb-0 tw-mr-2">
|
||||
<button type="button" bitLink appStopClick [attr.aria-expanded]="showOptions">
|
||||
<i
|
||||
class="bwi"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
|
||||
></i>
|
||||
{{ "options" | i18n }}
|
||||
</button>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="options" [hidden]="!showOptions">
|
||||
<div class="tw-flex">
|
||||
<div *ngIf="!editMode" class="tw-w-1/2 tw-pr-3">
|
||||
<bit-form-field>
|
||||
<bit-label for="deletionDate">{{ "deletionDate" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
id="deletionDate"
|
||||
name="SelectedDeletionDatePreset"
|
||||
formControlName="selectedDeletionDatePreset"
|
||||
>
|
||||
<bit-option
|
||||
*ngFor="let o of deletionDatePresets"
|
||||
[value]="o.value"
|
||||
[label]="o.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
<ng-container *ngIf="formGroup.controls['selectedDeletionDatePreset'].value === 0">
|
||||
<input
|
||||
bitInput
|
||||
id="deletionDateCustom"
|
||||
type="datetime-local"
|
||||
name="DeletionDate"
|
||||
formControlName="defaultDeletionDateTime"
|
||||
placeholder="MM/DD/YYYY HH:MM AM/PM"
|
||||
/>
|
||||
</ng-container>
|
||||
<bit-hint>{{ "deletionDateDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div *ngIf="editMode" class="tw-w-1/2 tw-pr-3">
|
||||
<bit-form-field>
|
||||
<bit-label for="deletionDate">{{ "deletionDate" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
id="deletionDateCustom"
|
||||
type="datetime-local"
|
||||
name="DeletionDate"
|
||||
formControlName="defaultDeletionDateTime"
|
||||
placeholder="MM/DD/YYYY HH:MM AM/PM"
|
||||
/>
|
||||
<bit-hint>{{ "deletionDateDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div *ngIf="!editMode" class="tw-w-1/2 tw-pl-3">
|
||||
<bit-form-field>
|
||||
<bit-label for="expirationDate">
|
||||
{{ "expirationDate" | i18n }}
|
||||
</bit-label>
|
||||
<bit-select
|
||||
bitInput
|
||||
id="expirationDate"
|
||||
name="SelectedExpirationDatePreset"
|
||||
formControlName="selectedExpirationDatePreset"
|
||||
>
|
||||
<bit-option
|
||||
*ngFor="let e of expirationDatePresets"
|
||||
[value]="e.value"
|
||||
[label]="e.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
<ng-container *ngIf="formGroup.controls['selectedExpirationDatePreset'].value === 0">
|
||||
<input
|
||||
bitInput
|
||||
id="expirationDateCustom"
|
||||
type="datetime-local"
|
||||
name="ExpirationDate"
|
||||
formControlName="defaultExpirationDateTime"
|
||||
placeholder="MM/DD/YYYY HH:MM AM/PM"
|
||||
/>
|
||||
</ng-container>
|
||||
<bit-hint>{{ "expirationDateDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div *ngIf="editMode" class="tw-w-1/2 tw-pl-3">
|
||||
<bit-form-field>
|
||||
<bit-label class="tw-flex" for="expirationDate">
|
||||
{{ "expirationDate" | i18n }}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitLink
|
||||
appStopClick
|
||||
(click)="clearExpiration()"
|
||||
*ngIf="!disableSend"
|
||||
class="tw-ml-auto"
|
||||
slot="end"
|
||||
>
|
||||
{{ "clear" | i18n }}
|
||||
</button>
|
||||
</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
id="expirationDateCustom"
|
||||
type="datetime-local"
|
||||
name="ExpirationDate"
|
||||
formControlName="defaultExpirationDateTime"
|
||||
placeholder="MM/DD/YYYY HH:MM AM/PM"
|
||||
/>
|
||||
<bit-hint>{{ "expirationDateDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-flex">
|
||||
<bit-form-field class="tw-w-1/2 tw-pr-3">
|
||||
<bit-label for="maxAccessCount">{{ "maxAccessCount" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="maxAccessCount" min="1" />
|
||||
<bit-hint>{{ "maxAccessCountDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-w-1/2 tw-pl-3" *ngIf="editMode">
|
||||
<bit-label for="accessCount">{{ "currentAccessCount" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="accessCount" readonly />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-flex">
|
||||
<bit-form-field class="tw-w-1/2 tw-pr-3">
|
||||
<bit-label for="password" *ngIf="!hasPassword">{{ "password" | i18n }}</bit-label>
|
||||
<bit-label for="password" *ngIf="hasPassword">{{ "newPassword" | i18n }}</bit-label>
|
||||
|
||||
<input bitInput type="password" formControlName="password" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
<bit-hint>{{ "sendPasswordDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "notes" | i18n }}</bit-label>
|
||||
<textarea bitInput formControlName="notes" rows="6"></textarea>
|
||||
<bit-hint>{{ "sendNotesDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<input bitCheckbox type="checkbox" formControlName="hideEmail" />
|
||||
<bit-label>{{ "hideEmail" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<bit-form-control>
|
||||
<input bitCheckbox type="checkbox" formControlName="disabled" />
|
||||
<bit-label>{{ "disableThisSend" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</span>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
type="submit"
|
||||
bitButton
|
||||
bitFormButton
|
||||
[appA11yTitle]="'save' | i18n"
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
[appA11yTitle]="'cancel' | i18n"
|
||||
bitDialogClose
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="editMode"
|
||||
type="button"
|
||||
class="tw-ml-auto"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="deleteAndClose"
|
||||
></button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -1,102 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-send-add-edit",
|
||||
templateUrl: "add-edit.component.html",
|
||||
})
|
||||
export class AddEditComponent extends BaseAddEditComponent {
|
||||
override componentName = "app-send-add-edit";
|
||||
protected selectedFile: File;
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
datePipe: DatePipe,
|
||||
sendService: SendService,
|
||||
stateService: StateService,
|
||||
messagingService: MessagingService,
|
||||
policyService: PolicyService,
|
||||
logService: LogService,
|
||||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
formBuilder: FormBuilder,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) params: { sendId: string },
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
environmentService,
|
||||
datePipe,
|
||||
sendService,
|
||||
messagingService,
|
||||
policyService,
|
||||
logService,
|
||||
stateService,
|
||||
sendApiService,
|
||||
dialogService,
|
||||
formBuilder,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
|
||||
this.sendId = params.sendId;
|
||||
}
|
||||
|
||||
async copyLinkToClipboard(link: string): Promise<void | boolean> {
|
||||
// Copy function on web depends on the modal being open or not. Since this event occurs during a transition
|
||||
// of the modal closing we need to add a small delay to make sure state of the DOM is consistent.
|
||||
return new Promise((resolve) => {
|
||||
window.setTimeout(() => resolve(super.copyLinkToClipboard(link)), 500);
|
||||
});
|
||||
}
|
||||
|
||||
protected setSelectedFile(event: Event) {
|
||||
const fileInputEl = <HTMLInputElement>event.target;
|
||||
const file = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
|
||||
this.selectedFile = file;
|
||||
}
|
||||
|
||||
submitAndClose = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await this.submit();
|
||||
if (success) {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
};
|
||||
|
||||
deleteAndClose = async () => {
|
||||
const success = await this.delete();
|
||||
if (success) {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<button bitButton [bitMenuTriggerFor]="itemOptions" buttonType="primary" type="button">
|
||||
<i *ngIf="!hideIcon" class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
{{ (hideIcon ? "createSend" : "new") | i18n }}
|
||||
</button>
|
||||
<bit-menu #itemOptions>
|
||||
<a bitMenuItem (click)="createSend(sendType.Text)">
|
||||
<i class="bwi bwi-file-text" slot="start" aria-hidden="true"></i>
|
||||
{{ "sendTypeText" | i18n }}
|
||||
</a>
|
||||
<a bitMenuItem (click)="createSend(sendType.File)">
|
||||
<i class="bwi bwi-file" slot="start" aria-hidden="true"></i>
|
||||
{{ "sendTypeFile" | i18n }}
|
||||
<button
|
||||
type="button"
|
||||
slot="end"
|
||||
*ngIf="!(canAccessPremium$ | async)"
|
||||
bitBadge
|
||||
variant="success"
|
||||
>
|
||||
{{ "premium" | i18n }}
|
||||
</button>
|
||||
</a>
|
||||
</bit-menu>
|
||||
@@ -0,0 +1,63 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { BadgeModule, ButtonModule, DialogService, MenuModule } from "@bitwarden/components";
|
||||
import { DefaultSendFormConfigService, SendAddEditDialogComponent } from "@bitwarden/send-ui";
|
||||
|
||||
@Component({
|
||||
selector: "tools-new-send-dropdown",
|
||||
templateUrl: "new-send-dropdown.component.html",
|
||||
standalone: true,
|
||||
imports: [JslibModule, CommonModule, ButtonModule, MenuModule, BadgeModule],
|
||||
providers: [DefaultSendFormConfigService],
|
||||
})
|
||||
/**
|
||||
* A dropdown component that allows the user to create a new Send of a specific type.
|
||||
*/
|
||||
export class NewSendDropdownComponent {
|
||||
/** If true, the plus icon will be hidden */
|
||||
@Input() hideIcon: boolean = false;
|
||||
|
||||
/** SendType provided for the markup to pass back the selected type of Send */
|
||||
protected sendType = SendType;
|
||||
|
||||
/** Indicates whether the user can access premium features. */
|
||||
protected canAccessPremium$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private accountService: AccountService,
|
||||
private dialogService: DialogService,
|
||||
private addEditFormConfigService: DefaultSendFormConfigService,
|
||||
) {
|
||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
|
||||
: of(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the SendAddEditComponent for a new Send with the provided type.
|
||||
* If has user does not have premium access and the type is File, the user will be redirected to the premium settings page.
|
||||
* @param type The type of Send to create.
|
||||
*/
|
||||
async createSend(type: SendType) {
|
||||
if (!(await firstValueFrom(this.canAccessPremium$)) && type === SendType.File) {
|
||||
return await this.router.navigate(["settings/subscription/premium"]);
|
||||
}
|
||||
|
||||
const formConfig = await this.addEditFormConfigService.buildConfig("add", undefined, type);
|
||||
|
||||
await SendAddEditDialogComponent.open(this.dialogService, { formConfig });
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,7 @@
|
||||
</ng-container>
|
||||
</small>
|
||||
</ng-container>
|
||||
|
||||
<button type="button" bitButton buttonType="primary" (click)="addSend()" [disabled]="disableSend">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "createSend" | i18n }}
|
||||
</button>
|
||||
<tools-new-send-dropdown *ngIf="!disableSend"></tools-new-send-dropdown>
|
||||
</app-header>
|
||||
|
||||
<bit-callout type="warning" title="{{ 'sendDisabled' | i18n }}" *ngIf="disableSend">
|
||||
@@ -198,10 +194,11 @@
|
||||
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
|
||||
<ng-container slot="title">{{ "sendsNoItemsTitle" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "sendsNoItemsMessage" | i18n }}</ng-container>
|
||||
<button slot="button" type="button" bitButton buttonType="secondary" (click)="addSend()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "createSend" | i18n }}
|
||||
</button>
|
||||
<tools-new-send-dropdown
|
||||
[hideIcon]="true"
|
||||
*ngIf="!disableSend"
|
||||
slot="button"
|
||||
></tools-new-send-dropdown>
|
||||
</bit-no-items>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone, ViewChild, OnInit, OnDestroy, ViewContainerRef } from "@angular/core";
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, NgZone, OnInit, OnDestroy } from "@angular/core";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
@@ -14,6 +15,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SendId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
DialogService,
|
||||
NoItemsModule,
|
||||
@@ -21,24 +23,30 @@ import {
|
||||
TableDataSource,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { NoSendsIcon } from "@bitwarden/send-ui";
|
||||
import {
|
||||
DefaultSendFormConfigService,
|
||||
NoSendsIcon,
|
||||
SendFormConfig,
|
||||
SendAddEditDialogComponent,
|
||||
SendItemDialogResult,
|
||||
} from "@bitwarden/send-ui";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { NewSendDropdownComponent } from "./new-send/new-send-dropdown.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "SendComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-send",
|
||||
standalone: true,
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule],
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule, NewSendDropdownComponent],
|
||||
templateUrl: "send.component.html",
|
||||
providers: [DefaultSendFormConfigService],
|
||||
})
|
||||
export class SendComponent extends BaseSendComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("sendAddEdit", { read: ViewContainerRef, static: true })
|
||||
sendAddEditModalRef: ViewContainerRef;
|
||||
private sendItemDialogRef?: DialogRef<SendItemDialogResult> | undefined;
|
||||
noItemIcon = NoSendsIcon;
|
||||
|
||||
override set filteredSends(filteredSends: SendView[]) {
|
||||
@@ -65,6 +73,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
toastService: ToastService,
|
||||
private addEditFormConfigService: DefaultSendFormConfigService,
|
||||
) {
|
||||
super(
|
||||
sendService,
|
||||
@@ -111,17 +120,41 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
return;
|
||||
}
|
||||
|
||||
await this.editSend(null);
|
||||
const config = await this.addEditFormConfigService.buildConfig("add", null, 0);
|
||||
|
||||
await this.openSendItemDialog(config);
|
||||
}
|
||||
|
||||
async editSend(send: SendView) {
|
||||
const dialog = this.dialogService.open(AddEditComponent, {
|
||||
data: {
|
||||
sendId: send == null ? null : send.id,
|
||||
},
|
||||
const config = await this.addEditFormConfigService.buildConfig(
|
||||
send == null ? "add" : "edit",
|
||||
send == null ? null : (send.id as SendId),
|
||||
send.type,
|
||||
);
|
||||
|
||||
await this.openSendItemDialog(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the send item dialog.
|
||||
* @param formConfig The form configuration.
|
||||
* */
|
||||
async openSendItemDialog(formConfig: SendFormConfig) {
|
||||
// Prevent multiple dialogs from being opened.
|
||||
if (this.sendItemDialogRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendItemDialogRef = SendAddEditDialogComponent.open(this.dialogService, {
|
||||
formConfig,
|
||||
});
|
||||
|
||||
await lastValueFrom(dialog.closed);
|
||||
await this.load();
|
||||
const result = await lastValueFrom(this.sendItemDialogRef.closed);
|
||||
this.sendItemDialogRef = undefined;
|
||||
|
||||
// If the dialog was closed by deleting the cipher, refresh the vault.
|
||||
if (result === SendItemDialogResult.Deleted || result === SendItemDialogResult.Saved) {
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherAttachmentsComponent,
|
||||
CipherFormComponent,
|
||||
CipherFormConfig,
|
||||
@@ -43,6 +44,9 @@ import {
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
@@ -136,6 +140,8 @@ export enum VaultItemDialogResult {
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService },
|
||||
RoutedVaultFilterService,
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
export class VaultItemDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -5,11 +5,11 @@ import { RouterModule } from "@angular/router";
|
||||
|
||||
import { TableModule } from "@bitwarden/components";
|
||||
|
||||
import { CollectionBadgeModule } from "../../../admin-console/organizations/collections/collection-badge/collection-badge.module";
|
||||
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { OrganizationBadgeModule } from "../../individual-vault/organization-badge/organization-badge.module";
|
||||
import { PipesModule } from "../../individual-vault/pipes/pipes.module";
|
||||
import { CollectionBadgeModule } from "../../org-vault/collection-badge/collection-badge.module";
|
||||
import { GroupBadgeModule } from "../../org-vault/group-badge/group-badge.module";
|
||||
|
||||
import { VaultCipherRowComponent } from "./vault-cipher-row.component";
|
||||
import { VaultCollectionRowComponent } from "./vault-collection-row.component";
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<bit-dialog dialogSize="default" background="alt">
|
||||
<span bitDialogTitle>
|
||||
{{ title }}
|
||||
{{ titleKey | i18n }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<vault-cipher-form-generator
|
||||
[type]="params.type"
|
||||
[uri]="uri"
|
||||
(valueGenerated)="onValueGenerated($event)"
|
||||
(algorithmSelected)="onAlgorithmSelected($event)"
|
||||
disableMargin
|
||||
></vault-cipher-form-generator>
|
||||
</ng-container>
|
||||
@@ -17,8 +18,9 @@
|
||||
buttonType="primary"
|
||||
(click)="selectValue()"
|
||||
data-testid="select-button"
|
||||
[disabled]="!(buttonLabel && generatedValue)"
|
||||
>
|
||||
{{ selectButtonText }}
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Input, Output, EventEmitter } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
import {
|
||||
WebVaultGeneratorDialogAction,
|
||||
WebVaultGeneratorDialogComponent,
|
||||
WebVaultGeneratorDialogParams,
|
||||
WebVaultGeneratorDialogResult,
|
||||
} from "./web-generator-dialog.component";
|
||||
|
||||
@Component({
|
||||
@@ -22,7 +22,8 @@ import {
|
||||
standalone: true,
|
||||
})
|
||||
class MockCipherFormGenerator {
|
||||
@Input() type: "password" | "username";
|
||||
@Input() type: "password" | "username" = "password";
|
||||
@Output() algorithmSelected: EventEmitter<AlgorithmInfo> = new EventEmitter();
|
||||
@Input() uri?: string;
|
||||
@Output() valueGenerated = new EventEmitter<string>();
|
||||
}
|
||||
@@ -30,35 +31,20 @@ class MockCipherFormGenerator {
|
||||
describe("WebVaultGeneratorDialogComponent", () => {
|
||||
let component: WebVaultGeneratorDialogComponent;
|
||||
let fixture: ComponentFixture<WebVaultGeneratorDialogComponent>;
|
||||
|
||||
let dialogRef: MockProxy<DialogRef<any>>;
|
||||
let dialogRef: MockProxy<DialogRef<WebVaultGeneratorDialogResult>>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRef = mock<DialogRef<any>>();
|
||||
dialogRef = mock<DialogRef<WebVaultGeneratorDialogResult>>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
const mockDialogData: WebVaultGeneratorDialogParams = { type: "password" };
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, WebVaultGeneratorDialogComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: DialogRef,
|
||||
useValue: dialogRef,
|
||||
},
|
||||
{
|
||||
provide: DIALOG_DATA,
|
||||
useValue: mockDialogData,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mockI18nService,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: mock<PlatformUtilsService>(),
|
||||
},
|
||||
{ provide: DialogRef, useValue: dialogRef },
|
||||
{ provide: DIALOG_DATA, useValue: { type: "password" } },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(WebVaultGeneratorDialogComponent, {
|
||||
@@ -72,38 +58,73 @@ describe("WebVaultGeneratorDialogComponent", () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("initializes without errors", () => {
|
||||
fixture.detectChanges();
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("closes the dialog with 'canceled' result when close is called", () => {
|
||||
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||
it("should enable button when value and algorithm are selected", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
(component as any).close();
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledWith({
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("should disable the button if no value has been generated", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable the button if no algorithm is selected", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
const button = fixture.debugElement.query(
|
||||
By.css("[data-testid='select-button']"),
|
||||
).nativeElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should close with selected value when confirmed", () => {
|
||||
const generator = fixture.debugElement.query(
|
||||
By.css("vault-cipher-form-generator"),
|
||||
).componentInstance;
|
||||
generator.algorithmSelected.emit({ useGeneratedValue: "Use Password" } as any);
|
||||
generator.valueGenerated.emit("test-password");
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.debugElement.query(By.css("[data-testid='select-button']")).nativeElement.click();
|
||||
|
||||
expect(dialogRef.close).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Selected,
|
||||
generatedValue: "test-password",
|
||||
});
|
||||
});
|
||||
|
||||
it("should close with canceled action when dismissed", () => {
|
||||
component["close"]();
|
||||
expect(dialogRef.close).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Canceled,
|
||||
});
|
||||
});
|
||||
|
||||
it("closes the dialog with 'selected' result when selectValue is called", () => {
|
||||
const closeSpy = jest.spyOn(dialogRef, "close");
|
||||
const generatedValue = "generated-value";
|
||||
component.onValueGenerated(generatedValue);
|
||||
|
||||
(component as any).selectValue();
|
||||
|
||||
expect(closeSpy).toHaveBeenCalledWith({
|
||||
action: WebVaultGeneratorDialogAction.Selected,
|
||||
generatedValue: generatedValue,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates generatedValue when onValueGenerated is called", () => {
|
||||
const generatedValue = "new-generated-value";
|
||||
component.onValueGenerated(generatedValue);
|
||||
|
||||
expect((component as any).generatedValue).toBe(generatedValue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import { AlgorithmInfo } from "@bitwarden/generator-core";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { CipherFormGeneratorComponent } from "@bitwarden/vault";
|
||||
|
||||
export interface WebVaultGeneratorDialogParams {
|
||||
@@ -27,13 +29,11 @@ export enum WebVaultGeneratorDialogAction {
|
||||
selector: "web-vault-generator-dialog",
|
||||
templateUrl: "./web-generator-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule],
|
||||
imports: [CommonModule, CipherFormGeneratorComponent, ButtonModule, DialogModule, I18nPipe],
|
||||
})
|
||||
export class WebVaultGeneratorDialogComponent {
|
||||
protected title = this.i18nService.t(this.isPassword ? "passwordGenerator" : "usernameGenerator");
|
||||
protected selectButtonText = this.i18nService.t(
|
||||
this.isPassword ? "useThisPassword" : "useThisUsername",
|
||||
);
|
||||
protected titleKey = this.isPassword ? "passwordGenerator" : "usernameGenerator";
|
||||
protected buttonLabel: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether the dialog is generating a password/passphrase. If false, it is generating a username.
|
||||
@@ -80,6 +80,16 @@ export class WebVaultGeneratorDialogComponent {
|
||||
this.generatedValue = value;
|
||||
}
|
||||
|
||||
onAlgorithmSelected = (selected?: AlgorithmInfo) => {
|
||||
if (selected) {
|
||||
this.buttonLabel = selected.useGeneratedValue;
|
||||
} else {
|
||||
// default to email
|
||||
this.buttonLabel = this.i18nService.t("useThisEmail");
|
||||
}
|
||||
this.generatedValue = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the vault generator dialog.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { CollectionBadgeModule } from "../../admin-console/organizations/collections/collection-badge/collection-badge.module";
|
||||
import { GroupBadgeModule } from "../../admin-console/organizations/collections/group-badge/group-badge.module";
|
||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||
import { CollectionDialogModule } from "../components/collection-dialog";
|
||||
import { CollectionBadgeModule } from "../org-vault/collection-badge/collection-badge.module";
|
||||
import { GroupBadgeModule } from "../org-vault/group-badge/group-badge.module";
|
||||
|
||||
import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module";
|
||||
import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module";
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -21,6 +22,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { ChangeLoginPasswordService, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
|
||||
import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component";
|
||||
|
||||
@@ -82,7 +84,33 @@ describe("ViewComponent", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideComponent(ViewComponent, {
|
||||
remove: {
|
||||
providers: [
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: PlatformUtilsService, useValue: PlatformUtilsService },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
useValue: ChangeLoginPasswordService,
|
||||
},
|
||||
],
|
||||
},
|
||||
add: {
|
||||
providers: [
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mock<TaskService>(),
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
useValue: mock<ChangeLoginPasswordService>(),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { CipherViewComponent } from "@bitwarden/vault";
|
||||
import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
|
||||
@@ -74,6 +74,7 @@ export interface ViewCipherDialogCloseResult {
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
],
|
||||
})
|
||||
export class ViewComponent implements OnInit {
|
||||
|
||||
@@ -82,8 +82,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const titleForSmallerScreens = document.getElementById("title-smaller-screens");
|
||||
const titleForLargerScreens = document.getElementById("title-larger-screens");
|
||||
|
||||
titleForSmallerScreens.innerText = localeService.t("verifyIdentity");
|
||||
titleForLargerScreens.innerText = localeService.t("verifyIdentity");
|
||||
titleForSmallerScreens.innerText = localeService.t("verifyYourIdentity");
|
||||
titleForLargerScreens.innerText = localeService.t("verifyYourIdentity");
|
||||
|
||||
const subtitle = document.getElementById("subtitle");
|
||||
subtitle.innerText = localeService.t("followTheStepsBelowToFinishLoggingIn");
|
||||
|
||||
@@ -222,6 +222,9 @@
|
||||
"notes": {
|
||||
"message": "Notes"
|
||||
},
|
||||
"privateNote": {
|
||||
"message": "Private note"
|
||||
},
|
||||
"note": {
|
||||
"message": "Note"
|
||||
},
|
||||
@@ -1200,7 +1203,7 @@
|
||||
"authenticationSessionTimedOut": {
|
||||
"message": "The authentication session timed out. Please restart the login process."
|
||||
},
|
||||
"verifyIdentity": {
|
||||
"verifyYourIdentity": {
|
||||
"message": "Verify your Identity"
|
||||
},
|
||||
"weDontRecognizeThisDevice": {
|
||||
@@ -5105,12 +5108,40 @@
|
||||
"requireSsoExemption": {
|
||||
"message": "Organization owners and admins are exempt from this policy's enforcement."
|
||||
},
|
||||
"limitSendViews": {
|
||||
"message": "Limit views"
|
||||
},
|
||||
"limitSendViewsHint": {
|
||||
"message": "No one can view this Send after the limit is reached.",
|
||||
"description": "Displayed under the limit views field on Send"
|
||||
},
|
||||
"limitSendViewsCount": {
|
||||
"message": "$ACCESSCOUNT$ views left",
|
||||
"description": "Displayed under the limit views field on Send",
|
||||
"placeholders": {
|
||||
"accessCount": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sendDetails": {
|
||||
"message": "Send details",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendTypeTextToShare": {
|
||||
"message": "Text to share"
|
||||
},
|
||||
"sendTypeFile": {
|
||||
"message": "File"
|
||||
},
|
||||
"sendTypeText": {
|
||||
"message": "Text"
|
||||
},
|
||||
"sendPasswordDescV3": {
|
||||
"message": "Add an optional password for recipients to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"createSend": {
|
||||
"message": "New Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
@@ -5135,19 +5166,15 @@
|
||||
"message": "Delete Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deleteSendConfirmation": {
|
||||
"message": "Are you sure you want to delete this Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"whatTypeOfSend": {
|
||||
"message": "What type of Send is this?",
|
||||
"deleteSendPermanentConfirmation": {
|
||||
"message": "Are you sure you want to permanently delete this Send?",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"deletionDate": {
|
||||
"message": "Deletion date"
|
||||
},
|
||||
"deletionDateDesc": {
|
||||
"message": "The Send will be permanently deleted on the specified date and time.",
|
||||
"deletionDateDescV2": {
|
||||
"message": "The Send will be permanently deleted on this date.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"expirationDate": {
|
||||
@@ -5160,21 +5187,6 @@
|
||||
"maxAccessCount": {
|
||||
"message": "Maximum access count"
|
||||
},
|
||||
"maxAccessCountDesc": {
|
||||
"message": "If set, users will no longer be able to access this Send once the maximum access count is reached.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"currentAccessCount": {
|
||||
"message": "Current access count"
|
||||
},
|
||||
"sendPasswordDesc": {
|
||||
"message": "Optionally require a password for users to access this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendNotesDesc": {
|
||||
"message": "Private notes about this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"disabled": {
|
||||
"message": "Disabled"
|
||||
},
|
||||
@@ -5201,13 +5213,6 @@
|
||||
"removePasswordConfirmation": {
|
||||
"message": "Are you sure you want to remove the password?"
|
||||
},
|
||||
"hideEmail": {
|
||||
"message": "Hide my email address from recipients."
|
||||
},
|
||||
"disableThisSend": {
|
||||
"message": "Deactivate this Send so that no one can access it.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"allSends": {
|
||||
"message": "All Sends"
|
||||
},
|
||||
@@ -5218,6 +5223,9 @@
|
||||
"pendingDeletion": {
|
||||
"message": "Pending deletion"
|
||||
},
|
||||
"hideTextByDefault": {
|
||||
"message": "Hide text by default"
|
||||
},
|
||||
"expired": {
|
||||
"message": "Expired"
|
||||
},
|
||||
@@ -5439,13 +5447,6 @@
|
||||
"message": "Always show member’s email address with recipients when creating or editing a Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendOptionsPolicyInEffect": {
|
||||
"message": "The following organization policies are currently in effect:"
|
||||
},
|
||||
"sendDisableHideEmailInEffect": {
|
||||
"message": "Users are not allowed to hide their email address from recipients when creating or editing a Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"modifiedPolicyId": {
|
||||
"message": "Modified policy $ID$.",
|
||||
"placeholders": {
|
||||
@@ -5545,27 +5546,6 @@
|
||||
"personalOwnershipCheckboxDesc": {
|
||||
"message": "Remove individual ownership for organization users"
|
||||
},
|
||||
"textHiddenByDefault": {
|
||||
"message": "When accessing the Send, hide the text by default",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendNameDesc": {
|
||||
"message": "A friendly name to describe this Send.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendTextDesc": {
|
||||
"message": "The text you want to Send."
|
||||
},
|
||||
"sendFileDesc": {
|
||||
"message": "The file you want to Send."
|
||||
},
|
||||
"copySendLinkOnSave": {
|
||||
"message": "Copy the link to share this Send to my clipboard upon save."
|
||||
},
|
||||
"sendLinkLabel": {
|
||||
"message": "Send link",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"send": {
|
||||
"message": "Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
@@ -5714,6 +5694,9 @@
|
||||
"dateParsingError": {
|
||||
"message": "There was an error saving your deletion and expiration dates."
|
||||
},
|
||||
"hideYourEmail": {
|
||||
"message": "Hide your email address from viewers."
|
||||
},
|
||||
"webAuthnFallbackMsg": {
|
||||
"message": "To verify your 2FA please click the button below."
|
||||
},
|
||||
@@ -9875,9 +9858,15 @@
|
||||
"learnMoreAboutApi": {
|
||||
"message": "Learn more about Bitwarden's API"
|
||||
},
|
||||
"fileSend": {
|
||||
"message": "File Send"
|
||||
},
|
||||
"fileSends": {
|
||||
"message": "File Sends"
|
||||
},
|
||||
"textSend": {
|
||||
"message": "Text Send"
|
||||
},
|
||||
"textSends": {
|
||||
"message": "Text Sends"
|
||||
},
|
||||
@@ -10514,6 +10503,9 @@
|
||||
"assignedExceedsAvailable": {
|
||||
"message": "Assigned seats exceed available seats."
|
||||
},
|
||||
"changeAtRiskPassword": {
|
||||
"message": "Change at-risk password"
|
||||
},
|
||||
"removeUnlockWithPinPolicyTitle": {
|
||||
"message": "Remove Unlock with PIN"
|
||||
},
|
||||
|
||||
@@ -35,7 +35,12 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<bit-table *ngIf="!(isLoading$ | async)" [dataSource]="dataSource" class="tw-mt-2">
|
||||
<bit-table-scroll
|
||||
*ngIf="!(isLoading$ | async)"
|
||||
[dataSource]="dataSource"
|
||||
[rowSize]="53"
|
||||
class="tw-table tw-w-full table-hover table-list"
|
||||
>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell bitSortable="name" default>{{ "members" | i18n }}</th>
|
||||
@@ -44,25 +49,23 @@
|
||||
<th bitCell bitSortable="itemsCount">{{ "items" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar size="small" [text]="r.name" class="tw-mr-3"></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<button type="button" bitLink (click)="edit(r)">
|
||||
{{ r.name }}
|
||||
</button>
|
||||
<ng-template bitRowDef let-row>
|
||||
<td bitCell>
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar size="small" [text]="row.name" class="tw-mr-3"></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<button type="button" bitLink (click)="edit(row)">
|
||||
{{ row.name }}
|
||||
</button>
|
||||
|
||||
<div class="tw-text-sm tw-mt-1 tw-text-muted">
|
||||
{{ r.email }}
|
||||
</div>
|
||||
<div class="tw-text-sm tw-mt-1 tw-text-muted">
|
||||
{{ row.email }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ r.groupsCount }}</td>
|
||||
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ r.collectionsCount }}</td>
|
||||
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ r.itemsCount }}</td>
|
||||
</tr>
|
||||
</div>
|
||||
</td>
|
||||
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ row.groupsCount }}</td>
|
||||
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ row.collectionsCount }}</td>
|
||||
<td bitCell class="tw-text-muted tw-w-[278px] tw-p-4">{{ row.itemsCount }}</td>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</bit-table-scroll>
|
||||
|
||||
@@ -45,8 +45,6 @@ import {
|
||||
DefaultAuthRequestApiService,
|
||||
DefaultLoginSuccessHandlerService,
|
||||
LoginSuccessHandlerService,
|
||||
PasswordLoginStrategy,
|
||||
PasswordLoginStrategyData,
|
||||
LoginApprovalComponentServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -1460,37 +1458,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultLoginSuccessHandlerService,
|
||||
deps: [SyncService, UserAsymmetricKeysRegenerationService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordLoginStrategy,
|
||||
useClass: PasswordLoginStrategy,
|
||||
deps: [
|
||||
PasswordLoginStrategyData,
|
||||
PasswordStrengthServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
ApiServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
LogService,
|
||||
StateServiceAbstraction,
|
||||
TwoFactorServiceAbstraction,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
BillingAccountProfileStateService,
|
||||
VaultTimeoutSettingsService,
|
||||
KdfConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PasswordLoginStrategyData,
|
||||
useClass: PasswordLoginStrategyData,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaskService,
|
||||
useClass: DefaultTaskService,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from, map, switchMap, takeUntil } from "rxjs";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -22,11 +21,9 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
loaded = false;
|
||||
ciphers: CipherView[] = [];
|
||||
searchPlaceholder: string = null;
|
||||
filter: (cipher: CipherView) => boolean = null;
|
||||
deleted = false;
|
||||
organization: Organization;
|
||||
accessEvents = false;
|
||||
|
||||
protected searchPending = false;
|
||||
|
||||
@@ -45,20 +42,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
protected searchService: SearchService,
|
||||
protected cipherService: CipherService,
|
||||
protected accountService: AccountService,
|
||||
) {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.cipherService.cipherViews$(userId).pipe(map((ciphers) => ({ userId, ciphers }))),
|
||||
),
|
||||
takeUntilDestroyed(),
|
||||
)
|
||||
.subscribe(({ userId, ciphers }) => {
|
||||
void this.doSearch(ciphers, userId);
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._searchText$
|
||||
|
||||
@@ -161,8 +161,9 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned OFF, we must force a reload to ensure the correct UI is shown
|
||||
if (!flag) {
|
||||
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
const uniqueQueryParams = {
|
||||
...this.activatedRoute.queryParams,
|
||||
...qParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(), // Adding a unique timestamp as a query parameter
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
|
||||
import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service";
|
||||
import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service";
|
||||
import { PasswordLoginStrategy } from "../../common/login-strategies/password-login.strategy";
|
||||
|
||||
/**
|
||||
* Component for verifying a new device via a one-time password (OTP).
|
||||
@@ -58,7 +57,6 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private formBuilder: FormBuilder,
|
||||
private passwordLoginStrategy: PasswordLoginStrategy,
|
||||
private apiService: ApiService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private logService: LogService,
|
||||
|
||||
@@ -427,7 +427,6 @@ export class SsoComponent implements OnInit {
|
||||
);
|
||||
this.formPromise = this.loginStrategyService.logIn(credentials);
|
||||
const authResult = await this.formPromise;
|
||||
|
||||
if (authResult.requiresTwoFactor) {
|
||||
return await this.handleTwoFactorRequired(orgSsoIdentifier);
|
||||
}
|
||||
@@ -441,9 +440,10 @@ export class SsoComponent implements OnInit {
|
||||
// - Browser SSO on extension open
|
||||
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
|
||||
|
||||
// Grabbing the active user id right before making the state set to ensure it exists.
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId);
|
||||
await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
|
||||
orgSsoIdentifier,
|
||||
authResult.userId,
|
||||
);
|
||||
|
||||
// must come after 2fa check since user decryption options aren't available if 2fa is required
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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";
|
||||
@@ -53,6 +54,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
@@ -88,6 +90,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
masterPasswordService = new FakeMasterPasswordService();
|
||||
@@ -117,6 +120,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
|
||||
tokenResponse = identityTokenResponseFactory();
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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";
|
||||
@@ -123,6 +124,7 @@ describe("LoginStrategy", () => {
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
|
||||
let passwordLoginStrategy: PasswordLoginStrategy;
|
||||
let credentials: PasswordLoginCredentials;
|
||||
@@ -147,6 +149,7 @@ describe("LoginStrategy", () => {
|
||||
policyService = mock<PolicyService>();
|
||||
passwordStrengthService = mock<PasswordStrengthService>();
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
|
||||
@@ -175,6 +178,7 @@ describe("LoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
});
|
||||
@@ -496,6 +500,7 @@ describe("LoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
@@ -559,6 +564,7 @@ describe("LoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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";
|
||||
@@ -93,6 +94,7 @@ export abstract class LoginStrategy {
|
||||
protected billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
protected KdfConfigService: KdfConfigService,
|
||||
protected environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
abstract exportCache(): CacheData;
|
||||
@@ -196,6 +198,10 @@ export abstract class LoginStrategy {
|
||||
emailVerified: accountInformation.email_verified ?? false,
|
||||
});
|
||||
|
||||
// User env must be seeded from currently set env before switching to the account
|
||||
// to avoid any incorrect emissions of the global default env.
|
||||
await this.environmentService.seedUserEnvironment(userId);
|
||||
|
||||
await this.accountService.switchAccount(userId);
|
||||
|
||||
await this.stateService.addAccount(
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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";
|
||||
@@ -80,6 +81,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
|
||||
let passwordLoginStrategy: PasswordLoginStrategy;
|
||||
let credentials: PasswordLoginCredentials;
|
||||
@@ -106,6 +108,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({
|
||||
@@ -144,6 +147,7 @@ describe("PasswordLoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
credentials = new PasswordLoginCredentials(email, masterPassword);
|
||||
tokenResponse = identityTokenResponseFactory(masterPasswordPolicy);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -63,6 +64,7 @@ describe("SsoLoginStrategy", () => {
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
|
||||
let ssoLoginStrategy: SsoLoginStrategy;
|
||||
let credentials: SsoLoginCredentials;
|
||||
@@ -98,6 +100,7 @@ describe("SsoLoginStrategy", () => {
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
@@ -142,6 +145,7 @@ describe("SsoLoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
|
||||
});
|
||||
|
||||
@@ -97,7 +97,6 @@ describe("UserApiLoginStrategy", () => {
|
||||
|
||||
apiLogInStrategy = new UserApiLoginStrategy(
|
||||
cache,
|
||||
environmentService,
|
||||
keyConnectorService,
|
||||
accountService,
|
||||
masterPasswordService,
|
||||
@@ -115,6 +114,7 @@ describe("UserApiLoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
|
||||
credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con
|
||||
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||
@@ -31,7 +30,6 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||
|
||||
constructor(
|
||||
data: UserApiLoginStrategyData,
|
||||
private environmentService: EnvironmentService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
...sharedDeps: ConstructorParameters<typeof LoginStrategy>
|
||||
) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
VaultTimeoutSettingsService,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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";
|
||||
@@ -52,6 +53,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
let billingAccountProfileStateService: MockProxy<BillingAccountProfileStateService>;
|
||||
let vaultTimeoutSettingsService: MockProxy<VaultTimeoutSettingsService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
|
||||
let webAuthnLoginStrategy!: WebAuthnLoginStrategy;
|
||||
|
||||
@@ -95,6 +97,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
billingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
@@ -120,6 +123,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
billingAccountProfileStateService,
|
||||
vaultTimeoutSettingsService,
|
||||
kdfConfigService,
|
||||
environmentService,
|
||||
);
|
||||
|
||||
// Create credentials
|
||||
|
||||
@@ -402,6 +402,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
this.billingAccountProfileStateService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.kdfConfigService,
|
||||
this.environmentService,
|
||||
];
|
||||
|
||||
return source.pipe(
|
||||
@@ -430,7 +431,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
case AuthenticationType.UserApiKey:
|
||||
return new UserApiLoginStrategy(
|
||||
data?.userApiKey ?? new UserApiLoginStrategyData(),
|
||||
this.environmentService,
|
||||
this.keyConnectorService,
|
||||
...sharedDeps,
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
|
||||
/**
|
||||
* Feature flags.
|
||||
*
|
||||
@@ -5,7 +7,6 @@
|
||||
*/
|
||||
export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
||||
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
|
||||
LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
|
||||
@@ -23,6 +24,9 @@ export enum FeatureFlag {
|
||||
NotificationRefresh = "notification-refresh",
|
||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||
|
||||
/* Key Management */
|
||||
UseSDKForDecryption = "use-sdk-for-decryption",
|
||||
|
||||
/* Tools */
|
||||
ItemShare = "item-share",
|
||||
CriticalApps = "pm-14466-risk-insights-critical-application",
|
||||
@@ -64,7 +68,6 @@ const FALSE = false as boolean;
|
||||
*/
|
||||
export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
||||
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
|
||||
[FeatureFlag.LimitItemDeletion]: FALSE,
|
||||
@@ -82,6 +85,9 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||
|
||||
/* Key Management */
|
||||
[FeatureFlag.UseSDKForDecryption]: FALSE,
|
||||
|
||||
/* Tools */
|
||||
[FeatureFlag.ItemShare]: FALSE,
|
||||
[FeatureFlag.CriticalApps]: FALSE,
|
||||
@@ -113,3 +119,14 @@ export const DefaultFeatureFlagValue = {
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
export type FeatureFlagValueType<Flag extends FeatureFlag> = DefaultFeatureFlagValueType[Flag];
|
||||
|
||||
export function getFeatureFlagValue<Flag extends FeatureFlag>(
|
||||
serverConfig: ServerConfig | null,
|
||||
flag: Flag,
|
||||
) {
|
||||
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
|
||||
return DefaultFeatureFlagValue[flag];
|
||||
}
|
||||
|
||||
return serverConfig.featureStates[flag] as FeatureFlagValueType<Flag>;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OnServerConfigChange } from "../../../platform/abstractions/config/config.service";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class BulkEncryptService {
|
||||
export abstract class BulkEncryptService implements OnServerConfigChange {
|
||||
abstract decryptItems<T extends InitializerMetadata>(
|
||||
items: Decryptable<T>[],
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<T[]>;
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { OnServerConfigChange } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { Encrypted } from "@bitwarden/common/platform/interfaces/encrypted";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
@@ -5,7 +7,7 @@ import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-arr
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export abstract class EncryptService {
|
||||
export abstract class EncryptService implements OnServerConfigChange {
|
||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
|
||||
/**
|
||||
@@ -55,4 +57,5 @@ export abstract class EncryptService {
|
||||
value: string | Uint8Array,
|
||||
algorithm: "sha1" | "sha256" | "sha512",
|
||||
): Promise<string>;
|
||||
abstract onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
|
||||
import { buildDecryptMessage, buildSetConfigMessage } from "./encrypt.worker";
|
||||
|
||||
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||
const workerTTL = 60000; // 1 minute
|
||||
const maxWorkers = 8;
|
||||
@@ -57,6 +61,13 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
this.workers.forEach((worker) => {
|
||||
const request = buildSetConfigMessage({ newConfig });
|
||||
worker.postMessage(request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends items to a set of web workers to decrypt them. This utilizes multiple workers to decrypt items
|
||||
* faster without interrupting other operations (e.g. updating UI).
|
||||
@@ -108,17 +119,18 @@ export class BulkEncryptServiceImplementation implements BulkEncryptService {
|
||||
itemsForWorker.push(...items.slice(end));
|
||||
}
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
const id = Utils.newGuid();
|
||||
const request = buildDecryptMessage({
|
||||
id,
|
||||
items: itemsForWorker,
|
||||
key: key,
|
||||
};
|
||||
});
|
||||
|
||||
worker.postMessage(JSON.stringify(request));
|
||||
worker.postMessage(request);
|
||||
results.push(
|
||||
firstValueFrom(
|
||||
fromEvent(worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
filter((response: MessageEvent) => response.data?.id === id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
|
||||
@@ -14,16 +14,31 @@ import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-arr
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import {
|
||||
DefaultFeatureFlagValue,
|
||||
FeatureFlag,
|
||||
getFeatureFlagValue,
|
||||
} from "../../../enums/feature-flag.enum";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
export class EncryptServiceImplementation implements EncryptService {
|
||||
private useSDKForDecryption: boolean = DefaultFeatureFlagValue[FeatureFlag.UseSDKForDecryption];
|
||||
|
||||
constructor(
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected logService: LogService,
|
||||
protected logMacFailures: boolean,
|
||||
) {}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
const old = this.useSDKForDecryption;
|
||||
this.useSDKForDecryption = getFeatureFlagValue(newConfig, FeatureFlag.UseSDKForDecryption);
|
||||
this.logService.debug("updated sdk decryption flag", old, this.useSDKForDecryption);
|
||||
}
|
||||
|
||||
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
@@ -53,20 +68,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
}
|
||||
|
||||
const encValue = await this.aesEncrypt(plainValue, key);
|
||||
let macLen = 0;
|
||||
if (encValue.mac != null) {
|
||||
macLen = encValue.mac.byteLength;
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
|
||||
encBytes.set([encValue.key.encType]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
if (encValue.mac != null) {
|
||||
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
||||
}
|
||||
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes);
|
||||
return EncArrayBuffer.fromParts(encValue.key.encType, encValue.iv, encValue.data, encValue.mac);
|
||||
}
|
||||
|
||||
async decryptToUtf8(
|
||||
@@ -74,6 +76,15 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
key: SymmetricCryptoKey,
|
||||
decryptContext: string = "no context",
|
||||
): Promise<string> {
|
||||
if (this.useSDKForDecryption) {
|
||||
this.logService.debug("decrypting with SDK");
|
||||
if (encString == null || encString.encryptedString == null) {
|
||||
throw new Error("encString is null or undefined");
|
||||
}
|
||||
return PureCrypto.symmetric_decrypt(encString.encryptedString, key.keyB64);
|
||||
}
|
||||
this.logService.debug("decrypting with javascript");
|
||||
|
||||
if (key == null) {
|
||||
throw new Error("No key provided for decryption.");
|
||||
}
|
||||
@@ -137,6 +148,25 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
key: SymmetricCryptoKey,
|
||||
decryptContext: string = "no context",
|
||||
): Promise<Uint8Array | null> {
|
||||
if (this.useSDKForDecryption) {
|
||||
this.logService.debug("decrypting bytes with SDK");
|
||||
if (
|
||||
encThing.encryptionType == null ||
|
||||
encThing.ivBytes == null ||
|
||||
encThing.dataBytes == null
|
||||
) {
|
||||
throw new Error("Cannot decrypt, missing type, IV, or data bytes.");
|
||||
}
|
||||
const buffer = EncArrayBuffer.fromParts(
|
||||
encThing.encryptionType,
|
||||
encThing.ivBytes,
|
||||
encThing.dataBytes,
|
||||
encThing.macBytes,
|
||||
).buffer;
|
||||
return PureCrypto.symmetric_decrypt_array_buffer(buffer, key.keyB64);
|
||||
}
|
||||
this.logService.debug("decrypting bytes with javascript");
|
||||
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
@@ -9,19 +9,48 @@ import { ContainerService } from "@bitwarden/common/platform/services/container.
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
|
||||
const workerApi: Worker = self as any;
|
||||
|
||||
let inited = false;
|
||||
let encryptService: EncryptServiceImplementation;
|
||||
let logService: LogService;
|
||||
|
||||
const DECRYPT_COMMAND_SHELL = Object.freeze({ command: "decrypt" });
|
||||
const SET_CONFIG_COMMAND_SHELL = Object.freeze({ command: "setConfig" });
|
||||
|
||||
type DecryptCommandData = {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
};
|
||||
|
||||
type SetConfigCommandData = { newConfig: ServerConfig };
|
||||
|
||||
export function buildDecryptMessage(data: DecryptCommandData): string {
|
||||
return JSON.stringify({
|
||||
...data,
|
||||
...DECRYPT_COMMAND_SHELL,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildSetConfigMessage(data: SetConfigCommandData): string {
|
||||
return JSON.stringify({
|
||||
...data,
|
||||
...SET_CONFIG_COMMAND_SHELL,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap the worker environment with services required for decryption
|
||||
*/
|
||||
export function init() {
|
||||
const cryptoFunctionService = new WebCryptoFunctionService(self);
|
||||
const logService = new ConsoleLogService(false);
|
||||
logService = new ConsoleLogService(false);
|
||||
encryptService = new EncryptServiceImplementation(cryptoFunctionService, logService, true);
|
||||
|
||||
const bitwardenContainerService = new ContainerService(null, encryptService);
|
||||
@@ -39,11 +68,20 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
}
|
||||
|
||||
const request: {
|
||||
id: string;
|
||||
items: Jsonify<Decryptable<any>>[];
|
||||
key: Jsonify<SymmetricCryptoKey>;
|
||||
command: string;
|
||||
} = JSON.parse(event.data);
|
||||
|
||||
switch (request.command) {
|
||||
case DECRYPT_COMMAND_SHELL.command:
|
||||
return await handleDecrypt(request as unknown as DecryptCommandData);
|
||||
case SET_CONFIG_COMMAND_SHELL.command:
|
||||
return await handleSetConfig(request as unknown as SetConfigCommandData);
|
||||
default:
|
||||
logService.error(`unknown worker command`, request.command, request);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleDecrypt(request: DecryptCommandData) {
|
||||
const key = SymmetricCryptoKey.fromJSON(request.key);
|
||||
const items = request.items.map((jsonItem) => {
|
||||
const initializer = getClassInitializer<Decryptable<any>>(jsonItem.initializerKey);
|
||||
@@ -55,4 +93,8 @@ workerApi.addEventListener("message", async (event: { data: string }) => {
|
||||
id: request.id,
|
||||
items: JSON.stringify(result),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSetConfig(request: SetConfigCommandData) {
|
||||
encryptService.onServerConfigChange(request.newConfig);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { BulkEncryptService } from "../../../key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { Decryptable } from "../../../platform/interfaces/decryptable.interface";
|
||||
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
|
||||
/**
|
||||
@@ -33,4 +33,8 @@ export class FallbackBulkEncryptService implements BulkEncryptService {
|
||||
async setFeatureFlagEncryptService(featureFlagEncryptService: BulkEncryptService) {
|
||||
this.featureFlagEncryptService = featureFlagEncryptService;
|
||||
}
|
||||
|
||||
onServerConfigChange(newConfig: ServerConfig): void {
|
||||
(this.featureFlagEncryptService ?? this.encryptService).onServerConfigChange(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { getClassInitializer } from "@bitwarden/common/platform/services/cryptography/get-class-initializer";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
|
||||
import { EncryptServiceImplementation } from "./encrypt.service.implementation";
|
||||
import { buildDecryptMessage, buildSetConfigMessage } from "./encrypt.worker";
|
||||
|
||||
// TTL (time to live) is not strictly required but avoids tying up memory resources if inactive
|
||||
const workerTTL = 3 * 60000; // 3 minutes
|
||||
@@ -47,17 +50,18 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
||||
|
||||
this.restartTimeout();
|
||||
|
||||
const request = {
|
||||
id: Utils.newGuid(),
|
||||
const id = Utils.newGuid();
|
||||
const request = buildDecryptMessage({
|
||||
id,
|
||||
items: items,
|
||||
key: key,
|
||||
};
|
||||
});
|
||||
|
||||
this.worker.postMessage(JSON.stringify(request));
|
||||
this.worker.postMessage(request);
|
||||
|
||||
return await firstValueFrom(
|
||||
fromEvent(this.worker, "message").pipe(
|
||||
filter((response: MessageEvent) => response.data?.id === request.id),
|
||||
filter((response: MessageEvent) => response.data?.id === id),
|
||||
map((response) => JSON.parse(response.data.items)),
|
||||
map((items) =>
|
||||
items.map((jsonItem: Jsonify<T>) => {
|
||||
@@ -71,6 +75,15 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple
|
||||
);
|
||||
}
|
||||
|
||||
override onServerConfigChange(newConfig: ServerConfig): void {
|
||||
super.onServerConfigChange(newConfig);
|
||||
|
||||
if (this.worker !== null) {
|
||||
const request = buildSetConfigMessage({ newConfig });
|
||||
this.worker.postMessage(request);
|
||||
}
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.clear$.next();
|
||||
this.worker?.terminate();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
import { Observable, Subscription } from "rxjs";
|
||||
import { SemVer } from "semver";
|
||||
|
||||
import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum";
|
||||
@@ -10,6 +10,8 @@ import { Region } from "../environment.service";
|
||||
|
||||
import { ServerConfig } from "./server-config";
|
||||
|
||||
export type ConfigCallback = (serverConfig: ServerConfig) => void;
|
||||
|
||||
export abstract class ConfigService {
|
||||
/** The server config of the currently active user */
|
||||
serverConfig$: Observable<ServerConfig | null>;
|
||||
@@ -54,4 +56,10 @@ export abstract class ConfigService {
|
||||
* Triggers a check that the config for the currently active user is up-to-date. If it is not, it will be fetched from the server and stored.
|
||||
*/
|
||||
abstract ensureConfigFetched(): Promise<void>;
|
||||
|
||||
abstract broadcastConfigChangesTo(...listeners: OnServerConfigChange[]): Subscription;
|
||||
}
|
||||
|
||||
export interface OnServerConfigChange {
|
||||
onServerConfigChange(newConfig: ServerConfig): void;
|
||||
}
|
||||
|
||||
15
libs/common/src/platform/enums/encryption-type.enum.spec.ts
Normal file
15
libs/common/src/platform/enums/encryption-type.enum.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
AsymmetricEncryptionTypes,
|
||||
EncryptionType,
|
||||
SymmetricEncryptionTypes,
|
||||
} from "./encryption-type.enum";
|
||||
|
||||
describe("EncryptionType", () => {
|
||||
it("classifies all types as symmetric or asymmetric", () => {
|
||||
const nSymmetric = SymmetricEncryptionTypes.length;
|
||||
const nAsymmetric = AsymmetricEncryptionTypes.length;
|
||||
const nTotal = nSymmetric + nAsymmetric;
|
||||
// enums are indexable by string and number
|
||||
expect(Object.keys(EncryptionType).length).toEqual(nTotal * 2);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,19 @@ export enum EncryptionType {
|
||||
Rsa2048_OaepSha1_HmacSha256_B64 = 6,
|
||||
}
|
||||
|
||||
export const SymmetricEncryptionTypes = [
|
||||
EncryptionType.AesCbc256_B64,
|
||||
EncryptionType.AesCbc128_HmacSha256_B64,
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
] as const;
|
||||
|
||||
export const AsymmetricEncryptionTypes = [
|
||||
EncryptionType.Rsa2048_OaepSha256_B64,
|
||||
EncryptionType.Rsa2048_OaepSha1_B64,
|
||||
EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64,
|
||||
EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64,
|
||||
] as const;
|
||||
|
||||
export function encryptionTypeToString(encryptionType: EncryptionType): string {
|
||||
if (encryptionType in EncryptionType) {
|
||||
return EncryptionType[encryptionType];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EncryptionType } from "../enums";
|
||||
|
||||
export interface Encrypted {
|
||||
encryptionType?: EncryptionType;
|
||||
dataBytes: Uint8Array;
|
||||
macBytes: Uint8Array;
|
||||
ivBytes: Uint8Array;
|
||||
dataBytes: Uint8Array | null;
|
||||
macBytes: Uint8Array | null | undefined;
|
||||
ivBytes: Uint8Array | null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import {
|
||||
EncryptionType,
|
||||
SymmetricEncryptionTypes,
|
||||
AsymmetricEncryptionTypes,
|
||||
encryptionTypeToString,
|
||||
} from "../../enums";
|
||||
|
||||
import { EncArrayBuffer } from "./enc-array-buffer";
|
||||
|
||||
@@ -71,4 +76,66 @@ describe("encArrayBuffer", () => {
|
||||
const bytes = makeStaticByteArray(50, 9);
|
||||
expect(() => new EncArrayBuffer(bytes)).toThrow("Error parsing encrypted ArrayBuffer");
|
||||
});
|
||||
|
||||
describe("fromParts factory", () => {
|
||||
const plainValue = makeStaticByteArray(16, 1);
|
||||
|
||||
it("throws if required data is null", () => {
|
||||
expect(() =>
|
||||
EncArrayBuffer.fromParts(EncryptionType.AesCbc128_HmacSha256_B64, plainValue, null!, null),
|
||||
).toThrow("encryptionType, iv, and data must be provided");
|
||||
expect(() =>
|
||||
EncArrayBuffer.fromParts(EncryptionType.AesCbc128_HmacSha256_B64, null!, plainValue, null),
|
||||
).toThrow("encryptionType, iv, and data must be provided");
|
||||
expect(() => EncArrayBuffer.fromParts(null!, plainValue, plainValue, null)).toThrow(
|
||||
"encryptionType, iv, and data must be provided",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(SymmetricEncryptionTypes.map((t) => encryptionTypeToString(t)))(
|
||||
"works for %s",
|
||||
async (typeName) => {
|
||||
const type = EncryptionType[typeName as keyof typeof EncryptionType];
|
||||
const iv = plainValue;
|
||||
const mac = type === EncryptionType.AesCbc256_B64 ? null : makeStaticByteArray(32, 20);
|
||||
const data = plainValue;
|
||||
|
||||
const actual = EncArrayBuffer.fromParts(type, iv, data, mac);
|
||||
|
||||
expect(actual.encryptionType).toEqual(type);
|
||||
expect(actual.ivBytes).toEqual(iv);
|
||||
expect(actual.macBytes).toEqual(mac);
|
||||
expect(actual.dataBytes).toEqual(data);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(SymmetricEncryptionTypes.filter((t) => t !== EncryptionType.AesCbc256_B64))(
|
||||
"validates mac length for %s",
|
||||
(type) => {
|
||||
const iv = plainValue;
|
||||
const mac = makeStaticByteArray(1, 20);
|
||||
const data = plainValue;
|
||||
|
||||
expect(() => EncArrayBuffer.fromParts(type, iv, data, mac)).toThrow("Invalid MAC length");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(SymmetricEncryptionTypes.map((t) => encryptionTypeToString(t)))(
|
||||
"requires or forbids mac for %s",
|
||||
async (typeName) => {
|
||||
const type = EncryptionType[typeName as keyof typeof EncryptionType];
|
||||
const iv = makeStaticByteArray(16, 10);
|
||||
const mac = type === EncryptionType.AesCbc256_B64 ? makeStaticByteArray(32, 20) : null;
|
||||
const data = plainValue;
|
||||
|
||||
expect(() => EncArrayBuffer.fromParts(type, iv, data, mac)).toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(AsymmetricEncryptionTypes)("throws for async type %s", (type) => {
|
||||
expect(() => EncArrayBuffer.fromParts(type, plainValue, plainValue, null)).toThrow(
|
||||
`Unknown EncryptionType ${type} for EncArrayBuffer.fromParts`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Encrypted } from "../../interfaces/encrypted";
|
||||
@@ -10,52 +8,86 @@ const MAC_LENGTH = 32;
|
||||
const MIN_DATA_LENGTH = 1;
|
||||
|
||||
export class EncArrayBuffer implements Encrypted {
|
||||
readonly encryptionType: EncryptionType = null;
|
||||
readonly dataBytes: Uint8Array = null;
|
||||
readonly ivBytes: Uint8Array = null;
|
||||
readonly macBytes: Uint8Array = null;
|
||||
readonly encryptionType: EncryptionType;
|
||||
readonly dataBytes: Uint8Array;
|
||||
readonly ivBytes: Uint8Array;
|
||||
readonly macBytes: Uint8Array | null = null;
|
||||
private static readonly DecryptionError = new Error(
|
||||
"Error parsing encrypted ArrayBuffer: data is corrupted or has an invalid format.",
|
||||
);
|
||||
|
||||
constructor(readonly buffer: Uint8Array) {
|
||||
const encBytes = buffer;
|
||||
const encType = encBytes[0];
|
||||
if (buffer == null) {
|
||||
throw new Error("EncArrayBuffer initialized with null buffer.");
|
||||
}
|
||||
|
||||
switch (encType) {
|
||||
this.encryptionType = this.buffer[0];
|
||||
|
||||
switch (this.encryptionType) {
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64: {
|
||||
const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH + MIN_DATA_LENGTH;
|
||||
if (encBytes.length < minimumLength) {
|
||||
this.throwDecryptionError();
|
||||
if (this.buffer.length < minimumLength) {
|
||||
throw EncArrayBuffer.DecryptionError;
|
||||
}
|
||||
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
this.macBytes = encBytes.slice(
|
||||
this.ivBytes = this.buffer.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
this.macBytes = this.buffer.slice(
|
||||
ENC_TYPE_LENGTH + IV_LENGTH,
|
||||
ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH,
|
||||
);
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH);
|
||||
this.dataBytes = this.buffer.slice(ENC_TYPE_LENGTH + IV_LENGTH + MAC_LENGTH);
|
||||
break;
|
||||
}
|
||||
case EncryptionType.AesCbc256_B64: {
|
||||
const minimumLength = ENC_TYPE_LENGTH + IV_LENGTH + MIN_DATA_LENGTH;
|
||||
if (encBytes.length < minimumLength) {
|
||||
this.throwDecryptionError();
|
||||
if (this.buffer.length < minimumLength) {
|
||||
throw EncArrayBuffer.DecryptionError;
|
||||
}
|
||||
|
||||
this.ivBytes = encBytes.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
this.dataBytes = encBytes.slice(ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
this.ivBytes = this.buffer.slice(ENC_TYPE_LENGTH, ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
this.dataBytes = this.buffer.slice(ENC_TYPE_LENGTH + IV_LENGTH);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.throwDecryptionError();
|
||||
throw EncArrayBuffer.DecryptionError;
|
||||
}
|
||||
|
||||
this.encryptionType = encType;
|
||||
}
|
||||
|
||||
private throwDecryptionError() {
|
||||
throw new Error(
|
||||
"Error parsing encrypted ArrayBuffer: data is corrupted or has an invalid format.",
|
||||
);
|
||||
static fromParts(
|
||||
encryptionType: EncryptionType,
|
||||
iv: Uint8Array,
|
||||
data: Uint8Array,
|
||||
mac: Uint8Array | undefined | null,
|
||||
) {
|
||||
if (encryptionType == null || iv == null || data == null) {
|
||||
throw new Error("encryptionType, iv, and data must be provided");
|
||||
}
|
||||
|
||||
switch (encryptionType) {
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
EncArrayBuffer.validateIvLength(iv);
|
||||
EncArrayBuffer.validateMacLength(encryptionType, mac);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown EncryptionType ${encryptionType} for EncArrayBuffer.fromParts`);
|
||||
}
|
||||
|
||||
let macLen = 0;
|
||||
if (mac != null) {
|
||||
macLen = mac.length;
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(1 + iv.byteLength + macLen + data.byteLength);
|
||||
bytes.set([encryptionType], 0);
|
||||
bytes.set(iv, 1);
|
||||
if (mac != null) {
|
||||
bytes.set(mac, 1 + iv.byteLength);
|
||||
}
|
||||
bytes.set(data, 1 + iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(bytes);
|
||||
}
|
||||
|
||||
static async fromResponse(response: {
|
||||
@@ -72,4 +104,28 @@ export class EncArrayBuffer implements Encrypted {
|
||||
const buffer = Utils.fromB64ToArray(b64);
|
||||
return new EncArrayBuffer(buffer);
|
||||
}
|
||||
|
||||
static validateIvLength(iv: Uint8Array) {
|
||||
if (iv == null || iv.length !== IV_LENGTH) {
|
||||
throw new Error("Invalid IV length");
|
||||
}
|
||||
}
|
||||
|
||||
static validateMacLength(encType: EncryptionType, mac: Uint8Array | null | undefined) {
|
||||
switch (encType) {
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if (mac != null) {
|
||||
throw new Error("mac must not be provided for AesCbc256_B64");
|
||||
}
|
||||
break;
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if (mac == null || mac.length !== MAC_LENGTH) {
|
||||
throw new Error("Invalid MAC length");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid encryption type and mac combination");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -17,7 +15,7 @@ export class EncString implements Encrypted {
|
||||
decryptedValue?: string;
|
||||
data?: string;
|
||||
iv?: string;
|
||||
mac?: string;
|
||||
mac: string | undefined | null;
|
||||
|
||||
constructor(
|
||||
encryptedStringOrType: string | EncryptionType,
|
||||
@@ -32,15 +30,15 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
}
|
||||
|
||||
get ivBytes(): Uint8Array {
|
||||
get ivBytes(): Uint8Array | null {
|
||||
return this.iv == null ? null : Utils.fromB64ToArray(this.iv);
|
||||
}
|
||||
|
||||
get macBytes(): Uint8Array {
|
||||
get macBytes(): Uint8Array | null {
|
||||
return this.mac == null ? null : Utils.fromB64ToArray(this.mac);
|
||||
}
|
||||
|
||||
get dataBytes(): Uint8Array {
|
||||
get dataBytes(): Uint8Array | null {
|
||||
return this.data == null ? null : Utils.fromB64ToArray(this.data);
|
||||
}
|
||||
|
||||
@@ -48,7 +46,7 @@ export class EncString implements Encrypted {
|
||||
return this.encryptedString as string;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<EncString>): EncString {
|
||||
static fromJSON(obj: Jsonify<EncString>): EncString | null {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -56,7 +54,12 @@ export class EncString implements Encrypted {
|
||||
return new EncString(obj);
|
||||
}
|
||||
|
||||
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
|
||||
private initFromData(
|
||||
encType: EncryptionType,
|
||||
data: string,
|
||||
iv: string | undefined,
|
||||
mac: string | undefined,
|
||||
) {
|
||||
if (iv != null) {
|
||||
this.encryptedString = (encType + "." + iv + "|" + data) as EncryptedString;
|
||||
} else {
|
||||
@@ -119,15 +122,13 @@ export class EncString implements Encrypted {
|
||||
} {
|
||||
const headerPieces = encryptedString.split(".");
|
||||
let encType: EncryptionType;
|
||||
let encPieces: string[] = null;
|
||||
let encPieces: string[];
|
||||
|
||||
if (headerPieces.length === 2) {
|
||||
try {
|
||||
encType = parseInt(headerPieces[0], null);
|
||||
encType = parseInt(headerPieces[0]);
|
||||
encPieces = headerPieces[1].split("|");
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return { encType: NaN, encPieces: [] };
|
||||
}
|
||||
} else {
|
||||
@@ -160,7 +161,7 @@ export class EncString implements Encrypted {
|
||||
|
||||
async decrypt(
|
||||
orgId: string | null,
|
||||
key: SymmetricCryptoKey = null,
|
||||
key: SymmetricCryptoKey | null = null,
|
||||
context?: string,
|
||||
): Promise<string> {
|
||||
if (this.decryptedValue != null) {
|
||||
@@ -219,7 +220,7 @@ export class EncString implements Encrypted {
|
||||
|
||||
return this.decryptedValue;
|
||||
}
|
||||
private async getKeyForDecryption(orgId: string) {
|
||||
private async getKeyForDecryption(orgId: string | null) {
|
||||
const keyService = Utils.getContainerService().getKeyService();
|
||||
return orgId != null
|
||||
? await keyService.getOrgKey(orgId)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user