1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 13:40:06 +00:00

Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows

This commit is contained in:
Alec Rippberger
2025-03-04 16:56:46 -06:00
64 changed files with 738 additions and 599 deletions

20
.github/CODEOWNERS vendored
View File

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

View File

@@ -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"],
},

View File

@@ -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."
@@ -5163,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"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}
};
}

View File

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

View File

@@ -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 });
}
}

View File

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

View File

@@ -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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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");

View File

@@ -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 members 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"
},

View File

@@ -5,7 +5,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",
@@ -64,7 +63,6 @@ const FALSE = false as boolean;
*/
export const DefaultFeatureFlagValue = {
/* Admin Console Team */
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
[FeatureFlag.AccountDeprovisioning]: FALSE,
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
[FeatureFlag.LimitItemDeletion]: FALSE,

View File

@@ -0,0 +1,35 @@
<bit-dialog #dialog dialogSize="large" background="alt">
<span bitDialogTitle>
{{ headerText }}
</span>
<span bitDialogContent>
<tools-send-form
formId="sendForm"
[config]="config"
(onSendCreated)="onSendCreated($event)"
(onSendUpdated)="onSendUpdated($event)"
[submitBtn]="submitBtn"
>
</tools-send-form>
</span>
<ng-container bitDialogFooter>
<button bitButton type="submit" form="sendForm" buttonType="primary" #submitBtn>
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
<div class="tw-ml-auto">
<button
*ngIf="config?.mode !== 'add'"
type="button"
buttonType="danger"
slot="end"
bitIconButton="bwi-trash"
[bitAction]="deleteSend"
appA11yTitle="{{ 'delete' | i18n }}"
></button>
</div>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,179 @@
// 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 { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import {
AsyncActionsModule,
ButtonModule,
DialogService,
IconButtonModule,
SearchModule,
ToastService,
DialogModule,
} from "@bitwarden/components";
import { SendFormConfig, SendFormMode, SendFormModule } from "../send-form";
export interface SendItemDialogParams {
/**
* The configuration object for the dialog and form.
*/
formConfig: SendFormConfig;
/**
* If true, the "edit" button will be disabled in the dialog.
*/
disableForm?: boolean;
}
export enum SendItemDialogResult {
/**
* A Send was saved (created or updated).
*/
Saved = "saved",
/**
* A Send was deleted.
*/
Deleted = "deleted",
}
/**
* Component for adding or editing a send item.
*/
@Component({
templateUrl: "send-add-edit-dialog.component.html",
standalone: true,
imports: [
CommonModule,
SearchModule,
JslibModule,
FormsModule,
ButtonModule,
IconButtonModule,
SendFormModule,
AsyncActionsModule,
DialogModule,
],
})
export class SendAddEditDialogComponent {
/**
* The header text for the component.
*/
headerText: string;
/**
* The configuration for the send form.
*/
config: SendFormConfig;
constructor(
@Inject(DIALOG_DATA) protected params: SendItemDialogParams,
private dialogRef: DialogRef<SendItemDialogResult>,
private i18nService: I18nService,
private sendApiService: SendApiService,
private toastService: ToastService,
private dialogService: DialogService,
) {
this.config = params.formConfig;
this.headerText = this.getHeaderText(this.config.mode, this.config.sendType);
}
/**
* Handles the event when the send is created.
*/
async onSendCreated(send: SendView) {
// FIXME Add dialogService.open send-created dialog
this.dialogRef.close(SendItemDialogResult.Saved);
return;
}
/**
* Handles the event when the send is updated.
*/
async onSendUpdated(send: SendView) {
this.dialogRef.close(SendItemDialogResult.Saved);
}
/**
* Handles the event when the send is deleted.
*/
async onSendDeleted() {
this.dialogRef.close(SendItemDialogResult.Deleted);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedSend"),
});
}
/**
* Handles the deletion of the current Send.
*/
deleteSend = async () => {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteSend" },
content: { key: "deleteSendPermanentConfirmation" },
type: "warning",
});
if (!confirmed) {
return;
}
try {
await this.sendApiService.delete(this.config.originalSend?.id);
} catch (e) {
this.toastService.showToast({
variant: "error",
title: null,
message: e.message,
});
return;
}
await this.onSendDeleted();
};
/**
* Gets the header text based on the mode and type.
* @param mode The mode of the send form.
* @param type The type of the send
* @returns The header text.
*/
private getHeaderText(mode: SendFormMode, type: SendType) {
const headerKey =
mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
switch (type) {
case SendType.Text:
return this.i18nService.t(headerKey, this.i18nService.t("textSend"));
case SendType.File:
return this.i18nService.t(headerKey, this.i18nService.t("fileSend"));
}
}
/**
* Opens the send add/edit dialog.
* @param dialogService Instance of the DialogService.
* @param params The parameters for the dialog.
* @returns The dialog result.
*/
static open(dialogService: DialogService, params: SendItemDialogParams) {
return dialogService.open<SendItemDialogResult, SendItemDialogParams>(
SendAddEditDialogComponent,
{
data: params,
},
);
}
}

