mirror of
https://github.com/bitwarden/browser
synced 2026-02-14 07:23:45 +00:00
PM-19255: Merge main into branch
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -94,7 +94,6 @@ 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
|
||||
# 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
|
||||
@@ -164,7 +163,6 @@ 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
|
||||
|
||||
158
.github/renovate.json5
vendored
158
.github/renovate.json5
vendored
@@ -4,51 +4,10 @@
|
||||
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",
|
||||
// Group all Github Action minor updates together to reduce PR noise.
|
||||
groupName: "Minor github-actions updates",
|
||||
matchManagers: ["github-actions"],
|
||||
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:",
|
||||
},
|
||||
{
|
||||
// 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",
|
||||
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:",
|
||||
matchUpdateTypes: ["minor"],
|
||||
addLabels: ["hold"],
|
||||
},
|
||||
{
|
||||
@@ -60,7 +19,7 @@
|
||||
{
|
||||
// By default, we send patch updates to the Dependency Dashboard and do not generate a PR.
|
||||
// We want to generate PRs for a select number of dependencies to ensure we stay up to date on these.
|
||||
matchPackageNames: ["browserslist", "electron", "rxjs", "typescript", "webpack"],
|
||||
matchPackageNames: ["browserslist", "electron", "rxjs", "typescript", "webpack", "zone.js"],
|
||||
matchUpdateTypes: ["patch"],
|
||||
dependencyDashboardApproval: false,
|
||||
},
|
||||
@@ -86,49 +45,7 @@
|
||||
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"],
|
||||
},
|
||||
{
|
||||
// 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"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"base64-loader",
|
||||
"buffer",
|
||||
"bufferutil",
|
||||
"core-js",
|
||||
"css-loader",
|
||||
"html-loader",
|
||||
"mini-css-extract-plugin",
|
||||
"postcss",
|
||||
"postcss-loader",
|
||||
"process",
|
||||
"sass",
|
||||
"sass-loader",
|
||||
"style-loader",
|
||||
"ts-loader",
|
||||
"url",
|
||||
"util",
|
||||
],
|
||||
matchPackageNames: ["buffer", "bufferutil", "core-js", "process", "url", "util"],
|
||||
description: "Admin Console owned dependencies",
|
||||
commitMessagePrefix: "[deps] AC:",
|
||||
reviewers: ["team:team-admin-console-dev"],
|
||||
@@ -179,7 +96,7 @@
|
||||
"lint-staged",
|
||||
"typescript-eslint",
|
||||
],
|
||||
groupName: "Linting minor-patch",
|
||||
groupName: "Minor and patch linting updates",
|
||||
matchUpdateTypes: ["minor", "patch"],
|
||||
},
|
||||
{
|
||||
@@ -236,6 +153,7 @@
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"babel-loader",
|
||||
"base64-loader",
|
||||
"base64",
|
||||
"bindgen",
|
||||
"browserslist",
|
||||
@@ -243,6 +161,7 @@
|
||||
"bytes",
|
||||
"core-foundation",
|
||||
"copy-webpack-plugin",
|
||||
"css-loader",
|
||||
"dirs",
|
||||
"electron",
|
||||
"electron-builder",
|
||||
@@ -254,6 +173,7 @@
|
||||
"futures",
|
||||
"hex",
|
||||
"homedir",
|
||||
"html-loader",
|
||||
"html-webpack-injector",
|
||||
"html-webpack-plugin",
|
||||
"interprocess",
|
||||
@@ -262,6 +182,7 @@
|
||||
"libc",
|
||||
"log",
|
||||
"lowdb",
|
||||
"mini-css-extract-plugin",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
@@ -272,15 +193,21 @@
|
||||
"oslog",
|
||||
"pin-project",
|
||||
"pkg",
|
||||
"postcss",
|
||||
"postcss-loader",
|
||||
"rand",
|
||||
"rxjs",
|
||||
"sass",
|
||||
"sass-loader",
|
||||
"scopeguard",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simplelog",
|
||||
"style-loader",
|
||||
"sysinfo",
|
||||
"ts-loader",
|
||||
"tsconfig-paths-webpack-plugin",
|
||||
"type-fest",
|
||||
"typenum",
|
||||
@@ -302,6 +229,52 @@
|
||||
commitMessagePrefix: "[deps] Platform:",
|
||||
reviewers: ["team:team-platform-dev"],
|
||||
},
|
||||
{
|
||||
// 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"],
|
||||
},
|
||||
{
|
||||
// We group all webpack build-related minor and patch updates together to reduce PR noise.
|
||||
// We include patch updates here because we want PRs for webpack patch updates and it's in this group.
|
||||
matchPackageNames: [
|
||||
"@babel/core",
|
||||
"@babel/preset-env",
|
||||
"babel-loader",
|
||||
"base64-loader",
|
||||
"browserslist",
|
||||
"copy-webpack-plugin",
|
||||
"css-loader",
|
||||
"html-loader",
|
||||
"html-webpack-injector",
|
||||
"html-webpack-plugin",
|
||||
"mini-css-extract-plugin",
|
||||
"postcss-loader",
|
||||
"postcss",
|
||||
"sass-loader",
|
||||
"sass",
|
||||
"style-loader",
|
||||
"ts-loader",
|
||||
"tsconfig-paths-webpack-plugin",
|
||||
"webpack-cli",
|
||||
"webpack-dev-server",
|
||||
"webpack-node-externals",
|
||||
"webpack",
|
||||
],
|
||||
description: "webpack-related build dependencies",
|
||||
groupName: "Minor and patch webpack updates",
|
||||
matchUpdateTypes: ["minor", "patch"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"@angular-devkit/build-angular",
|
||||
@@ -360,6 +333,11 @@
|
||||
commitMessagePrefix: "[deps] SM:",
|
||||
reviewers: ["team:team-secrets-manager-dev"],
|
||||
},
|
||||
{
|
||||
// We need to update several Jest-related packages together, for version compatibility.
|
||||
groupName: "jest",
|
||||
matchPackageNames: ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"@microsoft/signalr-protocol-msgpack",
|
||||
@@ -428,5 +406,5 @@
|
||||
reviewers: ["team:team-key-management-dev"],
|
||||
},
|
||||
],
|
||||
ignoreDeps: ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"],
|
||||
ignoreDeps: ["@types/koa-bodyparser", "bootstrap", "node-ipc", "@bitwarden/sdk-internal"],
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const config: StorybookConfig = {
|
||||
"../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../libs/tools/card/src/**/*.mdx",
|
||||
"../libs/tools/card/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../libs/angular/src/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
getAbsolutePath("@storybook/addon-links"),
|
||||
|
||||
@@ -3615,6 +3615,14 @@
|
||||
"message": "Use Send to securely share encrypted information with anyone.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendsTitleNoItems": {
|
||||
"message": "Send sensitive information safely",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendsBodyNoItems": {
|
||||
"message": "Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"inputRequired": {
|
||||
"message": "Input is required."
|
||||
},
|
||||
@@ -5019,6 +5027,15 @@
|
||||
"biometricsStatusHelptextUnavailableReasonUnknown": {
|
||||
"message": "Biometric unlock is currently unavailable for an unknown reason."
|
||||
},
|
||||
"unlockVault": {
|
||||
"message": "Unlock your vault in seconds"
|
||||
},
|
||||
"unlockVaultDesc": {
|
||||
"message": "You can customize your unlock and timeout settings to more quickly access your vault."
|
||||
},
|
||||
"unlockPinSet": {
|
||||
"message": "Unlock PIN set"
|
||||
},
|
||||
"authenticating": {
|
||||
"message": "Authenticating"
|
||||
},
|
||||
@@ -5341,6 +5358,23 @@
|
||||
"description": "Two part message",
|
||||
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
|
||||
},
|
||||
"generatorNudgeTitle": {
|
||||
"message": "Quickly create passwords"
|
||||
},
|
||||
"generatorNudgeBodyOne": {
|
||||
"message": "Easily create strong and unique passwords by clicking on",
|
||||
"description": "Two part message",
|
||||
"example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure."
|
||||
},
|
||||
"generatorNudgeBodyTwo": {
|
||||
"message": "to help you keep your logins secure.",
|
||||
"description": "Two part message",
|
||||
"example": "Easily create strong and unique passwords by clicking on {icon} to help you keep your logins secure."
|
||||
},
|
||||
"generatorNudgeBodyAria": {
|
||||
"message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.",
|
||||
"description": "Aria label for the body content of the generator nudge"
|
||||
},
|
||||
"noPermissionsViewPage": {
|
||||
"message": "You do not have permissions to view this page. Try logging in with a different account."
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<bit-spotlight
|
||||
*ngIf="showAccountSecurityNudge$ | async"
|
||||
[title]="'unlockVault' | i18n"
|
||||
[subtitle]="'unlockVaultDesc' | i18n"
|
||||
(onDismiss)="dismissAccountSecurityNudge()"
|
||||
class="tw-mb-6"
|
||||
></bit-spotlight>
|
||||
<div [formGroup]="form">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
|
||||
@@ -4,7 +4,10 @@ import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
@@ -16,14 +19,18 @@ import {
|
||||
VaultTimeoutStringType,
|
||||
VaultTimeoutAction,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { BiometricStateService, BiometricsService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -71,6 +78,13 @@ describe("AccountSecurityComponent", () => {
|
||||
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||
{ provide: VaultTimeoutService, useValue: mock<VaultTimeoutService>() },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||
{ provide: StateProvider, useValue: mock<StateProvider>() },
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: ApiService, useValue: mock<ApiService>() },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(AccountSecurityComponent, {
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import { FingerprintDialogComponent, VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -96,6 +98,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
SelectModule,
|
||||
SpotlightComponent,
|
||||
TypographyModule,
|
||||
VaultTimeoutInputComponent,
|
||||
],
|
||||
@@ -120,6 +123,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
enableAutoBiometricsPrompt: true,
|
||||
});
|
||||
|
||||
protected showAccountSecurityNudge$: Observable<boolean> =
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.vaultNudgesService.showNudgeSpotlight$(NudgeType.AccountSecurity, userId),
|
||||
),
|
||||
);
|
||||
|
||||
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
@@ -142,6 +153,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
private biometricStateService: BiometricStateService,
|
||||
private toastService: ToastService,
|
||||
private biometricsService: BiometricsService,
|
||||
private vaultNudgesService: NudgesService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -402,6 +414,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
protected async dismissAccountSecurityNudge() {
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (!activeAccount) {
|
||||
return;
|
||||
}
|
||||
await this.vaultNudgesService.dismissNudge(NudgeType.AccountSecurity, activeAccount.id);
|
||||
}
|
||||
|
||||
async saveVaultTimeoutAction(value: VaultTimeoutAction) {
|
||||
if (value === VaultTimeoutAction.LogOut) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
@@ -453,8 +473,15 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
|
||||
const requireReprompt = (await this.pinService.getPinLockType(userId)) == "EPHEMERAL";
|
||||
this.form.controls.pinLockWithMasterPassword.setValue(requireReprompt, { emitEvent: false });
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("unlockPinSet"),
|
||||
});
|
||||
await this.vaultNudgesService.dismissNudge(NudgeType.AccountSecurity, userId);
|
||||
} else {
|
||||
await this.vaultTimeoutSettingsService.clear();
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.vaultTimeoutSettingsService.clear(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,10 @@ const config: StorybookConfig = {
|
||||
},
|
||||
],
|
||||
});
|
||||
config.module.rules.push({
|
||||
test: /\.scss$/,
|
||||
use: [require.resolve("css-loader"), require.resolve("sass-loader")],
|
||||
});
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Meta, Controls, Primary } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./cipher-action.lit-stories";
|
||||
|
||||
<Meta title="Components/Ciphers/Cipher Action" of={stories} />
|
||||
|
||||
## Cipher Action
|
||||
|
||||
The `CipherAction` component is a functional UI element that handles actions related to ciphers in a
|
||||
secure environment. Built with the `lit` library and styled for consistency across themes, it
|
||||
provides flexibility and accessibility while supporting various notification types.
|
||||
|
||||
<Primary />
|
||||
<Controls />
|
||||
|
||||
## Props
|
||||
|
||||
| **Prop** | **Type** | **Required** | **Description** |
|
||||
| ------------------ | --------------------------------------------------- | ------------ | -------------------------------------------------------------- |
|
||||
| `handleAction` | `(e: Event) => void` | No | Function to execute when an action is triggered. |
|
||||
| `notificationType` | `NotificationTypes.Change \| NotificationTypes.Add` | Yes | Specifies the type of notification associated with the action. |
|
||||
| `theme` | `Theme` | Yes | The theme to style the component. Must match the `Theme` enum. |
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
1. Ensure the necessary dependencies are installed:
|
||||
|
||||
- `lit`: Used to render the component.
|
||||
|
||||
2. Pass the required props when rendering the component:
|
||||
- `handleAction`: Optional function to handle the triggered action.
|
||||
- `notificationType`: Mandatory type from `NotificationTypes` to define the action context.
|
||||
- `theme`: The styling theme (must be a valid `Theme` enum value).
|
||||
|
||||
## Accessibility (WCAG) Compliance
|
||||
|
||||
The `CipherAction` component is designed to be accessible, ensuring usability across diverse user
|
||||
bases. Below are the key considerations for accessibility:
|
||||
|
||||
### Keyboard Accessibility
|
||||
|
||||
- Fully navigable using the keyboard.
|
||||
- The action can be triggered using the `Enter` or `Space` key for users relying on keyboard
|
||||
interaction.
|
||||
|
||||
### Screen Reader Compatibility
|
||||
|
||||
- The semantic elements used in the `CipherAction` component ensure that assistive technologies can
|
||||
interpret the component correctly.
|
||||
- Text associated with the `notificationType` is programmatically linked, providing clarity for
|
||||
screen reader users.
|
||||
|
||||
### Focus Management
|
||||
|
||||
- The component includes focus styles to ensure visibility during navigation.
|
||||
- Proper focus management ensures the component works seamlessly with keyboard navigation.
|
||||
|
||||
### Visual Feedback
|
||||
|
||||
- Provides distinct visual states for different themes and states:
|
||||
- **Hover:** Adjustments to background, border, and text for enhanced visibility.
|
||||
- **Active:** Highlights the button with a focus state when activated.
|
||||
- **Disabled:** Grays out the component to indicate inactivity.
|
||||
|
||||
## Usage Example
|
||||
|
||||
Here's an example of how to integrate the `CipherAction` component:
|
||||
|
||||
```ts
|
||||
import { CipherAction } from "../../cipher/cipher-action";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
|
||||
|
||||
const handleAction = (e: Event) => {
|
||||
console.log("Cipher action triggered!", e);
|
||||
};
|
||||
|
||||
<CipherAction
|
||||
handleAction={handleAction}
|
||||
notificationType={NotificationTypes.Change}
|
||||
theme={ThemeTypes.Dark}
|
||||
/>;
|
||||
```
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Meta, Controls, Primary } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./cipher-icon.lit-stories";
|
||||
|
||||
<Meta title="Components/Ciphers/Cipher Icon" of={stories} />
|
||||
|
||||
## Cipher Icon
|
||||
|
||||
The `CipherIcon` component is a versatile icon renderer designed for secure environments. It
|
||||
dynamically supports custom icons provided via URIs or displays a default icon (`Globe`) styled
|
||||
based on the theme and provided properties.
|
||||
|
||||
<Primary />
|
||||
<Controls />
|
||||
|
||||
## Props
|
||||
|
||||
| **Prop** | **Type** | **Required** | **Description** |
|
||||
| -------- | ------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `color` | `string` | Yes | A contextual color override applied when the `uri` is not provided, ensuring consistent styling of the default icon. |
|
||||
| `size` | `string` | Yes | A valid CSS `width` value representing the width basis of the graphic. The height adjusts to maintain the original aspect ratio of the graphic. |
|
||||
| `theme` | `Theme` | Yes | The styling theme for the icon, matching the `Theme` enum. |
|
||||
| `uri` | `string` (optional) | No | A URL to an external graphic. If provided, the component displays this icon. If omitted, a default icon (`Globe`) styled with the provided `color` and `theme`. |
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
1. Ensure the necessary dependencies are installed:
|
||||
|
||||
- `lit`: Renders the component.
|
||||
- `@emotion/css`: Styles the component.
|
||||
|
||||
2. Pass the necessary props when using the component:
|
||||
- `color`: Used when no `uri` is provided to style the default icon.
|
||||
- `size`: Defines the width of the icon. Height maintains aspect ratio.
|
||||
- `theme`: Specifies the theme for styling.
|
||||
- `uri` (optional): If provided, this URI is used to display a custom icon.
|
||||
|
||||
## Accessibility (WCAG) Compliance
|
||||
|
||||
The `CipherIcon` component ensures accessible and user-friendly interactions through thoughtful
|
||||
design:
|
||||
|
||||
### Semantic Rendering
|
||||
|
||||
- When the `uri` is provided, the component renders an `<img>` element, which is semantically
|
||||
appropriate for external graphics.
|
||||
- If no `uri` is provided, the default icon is wrapped in a `<span>`, ensuring proper context for
|
||||
screen readers.
|
||||
|
||||
### Visual Feedback
|
||||
|
||||
- The component visually adjusts based on the `size`, `color`, and `theme`, ensuring the icon
|
||||
remains clear and legible across different environments.
|
||||
|
||||
### Keyboard and Screen Reader Support
|
||||
|
||||
- Ensure that any container or parent component provides appropriate `alt` text or labeling when
|
||||
`uri` is used with an `<img>` tag for additional accessibility.
|
||||
|
||||
## Usage Example
|
||||
|
||||
Here's an example of how to integrate the `CipherIcon` component:
|
||||
|
||||
```ts
|
||||
import { CipherIcon } from "./cipher-icon";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
<CipherIcon
|
||||
color="blue"
|
||||
size="32px"
|
||||
theme={ThemeTypes.Light}
|
||||
uri="https://example.com/icon.png"
|
||||
/>;
|
||||
```
|
||||
|
||||
This configuration displays a custom icon from the provided URI with a width of 32px, styled for the
|
||||
light theme. If the URI is omitted, the Globe icon is used as the fallback, colored in blue.
|
||||
|
||||
### Default Styles
|
||||
|
||||
- The default styles ensure responsive and clean design:
|
||||
|
||||
- Width: Defined by the size prop.
|
||||
- Height: Automatically adjusts to maintain the aspect ratio.
|
||||
- Fit Content: Ensures the icon does not overflow or distort its container.
|
||||
|
||||
### Notes
|
||||
|
||||
- Always validate the uri provided to ensure it points to a secure and accessible location.
|
||||
- Use the color and theme props for consistent fallback styling when uri is not provided.
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Meta, Controls, Primary } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./cipher-indicator-icon.lit-stories";
|
||||
|
||||
<Meta title="Components/Ciphers/Cipher Indicator Icon" of={stories} />
|
||||
|
||||
## Cipher Info Indicator Icons
|
||||
|
||||
The `CipherInfoIndicatorIcons` component displays a set of icons indicating specific attributes
|
||||
related to cipher information. It supports business and family organization indicators, styled
|
||||
dynamically based on the provided theme.
|
||||
|
||||
<Primary />
|
||||
<Controls />
|
||||
|
||||
## Props
|
||||
|
||||
| **Prop** | **Type** | **Required** | **Description** |
|
||||
| ------------------ | --------- | ------------ | ----------------------------------------------------------------------- |
|
||||
| `showBusinessIcon` | `boolean` | No | Displays the business organization icon when set to `true`. |
|
||||
| `showFamilyIcon` | `boolean` | No | Displays the family organization icon when set to `true`. |
|
||||
| `theme` | `Theme` | Yes | Defines the theme used to style the icons. Must match the `Theme` enum. |
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
1. Ensure the necessary dependencies are installed:
|
||||
|
||||
- `lit`: Renders the component.
|
||||
- `@emotion/css`: Used for styling.
|
||||
|
||||
2. Pass the required props when using the component:
|
||||
- `showBusinessIcon`: A boolean that, when `true`, displays the business icon.
|
||||
- `showFamilyIcon`: A boolean that, when `true`, displays the family icon.
|
||||
- `theme`: Specifies the theme for styling the icons.
|
||||
|
||||
## Accessibility (WCAG) Compliance
|
||||
|
||||
The `CipherInfoIndicatorIcons` component ensures accessibility and usability through its design:
|
||||
|
||||
### Screen Reader Compatibility
|
||||
|
||||
- Icons are rendered as `<svg>` elements, and parent components should provide appropriate labeling
|
||||
or descriptions to convey their meaning to screen readers.
|
||||
|
||||
### Visual Feedback
|
||||
|
||||
- Icons are styled dynamically based on the `theme` to ensure visual clarity and contrast in all
|
||||
supported themes.
|
||||
- The size of the icons is fixed at `12px` in height to maintain a consistent visual appearance.
|
||||
|
||||
## Usage Example
|
||||
|
||||
Here's an example of how to integrate the `CipherInfoIndicatorIcons` component:
|
||||
|
||||
```ts
|
||||
import { CipherInfoIndicatorIcons } from "./cipher-info-indicator-icons";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
<CipherInfoIndicatorIcons
|
||||
showBusinessIcon={true}
|
||||
showFamilyIcon={false}
|
||||
theme={ThemeTypes.Dark}
|
||||
/>;
|
||||
```
|
||||
|
||||
This example displays the business organization icon, styled for the dark theme, and omits the
|
||||
family organization icon.
|
||||
|
||||
### Styling Details
|
||||
|
||||
- The component includes the following styles:
|
||||
|
||||
- Icons: Rendered as SVGs with a height of 12px and a width that adjusts to maintain their aspect
|
||||
ratio.
|
||||
- Color: Icons are dynamically styled based on the theme, using muted text colors for a subtle
|
||||
appearance.
|
||||
|
||||
### Notes
|
||||
|
||||
- If neither showBusinessIcon nor showFamilyIcon is set to true, the component renders nothing. This
|
||||
behavior should be handled by the parent component.
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
|
||||
import { CipherAction, CipherActionProps } from "../../cipher/cipher-action";
|
||||
import { mockI18n } from "../mock-data";
|
||||
|
||||
export default {
|
||||
title: "Components/Ciphers/Cipher Action",
|
||||
argTypes: {
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
notificationType: {
|
||||
control: "select",
|
||||
options: [NotificationTypes.Change, NotificationTypes.Add],
|
||||
},
|
||||
handleAction: { control: false },
|
||||
},
|
||||
args: {
|
||||
theme: ThemeTypes.Light,
|
||||
notificationType: NotificationTypes.Change,
|
||||
handleAction: () => alert("Action triggered!"),
|
||||
i18n: mockI18n,
|
||||
},
|
||||
} as Meta<CipherActionProps>;
|
||||
|
||||
const Template = (args: CipherActionProps) => CipherAction({ ...args });
|
||||
|
||||
export const Default: StoryObj<CipherActionProps> = {
|
||||
render: Template,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/web-components";
|
||||
import { html } from "lit";
|
||||
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import { CipherIcon, CipherIconProps } from "../../cipher/cipher-icon";
|
||||
|
||||
export default {
|
||||
title: "Components/Ciphers/Cipher Icon",
|
||||
argTypes: {
|
||||
color: { control: "color" },
|
||||
size: { control: "text" },
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
uri: { control: "text" },
|
||||
},
|
||||
args: {
|
||||
size: "50px",
|
||||
theme: ThemeTypes.Light,
|
||||
uri: "",
|
||||
},
|
||||
} as Meta<CipherIconProps>;
|
||||
|
||||
const Template = (args: CipherIconProps) => {
|
||||
return html`
|
||||
<div style="width: ${args.size}; height: ${args.size}; overflow: hidden;">
|
||||
${CipherIcon({ ...args })}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
export const Default: StoryObj<CipherIconProps> = {
|
||||
render: Template,
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/web-components";
|
||||
import { html } from "lit";
|
||||
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import {
|
||||
CipherInfoIndicatorIcons,
|
||||
CipherInfoIndicatorIconsProps,
|
||||
} from "../../cipher/cipher-indicator-icons";
|
||||
import { OrganizationCategories } from "../../cipher/types";
|
||||
|
||||
export default {
|
||||
title: "Components/Ciphers/Cipher Indicator Icons",
|
||||
argTypes: {
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
},
|
||||
args: {
|
||||
theme: ThemeTypes.Light,
|
||||
organizationCategories: [...Object.values(OrganizationCategories)],
|
||||
},
|
||||
} as Meta<CipherInfoIndicatorIconsProps>;
|
||||
|
||||
const Template: StoryObj<CipherInfoIndicatorIconsProps>["render"] = (args) =>
|
||||
html`<div>${CipherInfoIndicatorIcons({ ...args })}</div>`;
|
||||
|
||||
export const Default: StoryObj<CipherInfoIndicatorIconsProps> = {
|
||||
render: Template,
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import { CipherInfo, CipherInfoProps } from "../../cipher/cipher-info";
|
||||
import { mockCiphers } from "../mock-data";
|
||||
|
||||
export default {
|
||||
title: "Components/Ciphers/Cipher Info",
|
||||
argTypes: {
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
},
|
||||
args: {
|
||||
cipher: mockCiphers[0],
|
||||
theme: ThemeTypes.Light,
|
||||
},
|
||||
} as Meta<CipherInfoProps>;
|
||||
|
||||
const Template = (args: CipherInfoProps) => CipherInfo({ ...args });
|
||||
|
||||
export const Default: StoryObj<CipherInfoProps> = {
|
||||
render: Template,
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { Meta, StoryObj } from "@storybook/web-components";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar";
|
||||
import { getConfirmationHeaderMessage } from "../../../../../notification/bar";
|
||||
import {
|
||||
NotificationConfirmationContainer,
|
||||
NotificationConfirmationContainerProps,
|
||||
@@ -35,8 +36,10 @@ export default {
|
||||
},
|
||||
} as Meta<NotificationConfirmationContainerProps>;
|
||||
|
||||
const Template = (args: NotificationConfirmationContainerProps) =>
|
||||
NotificationConfirmationContainer({ ...args });
|
||||
const Template = (args: NotificationConfirmationContainerProps) => {
|
||||
const headerMessage = getConfirmationHeaderMessage(args.i18n, args.type, args.error);
|
||||
return NotificationConfirmationContainer({ ...args, headerMessage });
|
||||
};
|
||||
|
||||
export const Default: StoryObj<NotificationConfirmationContainerProps> = {
|
||||
render: Template,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
|
||||
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
|
||||
import { getNotificationHeaderMessage } from "../../../../notification/bar";
|
||||
import { NotificationContainer, NotificationContainerProps } from "../../notification/container";
|
||||
import { mockBrowserI18nGetMessage, mockI18n } from "../mock-data";
|
||||
|
||||
@@ -46,7 +47,10 @@ export default {
|
||||
},
|
||||
} as Meta<NotificationContainerProps>;
|
||||
|
||||
const Template = (args: NotificationContainerProps) => NotificationContainer({ ...args });
|
||||
const Template = (args: NotificationContainerProps) => {
|
||||
const headerMessage = getNotificationHeaderMessage(args.i18n, args.type);
|
||||
return NotificationContainer({ ...args, headerMessage });
|
||||
};
|
||||
|
||||
export const Default: StoryObj<NotificationContainerProps> = {
|
||||
render: Template,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { html } from "lit";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import { NotificationHeader, NotificationHeaderProps } from "../../notification/header";
|
||||
import { mockI18n } from "../mock-data";
|
||||
|
||||
export default {
|
||||
title: "Components/Notifications/Header",
|
||||
@@ -17,6 +18,7 @@ export default {
|
||||
standalone: true,
|
||||
theme: ThemeTypes.Light,
|
||||
handleCloseNotification: () => alert("Close Clicked"),
|
||||
i18n: mockI18n,
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import { themes } from "../../constants/styles";
|
||||
import { ButtonRow, ButtonRowProps } from "../../rows/button-row";
|
||||
import { mockBrowserI18nGetMessage } from "../mock-data";
|
||||
|
||||
export default {
|
||||
title: "Components/Rows/Button Row",
|
||||
@@ -15,6 +16,49 @@ export default {
|
||||
window.alert("Button clicked!");
|
||||
},
|
||||
},
|
||||
selectButtons: [
|
||||
{
|
||||
id: "select-1",
|
||||
label: "select 1",
|
||||
options: [
|
||||
{
|
||||
text: "item 1",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
text: "item 2",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: "item 3",
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "select-2",
|
||||
label: "select 2",
|
||||
options: [
|
||||
{
|
||||
text: "item a",
|
||||
value: "a",
|
||||
},
|
||||
{
|
||||
text: "item b",
|
||||
value: "b",
|
||||
},
|
||||
{
|
||||
text: "item c",
|
||||
value: "c",
|
||||
},
|
||||
{
|
||||
text: "item d",
|
||||
value: "d",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as Meta<ButtonRowProps>;
|
||||
|
||||
@@ -51,3 +95,10 @@ export const Dark: StoryObj<ButtonRowProps> = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
window.chrome = {
|
||||
...window.chrome,
|
||||
i18n: {
|
||||
getMessage: mockBrowserI18nGetMessage,
|
||||
},
|
||||
} as typeof chrome;
|
||||
|
||||
@@ -3,30 +3,30 @@ import { Meta, StoryObj } from "@storybook/web-components";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
|
||||
import { CipherItem, CipherItemProps } from "../../cipher/cipher-item";
|
||||
import { CipherItemRow, CipherItemRowProps } from "../../rows/cipher-item-row";
|
||||
import { mockCiphers, mockI18n } from "../mock-data";
|
||||
|
||||
export default {
|
||||
title: "Components/Ciphers/Cipher Item",
|
||||
title: "Components/Rows/Cipher Item Row",
|
||||
argTypes: {
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
handleAction: { control: false },
|
||||
notificationType: {
|
||||
control: "select",
|
||||
options: [NotificationTypes.Change, NotificationTypes.Add],
|
||||
options: [...Object.values(NotificationTypes)],
|
||||
},
|
||||
handleAction: { control: false },
|
||||
},
|
||||
args: {
|
||||
cipher: mockCiphers[0],
|
||||
theme: ThemeTypes.Light,
|
||||
notificationType: NotificationTypes.Change,
|
||||
handleAction: () => alert("Clicked"),
|
||||
i18n: mockI18n,
|
||||
notificationType: NotificationTypes.Change,
|
||||
theme: ThemeTypes.Light,
|
||||
handleAction: () => window.alert("clicked!"),
|
||||
},
|
||||
} as Meta<CipherItemProps>;
|
||||
} as Meta<CipherItemRowProps>;
|
||||
|
||||
const Template = (args: CipherItemProps) => CipherItem({ ...args });
|
||||
const Template = (props: CipherItemRowProps) => CipherItemRow({ ...props });
|
||||
|
||||
export const Default: StoryObj<CipherItemProps> = {
|
||||
export const Default: StoryObj<CipherItemRowProps> = {
|
||||
render: Template,
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Meta, StoryObj } from "@storybook/web-components";
|
||||
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
|
||||
import { ItemRow, ItemRowProps } from "../../rows/item-row";
|
||||
|
||||
export default {
|
||||
title: "Components/Rows/Item Row",
|
||||
argTypes: {
|
||||
theme: { control: "select", options: [...Object.values(ThemeTypes)] },
|
||||
children: { control: "object" },
|
||||
},
|
||||
args: {
|
||||
theme: ThemeTypes.Light,
|
||||
},
|
||||
} as Meta<ItemRowProps>;
|
||||
|
||||
const Template = (args: ItemRowProps) => ItemRow({ ...args });
|
||||
|
||||
export const Default: StoryObj<ItemRowProps> = {
|
||||
render: Template,
|
||||
};
|
||||
@@ -4,11 +4,10 @@ import { html } from "lit";
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { NotificationType } from "../../../notification/abstractions/notification-bar";
|
||||
import { CipherItem } from "../cipher";
|
||||
import { NotificationCipherData } from "../cipher/types";
|
||||
import { I18n } from "../common-types";
|
||||
import { scrollbarStyles, spacing, themes, typography } from "../constants/styles";
|
||||
import { ItemRow } from "../rows/item-row";
|
||||
import { CipherItemRow } from "../rows/cipher-item-row";
|
||||
|
||||
export const componentClassPrefix = "notification-body";
|
||||
|
||||
@@ -37,15 +36,12 @@ export function NotificationBody({
|
||||
return html`
|
||||
<div class=${notificationBodyStyles({ isSafari, theme })}>
|
||||
${ciphers.map((cipher) =>
|
||||
ItemRow({
|
||||
CipherItemRow({
|
||||
cipher,
|
||||
theme,
|
||||
children: CipherItem({
|
||||
cipher,
|
||||
i18n,
|
||||
notificationType,
|
||||
theme,
|
||||
handleAction: handleEditOrUpdateAction,
|
||||
}),
|
||||
i18n,
|
||||
notificationType,
|
||||
handleAction: handleEditOrUpdateAction,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ export type NotificationConfirmationContainerProps = NotificationBarIframeInitDa
|
||||
handleOpenTasks: (e: Event) => void;
|
||||
} & {
|
||||
error?: string;
|
||||
headerMessage?: string;
|
||||
i18n: I18n;
|
||||
itemName: string;
|
||||
task?: NotificationTaskInfo;
|
||||
@@ -36,13 +37,13 @@ export function NotificationConfirmationContainer({
|
||||
handleCloseNotification,
|
||||
handleOpenVault,
|
||||
handleOpenTasks,
|
||||
headerMessage,
|
||||
i18n,
|
||||
itemName,
|
||||
task,
|
||||
theme = ThemeTypes.Light,
|
||||
type,
|
||||
}: NotificationConfirmationContainerProps) {
|
||||
const headerMessage = getHeaderMessage(i18n, type, error);
|
||||
const confirmationMessage = getConfirmationMessage(i18n, type, error);
|
||||
const buttonText = error ? i18n.newItem : i18n.view;
|
||||
const buttonAria = error
|
||||
@@ -125,20 +126,3 @@ function getConfirmationMessage(i18n: I18n, type?: NotificationType, error?: str
|
||||
? i18n.notificationLoginSaveConfirmation
|
||||
: i18n.notificationLoginUpdatedConfirmation;
|
||||
}
|
||||
|
||||
function getHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) {
|
||||
if (error) {
|
||||
return i18n.saveFailure;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case NotificationTypes.Add:
|
||||
return i18n.loginSaveSuccess;
|
||||
case NotificationTypes.Change:
|
||||
return i18n.loginUpdateSuccess;
|
||||
case NotificationTypes.Unlock:
|
||||
return "";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export type NotificationContainerProps = NotificationBarIframeInitData & {
|
||||
ciphers?: NotificationCipherData[];
|
||||
collections?: CollectionView[];
|
||||
folders?: FolderView[];
|
||||
headerMessage?: string;
|
||||
i18n: I18n;
|
||||
organizations?: OrgView[];
|
||||
personalVaultIsAllowed?: boolean;
|
||||
@@ -40,13 +41,13 @@ export function NotificationContainer({
|
||||
ciphers,
|
||||
collections,
|
||||
folders,
|
||||
headerMessage,
|
||||
i18n,
|
||||
organizations,
|
||||
personalVaultIsAllowed = true,
|
||||
theme = ThemeTypes.Light,
|
||||
type,
|
||||
}: NotificationContainerProps) {
|
||||
const headerMessage = getHeaderMessage(i18n, type);
|
||||
const showBody = type !== NotificationTypes.Unlock;
|
||||
|
||||
return html`
|
||||
@@ -98,16 +99,3 @@ const notificationContainerStyles = (theme: Theme) => css`
|
||||
padding-right: ${spacing["3"]};
|
||||
}
|
||||
`;
|
||||
|
||||
function getHeaderMessage(i18n: I18n, type?: NotificationType) {
|
||||
switch (type) {
|
||||
case NotificationTypes.Add:
|
||||
return i18n.saveLogin;
|
||||
case NotificationTypes.Change:
|
||||
return i18n.updateLogin;
|
||||
case NotificationTypes.Unlock:
|
||||
return i18n.unlockToSave;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { NotificationType } from "../../../notification/abstractions/notification-bar";
|
||||
import { CipherItem } from "../cipher/cipher-item";
|
||||
import { NotificationCipherData } from "../cipher/types";
|
||||
import { I18n } from "../common-types";
|
||||
import { spacing, themes, typography } from "../constants/styles";
|
||||
|
||||
export type CipherItemRowProps = {
|
||||
cipher: NotificationCipherData;
|
||||
i18n: I18n;
|
||||
notificationType?: NotificationType;
|
||||
theme: Theme;
|
||||
handleAction: (e: Event) => void;
|
||||
};
|
||||
|
||||
export function CipherItemRow({
|
||||
cipher,
|
||||
i18n,
|
||||
notificationType,
|
||||
theme,
|
||||
handleAction,
|
||||
}: CipherItemRowProps) {
|
||||
return html`
|
||||
<div class=${cipherItemRowStyles({ theme })}>
|
||||
${CipherItem({
|
||||
cipher,
|
||||
i18n,
|
||||
notificationType,
|
||||
theme,
|
||||
handleAction,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const cipherItemRowStyles = ({ theme }: { theme: Theme }) => css`
|
||||
${typography.body1}
|
||||
|
||||
gap: ${spacing["2"]};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-width: 0 0 0.5px 0;
|
||||
border-style: solid;
|
||||
border-radius: ${spacing["2"]};
|
||||
border-color: ${themes[theme].secondary["300"]};
|
||||
background-color: ${themes[theme].background.DEFAULT};
|
||||
padding: ${spacing["2"]} ${spacing["3"]};
|
||||
min-height: min-content;
|
||||
max-height: 52px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
color: ${themes[theme].text.main};
|
||||
font-weight: 400;
|
||||
|
||||
> div {
|
||||
:first-child {
|
||||
flex: 3 3 75%;
|
||||
min-width: 25%;
|
||||
}
|
||||
|
||||
:not(:first-child) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
max-width: 25%;
|
||||
|
||||
> button {
|
||||
max-width: min-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,55 +0,0 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { spacing, themes, typography } from "../../../content/components/constants/styles";
|
||||
|
||||
export type ItemRowProps = {
|
||||
theme: Theme;
|
||||
children: TemplateResult | TemplateResult[];
|
||||
};
|
||||
|
||||
export function ItemRow({ theme = ThemeTypes.Light, children }: ItemRowProps) {
|
||||
return html` <div class=${itemRowStyles({ theme })}>${children}</div> `;
|
||||
}
|
||||
|
||||
export const itemRowStyles = ({ theme }: { theme: Theme }) => css`
|
||||
${typography.body1}
|
||||
|
||||
gap: ${spacing["2"]};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-width: 0 0 0.5px 0;
|
||||
border-style: solid;
|
||||
border-radius: ${spacing["2"]};
|
||||
border-color: ${themes[theme].secondary["300"]};
|
||||
background-color: ${themes[theme].background.DEFAULT};
|
||||
padding: ${spacing["2"]} ${spacing["3"]};
|
||||
min-height: min-content;
|
||||
max-height: 52px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
color: ${themes[theme].text.main};
|
||||
font-weight: 400;
|
||||
|
||||
> div {
|
||||
:first-child {
|
||||
flex: 3 3 75%;
|
||||
min-width: 25%;
|
||||
}
|
||||
|
||||
:not(:first-child) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
max-width: 25%;
|
||||
|
||||
> button {
|
||||
max-width: min-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -6,7 +6,7 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view
|
||||
|
||||
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
|
||||
import { NotificationCipherData } from "../content/components/cipher/types";
|
||||
import { CollectionView, OrgView } from "../content/components/common-types";
|
||||
import { CollectionView, I18n, OrgView } from "../content/components/common-types";
|
||||
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container";
|
||||
import { NotificationContainer } from "../content/components/notification/container";
|
||||
import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder";
|
||||
@@ -113,6 +113,68 @@ const findElementById = <ElementType extends HTMLElement>(
|
||||
return element as ElementType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the localized header message for the notification bar based on the notification type.
|
||||
*
|
||||
* @returns The localized header message string, or undefined if the type is not recognized.
|
||||
*/
|
||||
export function getNotificationHeaderMessage(i18n: I18n, type?: NotificationType) {
|
||||
return type
|
||||
? {
|
||||
[NotificationTypes.Add]: i18n.saveLogin,
|
||||
[NotificationTypes.Change]: i18n.updateLogin,
|
||||
[NotificationTypes.Unlock]: i18n.unlockToSave,
|
||||
}[type]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized header message for the confirmation message bar based on the notification type.
|
||||
*
|
||||
* @returns The localized header message string, or undefined if the type is not recognized.
|
||||
*/
|
||||
export function getConfirmationHeaderMessage(i18n: I18n, type?: NotificationType, error?: string) {
|
||||
if (error) {
|
||||
return i18n.saveFailure;
|
||||
}
|
||||
|
||||
return type
|
||||
? {
|
||||
[NotificationTypes.Add]: i18n.loginSaveSuccess,
|
||||
[NotificationTypes.Change]: i18n.loginUpdateSuccess,
|
||||
[NotificationTypes.Unlock]: "",
|
||||
}[type]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the header message to the document title.
|
||||
* If the header message is already present, it avoids duplication.
|
||||
*/
|
||||
export function appendHeaderMessageToTitle(headerMessage?: string) {
|
||||
if (!headerMessage) {
|
||||
return;
|
||||
}
|
||||
const baseTitle = document.title.split(" - ")[0];
|
||||
document.title = `${baseTitle} - ${headerMessage}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the effective notification type to use based on initialization data.
|
||||
*
|
||||
* If the vault is locked, the notification type will be set to `Unlock`.
|
||||
* Otherwise, the type provided in the init data is returned.
|
||||
*
|
||||
* @returns The resolved `NotificationType` to be used for rendering logic.
|
||||
*/
|
||||
function resolveNotificationType(initData: NotificationBarIframeInitData): NotificationType {
|
||||
if (initData.isVaultLocked) {
|
||||
return NotificationTypes.Unlock;
|
||||
}
|
||||
|
||||
return initData.type as NotificationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text content of an element identified by ID within a template's content.
|
||||
*
|
||||
@@ -148,6 +210,10 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
||||
|
||||
if (useComponentBar) {
|
||||
const resolvedType = resolveNotificationType(notificationBarIframeInitData);
|
||||
const headerMessage = getNotificationHeaderMessage(i18n, resolvedType);
|
||||
appendHeaderMessageToTitle(headerMessage);
|
||||
|
||||
document.body.innerHTML = "";
|
||||
// Current implementations utilize a require for scss files which creates the need to remove the node.
|
||||
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
|
||||
@@ -156,7 +222,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
return render(
|
||||
NotificationContainer({
|
||||
...notificationBarIframeInitData,
|
||||
type: NotificationTypes.Unlock,
|
||||
headerMessage,
|
||||
type: resolvedType,
|
||||
theme: resolvedTheme,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
handleCloseNotification,
|
||||
@@ -199,7 +266,8 @@ async function initNotificationBar(message: NotificationBarWindowMessage) {
|
||||
return render(
|
||||
NotificationContainer({
|
||||
...notificationBarIframeInitData,
|
||||
type: notificationBarIframeInitData.type as NotificationType,
|
||||
headerMessage,
|
||||
type: resolvedType,
|
||||
theme: resolvedTheme,
|
||||
personalVaultIsAllowed: !personalVaultDisallowed,
|
||||
handleCloseNotification,
|
||||
@@ -429,6 +497,8 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
|
||||
const { cipherId, task, itemName } = data || {};
|
||||
const i18n = getI18n();
|
||||
const resolvedTheme = getResolvedTheme(theme ?? ThemeTypes.Light);
|
||||
const resolvedType = resolveNotificationType(notificationBarIframeInitData);
|
||||
const headerMessage = getConfirmationHeaderMessage(i18n, resolvedType, error);
|
||||
|
||||
globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000);
|
||||
|
||||
@@ -438,6 +508,7 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
|
||||
type: type as NotificationType,
|
||||
theme: resolvedTheme,
|
||||
handleCloseNotification,
|
||||
headerMessage,
|
||||
i18n,
|
||||
error,
|
||||
itemName: itemName ?? i18n.typeLogin,
|
||||
|
||||
@@ -21,10 +21,16 @@ describe("AutofillInlineMenuList", () => {
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
let autofillInlineMenuList: AutofillInlineMenuList;
|
||||
let autofillInlineMenuList: AutofillInlineMenuList | null;
|
||||
const portKey: string = "inlineMenuListPortKey";
|
||||
const events: { eventName: any; callback: any }[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
const oldEv = globalThis.addEventListener;
|
||||
globalThis.addEventListener = (eventName: any, callback: any) => {
|
||||
events.push({ eventName, callback });
|
||||
oldEv.call(globalThis, eventName, callback);
|
||||
};
|
||||
document.body.innerHTML = `<autofill-inline-menu-list></autofill-inline-menu-list>`;
|
||||
autofillInlineMenuList = document.querySelector("autofill-inline-menu-list");
|
||||
jest.spyOn(globalThis.document, "createElement");
|
||||
@@ -33,6 +39,9 @@ describe("AutofillInlineMenuList", () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
events.forEach(({ eventName, callback }) => {
|
||||
globalThis.removeEventListener(eventName, callback);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initAutofillInlineMenuList", () => {
|
||||
|
||||
@@ -14,6 +14,8 @@ import { RouterModule } from "@angular/router";
|
||||
import { filter, firstValueFrom, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import {
|
||||
@@ -55,7 +57,6 @@ import {
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { NudgesService, NudgeType, SpotlightComponent } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
|
||||
@@ -81,6 +81,7 @@ export class PopupViewCacheService implements ViewCacheService {
|
||||
injector = inject(Injector),
|
||||
initialValue,
|
||||
persistNavigation,
|
||||
clearOnTabChange,
|
||||
} = options;
|
||||
const cachedValue = this.cache[key]?.value
|
||||
? deserializer(JSON.parse(this.cache[key].value))
|
||||
@@ -89,6 +90,7 @@ export class PopupViewCacheService implements ViewCacheService {
|
||||
|
||||
const viewCacheOptions = {
|
||||
...(persistNavigation && { persistNavigation }),
|
||||
...(clearOnTabChange && { clearOnTabChange }),
|
||||
};
|
||||
|
||||
effect(
|
||||
|
||||
@@ -46,22 +46,18 @@ describe("LocalBackedSessionStorage", () => {
|
||||
it("returns a decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
const result = await sut.get("test");
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
encrypted,
|
||||
sessionKey,
|
||||
"browser-session-key",
|
||||
),
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted");
|
||||
});
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
await sut.get("test");
|
||||
expect(sut["cache"]["test"]).toEqual("decrypted");
|
||||
});
|
||||
@@ -69,22 +65,18 @@ describe("LocalBackedSessionStorage", () => {
|
||||
it("returns a decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
const result = await sut.get("test");
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
encrypted,
|
||||
sessionKey,
|
||||
"browser-session-key",
|
||||
),
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted");
|
||||
});
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
await sut.get("test");
|
||||
expect(sut["cache"]["test"]).toEqual("decrypted");
|
||||
});
|
||||
@@ -104,7 +96,7 @@ describe("LocalBackedSessionStorage", () => {
|
||||
|
||||
it("returns true when the key is in local storage", async () => {
|
||||
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
|
||||
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
const result = await sut.has("test");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -119,7 +111,7 @@ describe("LocalBackedSessionStorage", () => {
|
||||
async (nullish) => {
|
||||
localStorage.internalStore["session_test"] = nullish;
|
||||
await expect(sut.has("test")).resolves.toBe(false);
|
||||
expect(encryptService.decryptToUtf8).not.toHaveBeenCalled();
|
||||
expect(encryptService.decryptString).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -127,7 +119,7 @@ describe("LocalBackedSessionStorage", () => {
|
||||
describe("save", () => {
|
||||
const encString = makeEncString("encrypted");
|
||||
beforeEach(() => {
|
||||
encryptService.encrypt.mockResolvedValue(encString);
|
||||
encryptService.encryptString.mockResolvedValue(encString);
|
||||
});
|
||||
|
||||
it("logs a warning when saving the same value twice and in a dev environment", async () => {
|
||||
@@ -157,7 +149,10 @@ describe("LocalBackedSessionStorage", () => {
|
||||
|
||||
it("encrypts and saves the value to local storage", async () => {
|
||||
await sut.save("test", "value");
|
||||
expect(encryptService.encrypt).toHaveBeenCalledWith(JSON.stringify("value"), sessionKey);
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(
|
||||
JSON.stringify("value"),
|
||||
sessionKey,
|
||||
);
|
||||
expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString);
|
||||
});
|
||||
|
||||
|
||||
@@ -118,11 +118,7 @@ export class LocalBackedSessionStorageService
|
||||
return null;
|
||||
}
|
||||
|
||||
const valueJson = await this.encryptService.decryptToUtf8(
|
||||
new EncString(local),
|
||||
encKey,
|
||||
"browser-session-key",
|
||||
);
|
||||
const valueJson = await this.encryptService.decryptString(new EncString(local), encKey);
|
||||
if (valueJson == null) {
|
||||
// error with decryption, value is lost, delete state and start over
|
||||
await this.localStorage.remove(this.sessionStorageKey(key));
|
||||
@@ -139,7 +135,10 @@ export class LocalBackedSessionStorageService
|
||||
}
|
||||
|
||||
const valueJson = JSON.stringify(value);
|
||||
const encValue = await this.encryptService.encrypt(valueJson, await this.sessionKey.get());
|
||||
const encValue = await this.encryptService.encryptString(
|
||||
valueJson,
|
||||
await this.sessionKey.get(),
|
||||
);
|
||||
await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { switchMap, delay, filter, concatMap } from "rxjs";
|
||||
import { switchMap, delay, filter, concatMap, map, first, of } from "rxjs";
|
||||
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import {
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GlobalStateProvider,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { fromChromeEvent } from "../browser/from-chrome-event";
|
||||
|
||||
const popupClosedPortName = "new_popup";
|
||||
@@ -21,6 +22,12 @@ export type ViewCacheOptions = {
|
||||
* Optional flag to persist the cached value between navigation events.
|
||||
*/
|
||||
persistNavigation?: boolean;
|
||||
|
||||
/**
|
||||
* When set, the cached value will be cleared when the user changes tabs.
|
||||
* @optional
|
||||
*/
|
||||
clearOnTabChange?: true;
|
||||
};
|
||||
|
||||
export type ViewCacheState = {
|
||||
@@ -129,6 +136,37 @@ export class PopupViewCacheBackgroundService {
|
||||
),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// On tab changed, excluding extension tabs
|
||||
fromChromeEvent(chrome.tabs.onActivated)
|
||||
.pipe(
|
||||
switchMap((tabs) => BrowserApi.getTab(tabs[0].tabId)!),
|
||||
switchMap((tab) => {
|
||||
// FireFox sets the `url` to "about:blank" and won't populate the `url` until the `onUpdated` event
|
||||
if (tab.url !== "about:blank") {
|
||||
return of(tab);
|
||||
}
|
||||
|
||||
return fromChromeEvent(chrome.tabs.onUpdated).pipe(
|
||||
first(),
|
||||
switchMap(([tabId]) => BrowserApi.getTab(tabId)!),
|
||||
);
|
||||
}),
|
||||
map((tab) => tab.url || tab.pendingUrl),
|
||||
filter((url) => !url?.startsWith(chrome.runtime.getURL(""))),
|
||||
switchMap(() =>
|
||||
this.popupViewCacheState.update((state) => {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
// Only remove keys that are marked with `clearOnTabChange`
|
||||
return Object.fromEntries(
|
||||
Object.entries(state).filter(([, { options }]) => !options?.clearOnTabChange),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async clearState() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { combineLatest, map, Observable, switchMap } from "rxjs";
|
||||
import { combineLatest, map, Observable, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { NudgesService } from "@bitwarden/angular/vault";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Icons } from "@bitwarden/components";
|
||||
import { NudgesService } from "@bitwarden/vault";
|
||||
|
||||
import { NavButton } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
|
||||
@@ -23,6 +23,7 @@ export class TabsV2Component {
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM8851_BrowserOnboardingNudge),
|
||||
this.hasActiveBadges$,
|
||||
]).pipe(
|
||||
startWith([false, false]),
|
||||
map(([onboardingFeatureEnabled, hasBadges]) => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -20,15 +20,29 @@
|
||||
*ngIf="listState === sendState.Empty"
|
||||
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
|
||||
>
|
||||
<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>
|
||||
<tools-new-send-dropdown
|
||||
[hideIcon]="true"
|
||||
*ngIf="!sendsDisabled"
|
||||
slot="button"
|
||||
></tools-new-send-dropdown>
|
||||
</bit-no-items>
|
||||
<ng-container *ngIf="!(showSendSpotlight$ | async)">
|
||||
<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>
|
||||
<tools-new-send-dropdown
|
||||
[hideIcon]="true"
|
||||
*ngIf="!sendsDisabled"
|
||||
slot="button"
|
||||
></tools-new-send-dropdown>
|
||||
</bit-no-items>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showSendSpotlight$ | async">
|
||||
<bit-no-items [icon]="noItemIcon" class="tw-text-main">
|
||||
<ng-container slot="title">{{ "sendsTitleNoItems" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "sendsBodyNoItems" | i18n }}</ng-container>
|
||||
<tools-new-send-dropdown
|
||||
[hideIcon]="true"
|
||||
*ngIf="!sendsDisabled"
|
||||
slot="button"
|
||||
[buttonType]="'secondary'"
|
||||
></tools-new-send-dropdown>
|
||||
</bit-no-items>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="listState !== sendState.Empty">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of, BehaviorSubject } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService } from "@bitwarden/angular/vault";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -121,6 +122,7 @@ describe("SendV2Component", () => {
|
||||
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
||||
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: NudgesService, useValue: mock<NudgesService>() },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, OnDestroy } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { combineLatest, switchMap } from "rxjs";
|
||||
import { combineLatest, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -12,13 +12,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { ButtonModule, CalloutModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
import {
|
||||
NoSendsIcon,
|
||||
NewSendDropdownComponent,
|
||||
SendListItemsContainerComponent,
|
||||
NoSendsIcon,
|
||||
SendItemsService,
|
||||
SendSearchComponent,
|
||||
SendListFiltersComponent,
|
||||
SendListFiltersService,
|
||||
SendListItemsContainerComponent,
|
||||
SendSearchComponent,
|
||||
} from "@bitwarden/send-ui";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
@@ -46,14 +46,13 @@ export enum SendState {
|
||||
JslibModule,
|
||||
CommonModule,
|
||||
ButtonModule,
|
||||
RouterLink,
|
||||
NewSendDropdownComponent,
|
||||
SendListItemsContainerComponent,
|
||||
SendListFiltersComponent,
|
||||
SendSearchComponent,
|
||||
],
|
||||
})
|
||||
export class SendV2Component implements OnInit, OnDestroy {
|
||||
export class SendV2Component implements OnDestroy {
|
||||
sendType = SendType;
|
||||
sendState = SendState;
|
||||
|
||||
@@ -63,6 +62,12 @@ export class SendV2Component implements OnInit, OnDestroy {
|
||||
protected title: string = "allSends";
|
||||
protected noItemIcon = NoSendsIcon;
|
||||
protected noResultsIcon = Icons.NoResults;
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||
protected showSendSpotlight$: Observable<boolean> = this.activeUserId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.nudgesService.showNudgeSpotlight$(NudgeType.SendNudgeStatus, userId),
|
||||
),
|
||||
);
|
||||
|
||||
protected sendsDisabled = false;
|
||||
|
||||
@@ -71,6 +76,7 @@ export class SendV2Component implements OnInit, OnDestroy {
|
||||
protected sendListFiltersService: SendListFiltersService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
private nudgesService: NudgesService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.sendItemsService.emptyList$,
|
||||
@@ -111,7 +117,5 @@ export class SendV2Component implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
|
||||
<span
|
||||
*ngIf="downloadBitwardenNudgeStatus$ | async"
|
||||
*ngIf="showDownloadBitwardenNudge$ | async"
|
||||
bitBadge
|
||||
variant="notification"
|
||||
[attr.aria-label]="'nudgeBadgeAria' | i18n"
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BadgeComponent, ItemModule } from "@bitwarden/components";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/vault";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
||||
@@ -51,7 +51,7 @@ export class SettingsV2Component implements OnInit {
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
downloadBitwardenNudgeStatus$: Observable<boolean> = this.authenticatedAccount$.pipe(
|
||||
showDownloadBitwardenNudge$: Observable<boolean> = this.authenticatedAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id),
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../../../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
@@ -70,6 +72,7 @@ class QueryParams {
|
||||
this.uri = params.uri;
|
||||
this.username = params.username;
|
||||
this.name = params.name;
|
||||
this.prefillNameAndURIFromTab = params.prefillNameAndURIFromTab;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,6 +119,12 @@ class QueryParams {
|
||||
* Optional name to pre-fill for the cipher.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Optional flag to pre-fill the name and URI from the current tab.
|
||||
* NOTE: This will override the `uri` and `name` query parameters if set to true.
|
||||
*/
|
||||
prefillNameAndURIFromTab?: true;
|
||||
}
|
||||
|
||||
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
@@ -281,8 +290,7 @@ export class AddEditV2Component implements OnInit {
|
||||
if (config.mode === "edit" && !config.originalCipher.edit) {
|
||||
config.mode = "partial-edit";
|
||||
}
|
||||
|
||||
config.initialValues = this.setInitialValuesFromParams(params);
|
||||
config.initialValues = await this.setInitialValuesFromParams(params);
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
@@ -326,7 +334,7 @@ export class AddEditV2Component implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
setInitialValuesFromParams(params: QueryParams) {
|
||||
async setInitialValuesFromParams(params: QueryParams) {
|
||||
const initialValues = {} as OptionalInitialValues;
|
||||
if (params.folderId) {
|
||||
initialValues.folderId = params.folderId;
|
||||
@@ -346,6 +354,14 @@ export class AddEditV2Component implements OnInit {
|
||||
if (params.name) {
|
||||
initialValues.name = params.name;
|
||||
}
|
||||
|
||||
if (params.prefillNameAndURIFromTab) {
|
||||
const tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
initialValues.loginUri = tab.url;
|
||||
initialValues.name = Utils.getHostname(tab.url);
|
||||
}
|
||||
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export class AssignCollections {
|
||||
combineLatest([cipher$, this.collectionService.decryptedCollections$])
|
||||
.pipe(takeUntilDestroyed(), first())
|
||||
.subscribe(([cipherView, collections]) => {
|
||||
let availableCollections = collections.filter((c) => !c.readOnly);
|
||||
let availableCollections = collections;
|
||||
const organizationId = (cipherView?.organizationId as OrganizationId) ?? null;
|
||||
|
||||
// If the cipher is already a part of an organization,
|
||||
|
||||
@@ -94,8 +94,7 @@ describe("NewItemDropdownV2Component", () => {
|
||||
collectionId: "777-888-999",
|
||||
organizationId: "444-555-666",
|
||||
folderId: "222-333-444",
|
||||
uri: "https://example.com",
|
||||
name: "example.com",
|
||||
prefillNameAndURIFromTab: "true",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
import { RouterLink } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
@@ -35,10 +34,8 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
*/
|
||||
@Input()
|
||||
initialValues: NewItemInitialValues;
|
||||
constructor(
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
constructor(private dialogService: DialogService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
@@ -47,13 +44,12 @@ export class NewItemDropdownV2Component implements OnInit {
|
||||
buildQueryParams(type: CipherType): AddEditQueryParams {
|
||||
const poppedOut = BrowserPopupUtils.inPopout(window);
|
||||
|
||||
const loginDetails: { uri?: string; name?: string } = {};
|
||||
const loginDetails: { prefillNameAndURIFromTab?: string } = {};
|
||||
|
||||
// When a Login Cipher is created and the extension is not popped out,
|
||||
// pass along the uri and name
|
||||
if (!poppedOut && type === CipherType.Login && this.tab) {
|
||||
loginDetails.uri = this.tab.url;
|
||||
loginDetails.name = Utils.getHostname(this.tab.url);
|
||||
loginDetails.prefillNameAndURIFromTab = "true";
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -30,13 +31,7 @@ import {
|
||||
NoItemsModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
DecryptionFailureDialogComponent,
|
||||
NudgesService,
|
||||
NudgeType,
|
||||
SpotlightComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
@@ -159,7 +154,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
private introCarouselService: IntroCarouselService,
|
||||
private nudgesService: NudgesService,
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.vaultPopupItemsService.emptyVault$,
|
||||
|
||||
@@ -4,10 +4,10 @@ import { RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CardComponent, LinkModule, TypographyModule } from "@bitwarden/components";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/vault";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { FileResponse } from "../models/response/file.response";
|
||||
@@ -25,15 +23,15 @@ export abstract class DownloadCommand {
|
||||
/**
|
||||
* Fetches an attachment via the url, decrypts it's content and saves it to a file
|
||||
* @param url - url used to retrieve the attachment
|
||||
* @param key - SymmetricCryptoKey to decrypt the file contents
|
||||
* @param fileName - filename used when written to disk
|
||||
* @param decrypt - Function used to decrypt the response
|
||||
* @param output - If output is empty or `--raw` was passed to the initial command the content is output onto stdout
|
||||
* @returns Promise<FileResponse>
|
||||
*/
|
||||
protected async saveAttachmentToFile(
|
||||
url: string,
|
||||
key: SymmetricCryptoKey,
|
||||
fileName: string,
|
||||
decrypt: (resp: globalThis.Response) => Promise<Uint8Array>,
|
||||
output?: string,
|
||||
) {
|
||||
const response = await this.apiService.nativeFetch(
|
||||
@@ -46,8 +44,7 @@ export abstract class DownloadCommand {
|
||||
}
|
||||
|
||||
try {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
|
||||
const decBuf = await decrypt(response);
|
||||
if (process.env.BW_SERVE === "true") {
|
||||
const res = new FileResponse(Buffer.from(decBuf), fileName);
|
||||
return Response.success(res);
|
||||
|
||||
@@ -195,7 +195,7 @@ export class EditCommand {
|
||||
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage),
|
||||
);
|
||||
const request = new CollectionRequest();
|
||||
request.name = (await this.encryptService.encrypt(req.name, orgKey)).encryptedString;
|
||||
request.name = (await this.encryptService.encryptString(req.name, orgKey)).encryptedString;
|
||||
request.externalId = req.externalId;
|
||||
request.groups = groups;
|
||||
request.users = users;
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
@@ -345,12 +345,11 @@ export class GetCommand extends DownloadCommand {
|
||||
return Response.multipleResults(attachments.map((a) => a.id));
|
||||
}
|
||||
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const canAccessPremium = await firstValueFrom(
|
||||
this.accountProfileService.hasPremiumFromAnySource$(account.id),
|
||||
this.accountProfileService.hasPremiumFromAnySource$(activeUserId),
|
||||
);
|
||||
if (!canAccessPremium) {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const originalCipher = await this.cipherService.get(cipher.id, activeUserId);
|
||||
if (originalCipher == null || originalCipher.organizationId == null) {
|
||||
return Response.error("Premium status is required to use this feature.");
|
||||
@@ -374,11 +373,20 @@ export class GetCommand extends DownloadCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const key =
|
||||
attachments[0].key != null
|
||||
? attachments[0].key
|
||||
: await this.keyService.getOrgKey(cipher.organizationId);
|
||||
return await this.saveAttachmentToFile(url, key, attachments[0].fileName, options.output);
|
||||
const decryptBufferFn = (resp: globalThis.Response) =>
|
||||
this.cipherService.getDecryptedAttachmentBuffer(
|
||||
cipher.id as CipherId,
|
||||
attachments[0],
|
||||
resp,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
return await this.saveAttachmentToFile(
|
||||
url,
|
||||
attachments[0].fileName,
|
||||
decryptBufferFn,
|
||||
options.output,
|
||||
);
|
||||
}
|
||||
|
||||
private async getFolder(id: string) {
|
||||
@@ -453,10 +461,9 @@ export class GetCommand extends DownloadCommand {
|
||||
|
||||
const response = await this.apiService.getCollectionAccessDetails(options.organizationId, id);
|
||||
const decCollection = new CollectionView(response);
|
||||
decCollection.name = await this.encryptService.decryptToUtf8(
|
||||
decCollection.name = await this.encryptService.decryptString(
|
||||
new EncString(response.name),
|
||||
orgKey,
|
||||
`orgkey-${options.organizationId}`,
|
||||
);
|
||||
const groups =
|
||||
response.groups == null
|
||||
|
||||
@@ -61,7 +61,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||
if (sessionKey == null) {
|
||||
throw new Error("No session key available.");
|
||||
}
|
||||
const encValue = await this.encryptService.encryptToBytes(
|
||||
const encValue = await this.encryptService.encryptFileData(
|
||||
Utils.fromB64ToArray(plainValue),
|
||||
sessionKey,
|
||||
);
|
||||
@@ -80,7 +80,7 @@ export class NodeEnvSecureStorageService implements AbstractStorageService {
|
||||
}
|
||||
|
||||
const encBuf = EncArrayBuffer.fromB64(encValue);
|
||||
const decValue = await this.encryptService.decryptToBytes(encBuf, sessionKey);
|
||||
const decValue = await this.encryptService.decryptFileData(encBuf, sessionKey);
|
||||
if (decValue == null) {
|
||||
this.logService.info("Failed to decrypt.");
|
||||
return null;
|
||||
|
||||
@@ -874,7 +874,7 @@ export class ServiceContainer {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
await Promise.all([
|
||||
this.eventUploadService.uploadEvents(userId as UserId),
|
||||
this.keyService.clearKeys(),
|
||||
this.keyService.clearKeys(userId),
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
this.collectionService.clear(userId),
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access";
|
||||
@@ -98,10 +99,16 @@ export class SendReceiveCommand extends DownloadCommand {
|
||||
this.sendAccessRequest,
|
||||
apiUrl,
|
||||
);
|
||||
|
||||
const decryptBufferFn = async (resp: globalThis.Response) => {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(resp);
|
||||
return this.encryptService.decryptFileData(encBuf, this.decKey);
|
||||
};
|
||||
|
||||
return await this.saveAttachmentToFile(
|
||||
downloadData.url,
|
||||
this.decKey,
|
||||
response?.file?.fileName,
|
||||
decryptBufferFn,
|
||||
options.output,
|
||||
);
|
||||
}
|
||||
|
||||
44
apps/desktop/desktop_native/Cargo.lock
generated
44
apps/desktop/desktop_native/Cargo.lock
generated
@@ -103,12 +103,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -162,7 +162,7 @@ dependencies = [
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"url",
|
||||
"zbus 5.7.0",
|
||||
"zbus 5.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -504,9 +504,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
@@ -2255,6 +2255,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "oo7"
|
||||
version = "0.4.3"
|
||||
@@ -2280,8 +2286,8 @@ dependencies = [
|
||||
"sha2",
|
||||
"subtle",
|
||||
"tokio",
|
||||
"zbus 5.7.0",
|
||||
"zbus_macros 5.7.0",
|
||||
"zbus 5.7.1",
|
||||
"zbus_macros 5.7.1",
|
||||
"zeroize",
|
||||
"zvariant 5.5.3",
|
||||
]
|
||||
@@ -2354,9 +2360,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
|
||||
checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -2940,9 +2946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -3326,9 +3332,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.35.1"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a"
|
||||
checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
@@ -4735,9 +4741,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.7.0"
|
||||
version = "5.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88232b74ba057a0c85472ec1bae8a17569960be17da2d5e5ad30d5efe7ea6719"
|
||||
checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-recursion",
|
||||
@@ -4756,7 +4762,7 @@ dependencies = [
|
||||
"uds_windows",
|
||||
"windows-sys 0.59.0",
|
||||
"winnow",
|
||||
"zbus_macros 5.7.0",
|
||||
"zbus_macros 5.7.1",
|
||||
"zbus_names 4.2.0",
|
||||
"zvariant 5.5.3",
|
||||
]
|
||||
@@ -4776,9 +4782,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.7.0"
|
||||
version = "5.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6969c06899233334676e60da1675740539cf034ee472a6c5b5c54e50a0a554c9"
|
||||
checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
|
||||
@@ -14,10 +14,10 @@ anyhow = "=1.0.94"
|
||||
arboard = { version = "=3.5.0", default-features = false }
|
||||
argon2 = "=0.5.3"
|
||||
base64 = "=0.22.1"
|
||||
bindgen = "0.71.1"
|
||||
bindgen = "=0.71.1"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
|
||||
byteorder = "=1.5.0"
|
||||
bytes = "1.9.0"
|
||||
bytes = "=1.9.0"
|
||||
cbc = "=0.1.2"
|
||||
core-foundation = "=0.10.0"
|
||||
dirs = "=6.0.0"
|
||||
@@ -49,7 +49,7 @@ sha2 = "=0.10.8"
|
||||
simplelog = "=0.12.2"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = {version = "=0.6.7", default-features = false }
|
||||
sysinfo = "0.35.0"
|
||||
sysinfo = "=0.35.0"
|
||||
thiserror = "=2.0.12"
|
||||
tokio = "=1.45.0"
|
||||
tokio-stream = "=0.1.15"
|
||||
|
||||
@@ -110,9 +110,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
|
||||
@@ -506,7 +506,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
await this.updateRequirePasswordOnStart();
|
||||
}
|
||||
|
||||
await this.vaultTimeoutSettingsService.clear();
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
await this.vaultTimeoutSettingsService.clear(userId);
|
||||
}
|
||||
|
||||
this.messagingService.send("redrawMenu");
|
||||
|
||||
@@ -3703,6 +3703,15 @@
|
||||
"changeAtRiskPassword": {
|
||||
"message": "Change at-risk password"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
"collections": {
|
||||
"content": "$1",
|
||||
"example": "Work, Personal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"move": {
|
||||
"message": "Move"
|
||||
},
|
||||
|
||||
@@ -110,7 +110,7 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
|
||||
const encKey = await this.encryptService.encrypt(clientKeyHalf, userKey);
|
||||
const encKey = await this.encryptService.encryptString(clientKeyHalf, userKey);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*ngIf="cipherId && action === 'view'"
|
||||
[cipherId]="cipherId"
|
||||
[collectionId]="activeFilter?.selectedCollectionId"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
(onCloneCipher)="cloneCipherWithoutPasswordPrompt($event)"
|
||||
(onEditCipher)="editCipher($event)"
|
||||
(onViewCipherPasswordHistory)="viewCipherPasswordHistory($event)"
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
@@ -46,6 +47,7 @@ const BroadcasterSubscriptionId = "ViewComponent";
|
||||
})
|
||||
export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Output() onViewCipherPasswordHistory = new EventEmitter<CipherView>();
|
||||
@Input() masterPasswordAlreadyPrompted: boolean = false;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@@ -120,6 +122,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
}
|
||||
});
|
||||
});
|
||||
this.passwordReprompted = this.masterPasswordAlreadyPrompted;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -134,6 +137,7 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.passwordReprompted = this.masterPasswordAlreadyPrompted;
|
||||
}
|
||||
|
||||
viewHistory() {
|
||||
|
||||
@@ -362,8 +362,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
if (this.organization.canEditAllCiphers) {
|
||||
return collections;
|
||||
}
|
||||
// The user is only allowed to add/edit items to assigned collections that are not readonly
|
||||
return collections.filter((c) => c.assigned && !c.readOnly);
|
||||
return collections.filter((c) => c.assigned);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<input bitInput appAutofocus type="text" formControlName="name" />
|
||||
<bit-hint>{{ "characterMaximum" | i18n: 100 }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-form-field *ngIf="isExternalIdVisible">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -147,6 +146,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
return this.params.organizationId;
|
||||
}
|
||||
|
||||
protected get isExternalIdVisible(): boolean {
|
||||
return !!this.groupForm.get("externalId")?.value;
|
||||
}
|
||||
|
||||
protected get editMode(): boolean {
|
||||
return this.groupId != null;
|
||||
}
|
||||
@@ -227,10 +230,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
this.groupDetails$,
|
||||
]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null));
|
||||
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(map((isEnabled) => !isEnabled || !!this.groupForm.get("externalId")?.value));
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
|
||||
|
||||
@@ -177,13 +177,13 @@
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
</ng-container>
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-form-field *ngIf="isExternalIdVisible">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field *ngIf="isSsoExternalIdVisible$ | async">
|
||||
<bit-form-field *ngIf="isSsoExternalIdVisible">
|
||||
<bit-label>{{ "ssoExternalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="ssoExternalId" />
|
||||
<bit-hint>{{ "ssoExternalIdDesc" | i18n }}</bit-hint>
|
||||
|
||||
@@ -157,28 +157,20 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
manageResetPassword: false,
|
||||
});
|
||||
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return !isEnabled || !!this.formGroup.get("externalId")?.value;
|
||||
}),
|
||||
);
|
||||
get isExternalIdVisible(): boolean {
|
||||
return !!this.formGroup.get("externalId")?.value;
|
||||
}
|
||||
|
||||
protected isSsoExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return isEnabled && !!this.formGroup.get("ssoExternalId")?.value;
|
||||
}),
|
||||
);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
get isSsoExternalIdVisible(): boolean {
|
||||
return !!this.formGroup.get("ssoExternalId")?.value;
|
||||
}
|
||||
|
||||
get customUserTypeSelected(): boolean {
|
||||
return this.formGroup.value.type === OrganizationUserType.Custom;
|
||||
}
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
isEditDialogParams(
|
||||
params: EditMemberDialogParams | AddMemberDialogParams,
|
||||
): params is EditMemberDialogParams {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
|
||||
import { deepLinkGuard } from "../../auth/guards/deep-link/deep-link.guard";
|
||||
|
||||
import { VaultModule } from "./collections/vault.module";
|
||||
import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard";
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-form-field *ngIf="isExternalIdVisible">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
|
||||
@@ -38,7 +38,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
@@ -135,7 +134,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
protected showOrgSelector = false;
|
||||
protected formGroup = this.formBuilder.group({
|
||||
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
|
||||
externalId: "",
|
||||
externalId: { value: "", disabled: true },
|
||||
parent: undefined as string | undefined,
|
||||
access: [[] as AccessItemValue[]],
|
||||
selectedOrg: "",
|
||||
@@ -145,16 +144,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
protected showAddAccessWarning = false;
|
||||
protected collections: Collection[];
|
||||
protected buttonDisplayName: ButtonType = ButtonType.Save;
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return (
|
||||
!isEnabled ||
|
||||
(!!this.params.isAdminConsoleActive && !!this.formGroup.get("externalId")?.value)
|
||||
);
|
||||
}),
|
||||
);
|
||||
private orgExceedingCollectionLimit!: Organization;
|
||||
|
||||
constructor(
|
||||
@@ -165,7 +154,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
private groupService: GroupApiService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private dialogService: DialogService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@@ -354,6 +342,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
return this.formGroup.controls.selectedOrg;
|
||||
}
|
||||
|
||||
protected get isExternalIdVisible(): boolean {
|
||||
return this.params.isAdminConsoleActive && !!this.formGroup.get("externalId")?.value;
|
||||
}
|
||||
|
||||
protected get collectionId() {
|
||||
return this.params.collectionId;
|
||||
}
|
||||
@@ -490,23 +482,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
private handleFormGroupReadonly(readonly: boolean) {
|
||||
if (readonly) {
|
||||
this.formGroup.controls.name.disable();
|
||||
this.formGroup.controls.externalId.disable();
|
||||
this.formGroup.controls.parent.disable();
|
||||
this.formGroup.controls.access.disable();
|
||||
} else {
|
||||
this.formGroup.controls.name.enable();
|
||||
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((isEnabled) => {
|
||||
if (isEnabled) {
|
||||
this.formGroup.controls.externalId.disable();
|
||||
} else {
|
||||
this.formGroup.controls.externalId.enable();
|
||||
}
|
||||
});
|
||||
|
||||
this.formGroup.controls.parent.enable();
|
||||
this.formGroup.controls.access.enable();
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
this.keyService.clearKeys(),
|
||||
this.keyService.clearKeys(userId),
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
this.collectionService.clear(userId),
|
||||
|
||||
@@ -98,7 +98,7 @@ export class WebLoginComponentService
|
||||
const enforcedPasswordPolicyOptions = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { RouterService } from "../../core/router.service";
|
||||
|
||||
/**
|
||||
* Guard to persist and apply deep links to handle users who are not unlocked.
|
||||
* @returns returns true. If user is not Unlocked will store URL to state for redirect once
|
||||
* user is unlocked/Authenticated.
|
||||
*/
|
||||
export function deepLinkGuard(): CanActivateFn {
|
||||
return async (route, routerState) => {
|
||||
// Inject Services
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
const routerService = inject(RouterService);
|
||||
|
||||
// Fetch State
|
||||
const currentUrl = routerState.url;
|
||||
const transientPreviousUrl = routerService.getPreviousUrl();
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
|
||||
// Evaluate State
|
||||
/** before anything else, check if the user is already unlocked. */
|
||||
if (authStatus === AuthenticationStatus.Unlocked) {
|
||||
const persistedPreLoginUrl = await routerService.getAndClearLoginRedirectUrl();
|
||||
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
||||
return router.navigateByUrl(persistedPreLoginUrl);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* At this point the user is either `locked` or `loggedOut`, it doesn't matter.
|
||||
* We opt to persist the currentUrl over the transient previousUrl. This supports
|
||||
* the case where a user is locked out of their vault and they deep link from
|
||||
* the "lock" page.
|
||||
*
|
||||
* When the user is locked out of their vault the currentUrl contains "lock" so it will
|
||||
* not be persisted, the previousUrl will be persisted instead.
|
||||
*/
|
||||
if (isValidUrl(currentUrl)) {
|
||||
await routerService.persistLoginRedirectUrl(currentUrl);
|
||||
} else if (isValidUrl(transientPreviousUrl)) {
|
||||
await routerService.persistLoginRedirectUrl(transientPreviousUrl);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
function isValidUrl(url: string | null | undefined): boolean {
|
||||
return !Utils.isNullOrEmpty(url) && !url?.toLocaleLowerCase().includes("/lock");
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
||||
import { RouterService } from "../../core/router.service";
|
||||
import { RouterService } from "../../../core/router.service";
|
||||
|
||||
import { deepLinkGuard } from "./deep-link.guard";
|
||||
|
||||
@@ -99,6 +99,18 @@ describe("Deep Link Guard", () => {
|
||||
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not persist routerService.previousUrl when routerService.previousUrl contains "login-initiated"', async () => {
|
||||
// Arrange
|
||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
|
||||
routerService.getPreviousUrl.mockReturnValue("/login-initiated");
|
||||
|
||||
// Act
|
||||
await routerHarness.navigateByUrl("/lock-route");
|
||||
|
||||
// Assert
|
||||
expect(routerService.persistLoginRedirectUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Story: User's vault times out and previousUrl is undefined
|
||||
it("should not persist routerService.previousUrl when routerService.previousUrl is undefined", async () => {
|
||||
// Arrange
|
||||
99
apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts
Normal file
99
apps/web/src/app/auth/guards/deep-link/deep-link.guard.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { RouterService } from "../../../core/router.service";
|
||||
|
||||
/**
|
||||
* Guard to persist and apply deep links to handle users who are not unlocked.
|
||||
* @returns returns true. If user is not Unlocked will store URL to state for redirect once
|
||||
* user is unlocked/Authenticated.
|
||||
*/
|
||||
export function deepLinkGuard(): CanActivateFn {
|
||||
return async (route, routerState) => {
|
||||
// Inject Services
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
const routerService = inject(RouterService);
|
||||
|
||||
// Fetch State
|
||||
const currentUrl = routerState.url;
|
||||
const transientPreviousUrl = routerService.getPreviousUrl();
|
||||
const authStatus = await authService.getAuthStatus();
|
||||
|
||||
// Evaluate State
|
||||
/** before anything else, check if the user is already unlocked. */
|
||||
if (authStatus === AuthenticationStatus.Unlocked) {
|
||||
const persistedPreLoginUrl: string | undefined =
|
||||
await routerService.getAndClearLoginRedirectUrl();
|
||||
if (persistedPreLoginUrl === undefined) {
|
||||
// Url us undefined, so there is nothing to navigate to.
|
||||
return true;
|
||||
}
|
||||
// Check if the url is empty or null
|
||||
if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) {
|
||||
// const urlTree: string | UrlTree = persistedPreLoginUrl;
|
||||
return router.navigateByUrl(persistedPreLoginUrl);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* At this point the user is either `locked` or `loggedOut`, it doesn't matter.
|
||||
* We opt to persist the currentUrl over the transient previousUrl. This supports
|
||||
* the case where a user is locked out of their vault and they deep link from
|
||||
* the "lock" page.
|
||||
*
|
||||
* When the user is locked out of their vault the currentUrl contains "lock" so it will
|
||||
* not be persisted, the previousUrl will be persisted instead.
|
||||
*/
|
||||
if (isValidUrl(currentUrl)) {
|
||||
await routerService.persistLoginRedirectUrl(currentUrl);
|
||||
} else if (isValidUrl(transientPreviousUrl) && transientPreviousUrl !== undefined) {
|
||||
await routerService.persistLoginRedirectUrl(transientPreviousUrl);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the URL is valid for deep linking. A valid url is described as not including
|
||||
* "lock" or "login-initiated". Valid urls are only urls that are not part of login or
|
||||
* decryption flows.
|
||||
* We ignore the "lock" url because standard SSO flows will send users to the lock component.
|
||||
* We ignore "login-initiated" because TDE users decrypting with master passwords are
|
||||
* sent to the lock component.
|
||||
* @param url The URL to check.
|
||||
* @returns True if the URL is valid, false otherwise.
|
||||
*/
|
||||
function isValidUrl(url: string | null | undefined): boolean {
|
||||
if (url === undefined || url === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Utils.isNullOrEmpty(url)) {
|
||||
return false;
|
||||
}
|
||||
const lowerCaseUrl: string = url.toLocaleLowerCase();
|
||||
|
||||
/**
|
||||
* "Login-initiated" ignored because it is used for TDE users decrypting from a new device. A TDE user
|
||||
* can opt to decrypt using their password. Decrypting with a password will send the user to the lock component,
|
||||
* which is protected by the deep link guard. We don't persist the `login-initiated` url because it is not a
|
||||
* valid deep-link. We don't want users to be sent to the login-initiated url when they are unlocked.
|
||||
* If we did navigate to the login-initiated url, the user would get caught by the TDE Guard and be sent
|
||||
* to the vault and not the intended deep link.
|
||||
*
|
||||
* "Lock" is ignored because users cannot deep-link to the lock component if they are already unlocked.
|
||||
* Users logging in with SSO will be sent to the lock component after they are authenticated with their IdP.
|
||||
* SSO users would be navigated to the "lock" component loop if we persisted the "lock" url.
|
||||
*/
|
||||
|
||||
if (lowerCaseUrl.includes("/login-initiated") || lowerCaseUrl.includes("/lock")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
23
apps/web/src/app/auth/guards/deep-link/readme.md
Normal file
23
apps/web/src/app/auth/guards/deep-link/readme.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Deep-link Guard
|
||||
|
||||
The `deep-link.guard.ts` supports users who are trying to access a protected route from an unauthenticated or locked state.
|
||||
|
||||
This guard will persist the protected URL to session state when a user is either unauthenticated or in an encrypted/locked state. This allows users to have multiple tabs of the application running simultaneously without interfering with 'previousUrl` functionality.
|
||||
|
||||
Writing to session state allows users who are authenticating through SSO to be routed to their identity provider and back without losing the protected route they were trying to access in the first place.
|
||||
|
||||
The deep link guard will not persist Urls that are in the middle of authentication or decryption. SSO users will sometimes have to decrypt their vault after a successful authentication. This is why we do not persist the `/lock` route.
|
||||
|
||||
## General operation
|
||||
|
||||
The `deep-link.guard.ts` will always return true. The `deep-link.guard.ts` will only persist a URL if the user is in an unauthenticated or locked state. The URL cannot contain `/lock` or `/login-initiated`. The persisted URL is cleared from state when it is read.
|
||||
|
||||
## Routes to protect
|
||||
|
||||
The deep link guards should be used on routes where a user will be navigated to a protected route but may not be authenticated, decrypted, or have an account.
|
||||
|
||||
A use cases is the `emergency-access` route which is a link that is sent to the user's email address, and in order for them to accept the request, they must first authenticate and decrypt.
|
||||
|
||||
## TDE Users decrypting/unlocking with password
|
||||
|
||||
For TDE users opting to decrypt with a password they will be routed from the `login-initiated` to the `lock` route. We ignore the `login-initiated` route for this reason allowing TDE users who decrypt/unlock with a password to still be navigated to the initial request.
|
||||
@@ -627,6 +627,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get passwordManagerSubtotal() {
|
||||
if (!this.selectedPlan || !this.selectedPlan.PasswordManager) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let subTotal = this.selectedPlan.PasswordManager.basePrice;
|
||||
if (this.selectedPlan.PasswordManager.hasAdditionalSeatsOption) {
|
||||
subTotal += this.passwordManagerSeatTotal(this.selectedPlan);
|
||||
@@ -638,10 +642,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
secretsManagerSubtotal() {
|
||||
this.secretsManagerTotal = 0;
|
||||
const plan = this.selectedSecretsManagerPlan;
|
||||
const plan = this.selectedPlan;
|
||||
if (!plan || !plan.SecretsManager) {
|
||||
return this.secretsManagerTotal || 0;
|
||||
}
|
||||
|
||||
if (!this.organization.useSecretsManager) {
|
||||
if (this.secretsManagerTotal) {
|
||||
return this.secretsManagerTotal;
|
||||
}
|
||||
|
||||
@@ -653,6 +659,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get passwordManagerSeats() {
|
||||
if (!this.selectedPlan) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.selectedPlan.productTier === ProductTierType.Families) {
|
||||
return this.selectedPlan.PasswordManager.baseSeats;
|
||||
}
|
||||
@@ -660,7 +670,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get total() {
|
||||
if (this.organization && this.organization.useSecretsManager) {
|
||||
if (!this.organization || !this.selectedPlan) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.organization.useSecretsManager) {
|
||||
return (
|
||||
this.passwordManagerSubtotal +
|
||||
this.additionalStorageTotal(this.selectedPlan) +
|
||||
@@ -680,6 +694,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get additionalServiceAccount() {
|
||||
if (!this.currentPlan || !this.currentPlan.SecretsManager) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const baseServiceAccount = this.currentPlan.SecretsManager?.baseServiceAccount || 0;
|
||||
const usedServiceAccounts = this.sub?.smServiceAccounts || 0;
|
||||
|
||||
@@ -1096,8 +1114,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
get submitButtonLabel(): string {
|
||||
if (
|
||||
this.organization &&
|
||||
this.sub &&
|
||||
this.organization.productTierType !== ProductTierType.Free &&
|
||||
this.sub.subscription.status === "canceled"
|
||||
this.sub.subscription?.status === "canceled"
|
||||
) {
|
||||
return this.i18nService.t("restart");
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,6 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendWithIdRequest } from "@bitwarden/common/tools/send/models/request/send-with-id.request";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
EmergencyAccessTrustComponent,
|
||||
KeyRotationTrustInfoComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../../auth";
|
||||
@@ -96,6 +96,11 @@ describe("KeyRotationService", () => {
|
||||
const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("test-public-key")];
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(PureCrypto, "make_user_key_aes256_cbc_hmac").mockReturnValue(new Uint8Array(64));
|
||||
jest.spyOn(PureCrypto, "make_user_key_xchacha20_poly1305").mockReturnValue(new Uint8Array(70));
|
||||
jest
|
||||
.spyOn(PureCrypto, "encrypt_user_key_with_master_password")
|
||||
.mockReturnValue("mockNewUserKey");
|
||||
mockUserVerificationService = mock<UserVerificationService>();
|
||||
mockApiService = mock<UserKeyRotationApiService>();
|
||||
mockCipherService = mock<CipherService>();
|
||||
@@ -158,6 +163,7 @@ describe("KeyRotationService", () => {
|
||||
mockToastService,
|
||||
mockI18nService,
|
||||
mockDialogService,
|
||||
mockConfigService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -181,7 +187,7 @@ describe("KeyRotationService", () => {
|
||||
} as any,
|
||||
]);
|
||||
mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
mockEncryptService.wrapSymmetricKey.mockResolvedValue({
|
||||
encryptedString: "mockEncryptedData",
|
||||
@@ -286,6 +292,59 @@ describe("KeyRotationService", () => {
|
||||
expect(arg.accountUnlockData.emergencyAccessUnlockData.length).toBe(1);
|
||||
expect(arg.accountUnlockData.organizationAccountRecoveryUnlockData.length).toBe(1);
|
||||
expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2);
|
||||
expect(PureCrypto.make_user_key_aes256_cbc_hmac).toHaveBeenCalled();
|
||||
expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith(
|
||||
new Uint8Array(64),
|
||||
"newMasterPassword",
|
||||
mockUser.email,
|
||||
DEFAULT_KDF_CONFIG.toSdkConfig(),
|
||||
);
|
||||
expect(PureCrypto.make_user_key_xchacha20_poly1305).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rotates the userkey to xchacha20poly1305 and encrypted data and changes master password when featureflag is active", async () => {
|
||||
mockConfigService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
KeyRotationTrustInfoComponent.open = initialPromptedOpenTrue;
|
||||
EmergencyAccessTrustComponent.open = emergencyAccessTrustOpenTrusted;
|
||||
AccountRecoveryTrustComponent.open = accountRecoveryTrustOpenTrusted;
|
||||
await keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"newMasterPassword",
|
||||
mockUser,
|
||||
);
|
||||
|
||||
expect(mockApiService.postUserKeyUpdateV2).toHaveBeenCalled();
|
||||
const arg = mockApiService.postUserKeyUpdateV2.mock.calls[0][0];
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.masterKeyEncryptedUserKey).toBe(
|
||||
"mockNewUserKey",
|
||||
);
|
||||
expect(arg.oldMasterKeyAuthenticationHash).toBe("mockMasterPasswordHash");
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.email).toBe("mockEmail");
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.kdfType).toBe(
|
||||
DEFAULT_KDF_CONFIG.kdfType,
|
||||
);
|
||||
expect(arg.accountUnlockData.masterPasswordUnlockData.kdfIterations).toBe(
|
||||
DEFAULT_KDF_CONFIG.iterations,
|
||||
);
|
||||
|
||||
expect(arg.accountKeys.accountPublicKey).toBe(Utils.fromUtf8ToB64("mockPublicKey"));
|
||||
expect(arg.accountKeys.userKeyEncryptedAccountPrivateKey).toBe("mockEncryptedData");
|
||||
|
||||
expect(arg.accountData.ciphers.length).toBe(2);
|
||||
expect(arg.accountData.folders.length).toBe(2);
|
||||
expect(arg.accountData.sends.length).toBe(2);
|
||||
expect(arg.accountUnlockData.emergencyAccessUnlockData.length).toBe(1);
|
||||
expect(arg.accountUnlockData.organizationAccountRecoveryUnlockData.length).toBe(1);
|
||||
expect(arg.accountUnlockData.passkeyUnlockData.length).toBe(2);
|
||||
expect(PureCrypto.make_user_key_aes256_cbc_hmac).not.toHaveBeenCalled();
|
||||
expect(PureCrypto.encrypt_user_key_with_master_password).toHaveBeenCalledWith(
|
||||
new Uint8Array(70),
|
||||
"newMasterPassword",
|
||||
mockUser.email,
|
||||
DEFAULT_KDF_CONFIG.toSdkConfig(),
|
||||
);
|
||||
expect(PureCrypto.make_user_key_xchacha20_poly1305).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns early when first trust warning dialog is declined", async () => {
|
||||
@@ -344,21 +403,6 @@ describe("KeyRotationService", () => {
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("throws if user key creation fails", async () => {
|
||||
mockKeyService.makeUserKey.mockResolvedValueOnce([
|
||||
null as unknown as UserKey,
|
||||
null as unknown as EncString,
|
||||
]);
|
||||
|
||||
await expect(
|
||||
keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"mockMasterPassword",
|
||||
"mockMasterPassword1",
|
||||
mockUser,
|
||||
),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("legacy throws if no private key is found", async () => {
|
||||
privateKey.next(null);
|
||||
|
||||
|
||||
@@ -5,14 +5,17 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -26,6 +29,7 @@ import {
|
||||
EmergencyAccessTrustComponent,
|
||||
KeyRotationTrustInfoComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
import { PureCrypto } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
import { WebauthnLoginAdminService } from "../../auth/core";
|
||||
@@ -59,6 +63,7 @@ export class UserKeyRotationService {
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -116,8 +121,22 @@ export class UserKeyRotationService {
|
||||
|
||||
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
|
||||
|
||||
const [newUnencryptedUserKey, newMasterKeyEncryptedUserKey] =
|
||||
await this.keyService.makeUserKey(newMasterKey);
|
||||
let userKeyBytes: Uint8Array;
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.EnrollAeadOnKeyRotation)) {
|
||||
userKeyBytes = PureCrypto.make_user_key_xchacha20_poly1305();
|
||||
} else {
|
||||
userKeyBytes = PureCrypto.make_user_key_aes256_cbc_hmac();
|
||||
}
|
||||
|
||||
const newMasterKeyEncryptedUserKey = new EncString(
|
||||
PureCrypto.encrypt_user_key_with_master_password(
|
||||
userKeyBytes,
|
||||
newMasterPassword,
|
||||
email,
|
||||
kdfConfig.toSdkConfig(),
|
||||
),
|
||||
);
|
||||
const newUnencryptedUserKey = new SymmetricCryptoKey(userKeyBytes) as UserKey;
|
||||
|
||||
if (!newUnencryptedUserKey || !newMasterKeyEncryptedUserKey) {
|
||||
this.logService.info("[Userkey rotation] User key could not be created. Aborting!");
|
||||
|
||||
@@ -48,7 +48,7 @@ import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/m
|
||||
import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
|
||||
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
|
||||
import { deepLinkGuard } from "./auth/guards/deep-link/deep-link.guard";
|
||||
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
|
||||
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||
|
||||
@@ -933,7 +933,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
if (orgId && orgId !== "MyVault") {
|
||||
const organization = this.allOrganizations.find((o) => o.id === orgId);
|
||||
availableCollections = this.allCollections.filter(
|
||||
(c) => c.organizationId === organization.id && !c.readOnly,
|
||||
(c) => c.organizationId === organization.id,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,23 +10,20 @@
|
||||
</head>
|
||||
|
||||
<body class="layout_frontend">
|
||||
<div class="mt-5 d-flex justify-content-center">
|
||||
<div>
|
||||
<img
|
||||
src="../images/logo-primary@2x.png"
|
||||
class="mb-4"
|
||||
style="display: block; max-width: 290px; margin: 0 auto"
|
||||
alt="Bitwarden"
|
||||
/>
|
||||
<div id="content">
|
||||
<p class="text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
|
||||
title="Loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</p>
|
||||
</div>
|
||||
<div class="tw-p-8 tw-flex tw-flex-col tw-items-center">
|
||||
<img
|
||||
src="../images/logo-primary@2x.png"
|
||||
class="tw-mb-4 tw-block tw-max-w-xs tw-mx-auto"
|
||||
alt="Bitwarden"
|
||||
/>
|
||||
<div id="content">
|
||||
<p class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="Loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -108,7 +108,7 @@ function displayHandoffMessage(client: string) {
|
||||
if (!content) {
|
||||
throw new Error("content element not found");
|
||||
}
|
||||
content.className = "text-center";
|
||||
content.className = "tw-text-center";
|
||||
content.innerHTML = "";
|
||||
|
||||
const h1 = document.createElement("h1");
|
||||
@@ -123,8 +123,8 @@ function displayHandoffMessage(client: string) {
|
||||
? localeService.t("thisWindowWillCloseIn5Seconds")
|
||||
: localeService.t("youMayCloseThisWindow");
|
||||
|
||||
h1.className = "font-weight-semibold";
|
||||
p.className = "mb-4";
|
||||
h1.className = "tw-font-semibold";
|
||||
p.className = "tw-mb-4";
|
||||
|
||||
content.appendChild(h1);
|
||||
content.appendChild(p);
|
||||
@@ -133,7 +133,8 @@ function displayHandoffMessage(client: string) {
|
||||
if (client == "web") {
|
||||
const button = document.createElement("button");
|
||||
button.textContent = localeService.t("close");
|
||||
button.className = "bg-primary text-white border-0 rounded py-2 px-3";
|
||||
button.className =
|
||||
"tw-bg-primary-600 hover:tw-bg-primary-700 tw-text-contrast tw-px-4 tw-py-2 tw-rounded-md tw-transition tw-border-transparent tw-text-center focus:tw-outline-none";
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
window.close();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractio
|
||||
import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard";
|
||||
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
|
||||
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
|
||||
import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link.guard";
|
||||
import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link/deep-link.guard";
|
||||
|
||||
import { SsoComponent } from "../../auth/sso/sso.component";
|
||||
|
||||
|
||||
@@ -117,9 +117,15 @@ export class SetupComponent implements OnInit, OnDestroy {
|
||||
|
||||
submit = async () => {
|
||||
try {
|
||||
const requireProviderPaymentMethodDuringSetup = await firstValueFrom(
|
||||
this.requireProviderPaymentMethodDuringSetup$,
|
||||
);
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
const paymentValid = this.paymentComponent.validate();
|
||||
const paymentValid = requireProviderPaymentMethodDuringSetup
|
||||
? this.paymentComponent.validate()
|
||||
: true;
|
||||
const taxInformationValid = this.taxInformationComponent.validate();
|
||||
|
||||
if (!paymentValid || !taxInformationValid || !this.formGroup.valid) {
|
||||
@@ -146,10 +152,6 @@ export class SetupComponent implements OnInit, OnDestroy {
|
||||
request.taxInfo.city = taxInformation.city;
|
||||
request.taxInfo.state = taxInformation.state;
|
||||
|
||||
const requireProviderPaymentMethodDuringSetup = await firstValueFrom(
|
||||
this.requireProviderPaymentMethodDuringSetup$,
|
||||
);
|
||||
|
||||
if (requireProviderPaymentMethodDuringSetup) {
|
||||
request.paymentSource = await this.paymentComponent.tokenize();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { unauthGuardFn } from "@bitwarden/angular/auth/guards";
|
||||
import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular";
|
||||
import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link.guard";
|
||||
import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link/deep-link.guard";
|
||||
import { RouteDataProperties } from "@bitwarden/web-vault/app/core";
|
||||
|
||||
import { ProvidersModule } from "./admin-console/providers/providers.module";
|
||||
|
||||
@@ -123,9 +123,6 @@ const mockCryptoService = () => {
|
||||
encryptService.decryptString
|
||||
.calledWith(expect.any(EncString), expect.anything())
|
||||
.mockResolvedValue("DECRYPTED_STRING");
|
||||
encryptService.decryptToUtf8
|
||||
.calledWith(expect.any(EncString), expect.anything(), expect.anything())
|
||||
.mockResolvedValue("DECRYPTED_STRING");
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
|
||||
|
||||
|
||||
@@ -51,11 +51,6 @@ describe("DefaultvNextCollectionService", () => {
|
||||
.mockImplementation((encString, key) =>
|
||||
Promise.resolve(encString.data.replace("ENC_", "DEC_")),
|
||||
);
|
||||
encryptService.decryptToUtf8
|
||||
.calledWith(expect.any(EncString), expect.any(SymmetricCryptoKey), expect.any(String))
|
||||
.mockImplementation((encString, key) =>
|
||||
Promise.resolve(encString.data.replace("ENC_", "DEC_")),
|
||||
);
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(keyService, encryptService);
|
||||
|
||||
@@ -109,15 +104,13 @@ describe("DefaultvNextCollectionService", () => {
|
||||
|
||||
// Assert that the correct org keys were used for each encrypted string
|
||||
// This should be replaced with decryptString when the platform PR (https://github.com/bitwarden/clients/pull/14544) is merged
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection1.name)),
|
||||
orgKey1,
|
||||
expect.any(String),
|
||||
);
|
||||
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
expect.objectContaining(new EncString(collection2.name)),
|
||||
orgKey2,
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -111,16 +111,16 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Opens the self-hosted settings dialog when the self-hosted option is selected.
|
||||
*/
|
||||
if (
|
||||
option === Region.SelfHosted &&
|
||||
(await SelfHostedEnvConfigDialogComponent.open(this.dialogService))
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
|
||||
if (option === Region.SelfHosted) {
|
||||
const dialogResult = await SelfHostedEnvConfigDialogComponent.open(this.dialogService);
|
||||
if (dialogResult) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
}
|
||||
// Don't proceed to setEnvironment when the self-hosted dialog is cancelled
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ type BaseCacheOptions<T> = {
|
||||
* Optional flag to persist the cached value between navigation events.
|
||||
*/
|
||||
persistNavigation?: boolean;
|
||||
|
||||
/**
|
||||
* When set, the cached value will be cleared when the user changes tabs.
|
||||
* @optional
|
||||
*/
|
||||
clearOnTabChange?: true;
|
||||
} & (T extends JsonValue ? Deserializer<T> : Required<Deserializer<T>>);
|
||||
|
||||
export type SignalCacheOptions<T> = BaseCacheOptions<T> & {
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
cipherType = CipherType;
|
||||
|
||||
private previousCipherId: string;
|
||||
private passwordReprompted = false;
|
||||
protected passwordReprompted = false;
|
||||
|
||||
/**
|
||||
* Represents TOTP information including display formatting and timing
|
||||
|
||||
3
libs/angular/src/vault/index.ts
Normal file
3
libs/angular/src/vault/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Note: Nudge related code is exported from `libs/angular` because it is consumed by multiple
|
||||
// `libs/*` packages. Exporting from the `libs/vault` package creates circular dependencies.
|
||||
export { NudgesService, NudgeStatus, NudgeType } from "./services/nudges.service";
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { Observable, combineLatest, from, of } from "rxjs";
|
||||
import { catchError, map } from "rxjs/operators";
|
||||
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DefaultSingleNudgeService } from "../default-single-nudge.service";
|
||||
import { NudgeStatus, NudgeType } from "../nudges.service";
|
||||
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class AccountSecurityNudgeService extends DefaultSingleNudgeService {
|
||||
private vaultProfileService = inject(VaultProfileService);
|
||||
private logService = inject(LogService);
|
||||
private pinService = inject(PinServiceAbstraction);
|
||||
private vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService);
|
||||
|
||||
nudgeStatus$(nudgeType: NudgeType, userId: UserId): Observable<NudgeStatus> {
|
||||
const profileDate$ = from(this.vaultProfileService.getProfileCreationDate(userId)).pipe(
|
||||
catchError(() => {
|
||||
this.logService.error("Failed to load profile date:");
|
||||
// Default to today to ensure the nudge is shown in case of an error
|
||||
return of(new Date());
|
||||
}),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
profileDate$,
|
||||
this.getNudgeStatus$(nudgeType, userId),
|
||||
of(Date.now() - THIRTY_DAYS_MS),
|
||||
from(this.pinService.isPinSet(userId)),
|
||||
from(this.vaultTimeoutSettingsService.isBiometricLockSet(userId)),
|
||||
]).pipe(
|
||||
map(([profileCreationDate, status, profileCutoff, isPinSet, isBiometricLockSet]) => {
|
||||
const profileOlderThanCutoff = profileCreationDate.getTime() < profileCutoff;
|
||||
const hideNudge = profileOlderThanCutoff || isPinSet || isBiometricLockSet;
|
||||
return {
|
||||
hasBadgeDismissed: status.hasBadgeDismissed || hideNudge,
|
||||
hasSpotlightDismissed: status.hasSpotlightDismissed || hideNudge,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./autofill-nudge.service";
|
||||
export * from "./account-security-nudge.service";
|
||||
export * from "./has-items-nudge.service";
|
||||
export * from "./download-bitwarden-nudge.service";
|
||||
export * from "./empty-vault-nudge.service";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user