View File

@@ -1,6 +1,7 @@
export * from "./icons";
export * from "./send-form";
export { NewSendDropdownComponent } from "./new-send-dropdown/new-send-dropdown.component";
export * from "./add-edit/send-add-edit-dialog.component";
export { SendListItemsContainerComponent } from "./send-list-items-container/send-list-items-container.component";
export { SendItemsService } from "./services/send-items.service";
export { SendSearchComponent } from "./send-search/send-search.component";

View File

@@ -3,6 +3,19 @@
{{ "cardExpiredMessage" | i18n }}
</bit-callout>
<ng-container *ngIf="isSecurityTasksEnabled$ | async">
<bit-callout
*ngIf="cipher?.login.uris.length > 0 && hadPendingChangePasswordTask"
type="warning"
[title]="''"
>
<i class="bwi bwi-exclamation-triangle tw-text-warning" aria-hidden="true"></i>
<a bitLink (click)="launchChangePassword()">
{{ "changeAtRiskPassword" | i18n }}
<i class="bwi bwi-popout tw-ml-1" aria-hidden="true"></i>
</a>
</bit-callout>
</ng-container>
<!-- HELPER TEXT -->
<p
class="tw-text-sm tw-text-muted"
@@ -23,7 +36,15 @@
</app-item-details-v2>
<!-- LOGIN CREDENTIALS -->
<app-login-credentials-view *ngIf="hasLogin" [cipher]="cipher"></app-login-credentials-view>
<app-login-credentials-view
*ngIf="hasLogin"
[cipher]="cipher"
[activeUserId]="activeUserId$ | async"
[hadPendingChangePasswordTask]="
hadPendingChangePasswordTask && (isSecurityTasksEnabled$ | async)
"
(handleChangePassword)="launchChangePassword()"
></app-login-credentials-view>
<!-- AUTOFILL OPTIONS -->
<app-autofill-options-view

View File

@@ -1,6 +1,6 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnChanges, OnDestroy } from "@angular/core";
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -12,11 +12,17 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { isCardExpired } from "@bitwarden/common/autofill/utils";
import { CollectionId } from "@bitwarden/common/types/guid";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CollectionId, UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { CalloutModule, SearchModule } from "@bitwarden/components";
import { AnchorLinkDirective, CalloutModule, SearchModule } from "@bitwarden/components";
import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service";
import { TaskService, SecurityTaskType } from "../tasks";
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.component";
@@ -48,12 +54,13 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
ViewIdentitySectionsComponent,
LoginCredentialsViewComponent,
AutofillOptionsViewComponent,
AnchorLinkDirective,
],
})
export class CipherViewComponent implements OnChanges, OnDestroy {
@Input({ required: true }) cipher: CipherView | null = null;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
activeUserId$ = getUserId(this.accountService.activeAccount$);
/**
* Optional list of collections the cipher is assigned to. If none are provided, they will be fetched using the
@@ -68,12 +75,18 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
folder$: Observable<FolderView | undefined> | undefined;
private destroyed$: Subject<void> = new Subject();
cardIsExpired: boolean = false;
hadPendingChangePasswordTask: boolean = false;
isSecurityTasksEnabled$ = this.configService.getFeatureFlag$(FeatureFlag.SecurityTasks);
constructor(
private organizationService: OrganizationService,
private collectionService: CollectionService,
private folderService: FolderService,
private accountService: AccountService,
private defaultTaskService: TaskService,
private platformUtilsService: PlatformUtilsService,
private changeLoginPasswordService: ChangeLoginPasswordService,
private configService: ConfigService,
) {}
async ngOnChanges() {
@@ -137,7 +150,11 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
);
}
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const userId = await firstValueFrom(this.activeUserId$);
if (this.cipher.edit && this.cipher.viewPassword) {
await this.checkPendingChangePasswordTasks(userId);
}
if (this.cipher.organizationId && userId) {
this.organization$ = this.organizationService
@@ -147,15 +164,29 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
}
if (this.cipher.folderId) {
const activeUserId = await firstValueFrom(this.activeUserId$);
if (!activeUserId) {
return;
}
this.folder$ = this.folderService
.getDecrypted$(this.cipher.folderId, activeUserId)
.getDecrypted$(this.cipher.folderId, userId)
.pipe(takeUntil(this.destroyed$));
}
}
async checkPendingChangePasswordTasks(userId: UserId): Promise<void> {
const tasks = await firstValueFrom(this.defaultTaskService.pendingTasks$(userId));
this.hadPendingChangePasswordTask = tasks?.some((task) => {
return (
task.cipherId === this.cipher?.id && task.type === SecurityTaskType.UpdateAtRiskCredential
);
});
}
launchChangePassword = async () => {
if (this.cipher != null) {
const url = await this.changeLoginPasswordService.getChangePasswordUrl(this.cipher);
if (url == null) {
return;
}
this.platformUtilsService.launchUri(url);
}
};
}

View File

@@ -89,6 +89,12 @@
(click)="logCopyEvent()"
></button>
</bit-form-field>
<bit-hint *ngIf="hadPendingChangePasswordTask">
<a bitLink (click)="launchChangePasswordEvent()">
{{ "changeAtRiskPassword" | i18n }}
<i class="bwi bwi-popout tw-ml-1" aria-hidden="true"></i>
</a>
</bit-hint>
<div
*ngIf="showPasswordCount && passwordRevealed"
[ngClass]="{ 'tw-mt-3': !cipher.login.totp, 'tw-mb-2': true }"

View File

@@ -8,6 +8,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums";
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 { UserId } from "@bitwarden/common/types/guid";
@@ -74,6 +75,7 @@ describe("LoginCredentialsViewComponent", () => {
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: I18nService, useValue: { t: (...keys: string[]) => keys.join(" ") } },
{ provide: ConfigService, useValue: mock<ConfigService>() },
],
}).compileComponents();

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule, DatePipe } from "@angular/common";
import { Component, inject, Input } from "@angular/core";
import { Component, EventEmitter, inject, Input, Output } from "@angular/core";
import { Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -10,6 +10,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { UserId } from "@bitwarden/common/types/guid";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
@@ -17,6 +18,7 @@ import {
SectionComponent,
SectionHeaderComponent,
TypographyModule,
LinkModule,
IconButtonModule,
BadgeModule,
ColorPasswordModule,
@@ -46,10 +48,14 @@ type TotpCodeValues = {
ColorPasswordModule,
BitTotpCountdownComponent,
ReadOnlyCipherCardComponent,
LinkModule,
],
})
export class LoginCredentialsViewComponent {
@Input() cipher: CipherView;
@Input() activeUserId: UserId;
@Input() hadPendingChangePasswordTask: boolean;
@Output() handleChangePassword = new EventEmitter<void>();
isPremium$: Observable<boolean> = this.accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -59,6 +65,7 @@ export class LoginCredentialsViewComponent {
showPasswordCount: boolean = false;
passwordRevealed: boolean = false;
totpCodeCopyObj: TotpCodeValues;
private datePipe = inject(DatePipe);
constructor(
@@ -111,4 +118,8 @@ export class LoginCredentialsViewComponent {
this.cipher.organizationId,
);
}
launchChangePasswordEvent(): void {
this.handleChangePassword.emit();
}
}

View File

@@ -35,7 +35,7 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer
]);
if (!reliable || wellKnownChangeUrl == null) {
return cipher.login.uri;
return url.origin;
}
return wellKnownChangeUrl;

2
package-lock.json generated
View File

@@ -230,7 +230,7 @@
},
"apps/desktop": {
"name": "@bitwarden/desktop",
"version": "2025.2.2",
"version": "2025.2.1",
"hasInstallScript": true,
"license": "GPL-3.0"
},