mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 13:10:17 +00:00
Merge branch 'main' into uif/CL-622/dialog-as-drawer
This commit is contained in:
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -8,7 +8,8 @@
|
||||
apps/desktop/desktop_native @bitwarden/team-platform-dev
|
||||
apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
|
||||
## No ownership for Cargo.toml to allow dependency updates
|
||||
## No ownership fo Cargo.lock and Cargo.toml to allow dependency updates
|
||||
apps/desktop/desktop_native/Cargo.lock
|
||||
apps/desktop/desktop_native/Cargo.toml
|
||||
|
||||
## Auth team files ##
|
||||
|
||||
11
.github/workflows/build-cli.yml
vendored
11
.github/workflows/build-cli.yml
vendored
@@ -12,12 +12,13 @@ on:
|
||||
- 'cf-pages'
|
||||
paths:
|
||||
- 'apps/cli/**'
|
||||
- 'bitwarden_license/bit-cli/**'
|
||||
- 'bitwarden_license/bit-common/**'
|
||||
- 'libs/**'
|
||||
- '*'
|
||||
- '!*.md'
|
||||
- '!*.txt'
|
||||
- '.github/workflows/build-cli.yml'
|
||||
- 'bitwarden_license/bit-cli/**'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
@@ -25,12 +26,13 @@ on:
|
||||
- 'hotfix-rc-cli'
|
||||
paths:
|
||||
- 'apps/cli/**'
|
||||
- 'bitwarden_license/bit-cli/**'
|
||||
- 'bitwarden_license/bit-common/**'
|
||||
- 'libs/**'
|
||||
- '*'
|
||||
- '!*.md'
|
||||
- '!*.txt'
|
||||
- '.github/workflows/build-cli.yml'
|
||||
- 'bitwarden_license/bit-cli/**'
|
||||
workflow_call:
|
||||
inputs: {}
|
||||
workflow_dispatch:
|
||||
@@ -87,6 +89,7 @@ jobs:
|
||||
os:
|
||||
[
|
||||
{ base: "linux", distro: "ubuntu-22.04", target_suffix: "" },
|
||||
{ base: "linux", distro: "ubuntu-22.04-arm", target_suffix: "-arm64" },
|
||||
{ base: "mac", distro: "macos-13", target_suffix: "" },
|
||||
{ base: "mac", distro: "macos-14", target_suffix: "-arm64" }
|
||||
]
|
||||
@@ -130,7 +133,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
@@ -306,7 +309,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
|
||||
111
.github/workflows/build-desktop.yml
vendored
111
.github/workflows/build-desktop.yml
vendored
@@ -201,7 +201,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
TARGET: musl
|
||||
run: |
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
node build.js cross-platform
|
||||
node build.js --target=x86_64-unknown-linux-musl --release
|
||||
|
||||
- name: Build application
|
||||
run: npm run dist:lin
|
||||
@@ -298,6 +298,103 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
linux-arm64:
|
||||
name: Linux ARM64 Build
|
||||
# Note, before updating the ubuntu version of the workflow, ensure the snap base image
|
||||
# is equal or greater than the new version. Otherwise there might be GLIBC version issues.
|
||||
# The snap base for desktop is defined in `apps/desktop/electron-builder.json`
|
||||
# We intentionally keep this runner on the oldest supported OS in GitHub Actions
|
||||
# for maximum compatibility across GLIBC versions
|
||||
runs-on: ubuntu-22.04-arm
|
||||
needs: setup
|
||||
env:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ env._NODE_VERSION }}
|
||||
|
||||
- name: Set up environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install pkg-config libxss-dev rpm musl-dev musl-tools flatpak flatpak-builder
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
snap --version
|
||||
snapcraft --version || echo 'snapcraft unavailable'
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
working-directory: ./
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
artifacts: sdk-internal
|
||||
repo: bitwarden/sdk-internal
|
||||
path: ../sdk-internal
|
||||
if_no_artifact_found: fail
|
||||
|
||||
- name: Override SDK
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
working-directory: ./
|
||||
run: |
|
||||
ls -l ../
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
apps/desktop/desktop_native/napi/*.node
|
||||
apps/desktop/desktop_native/dist/*
|
||||
${{ env.RUNNER_TEMP }}/.cargo/registry
|
||||
${{ env.RUNNER_TEMP }}/.cargo/git
|
||||
key: rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }}
|
||||
|
||||
- name: Build Native Module
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: apps/desktop/desktop_native
|
||||
env:
|
||||
PKG_CONFIG_ALLOW_CROSS: true
|
||||
PKG_CONFIG_ALL_STATIC: true
|
||||
TARGET: musl
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
node build.js --target=aarch64-unknown-linux-musl --release
|
||||
|
||||
- name: Build application
|
||||
run: npm run dist:lin:arm64
|
||||
|
||||
- name: Upload tar.gz artifact
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: bitwarden_${{ env._PACKAGE_VERSION }}_arm64.tar.gz
|
||||
path: apps/desktop/dist/bitwarden_desktop_arm64.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
windows:
|
||||
name: Windows Build
|
||||
runs-on: windows-2022
|
||||
@@ -369,7 +466,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
@@ -705,7 +802,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
@@ -895,7 +992,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
@@ -1144,7 +1241,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
@@ -1425,7 +1522,7 @@ jobs:
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
|
||||
4
.github/workflows/build-web.yml
vendored
4
.github/workflows/build-web.yml
vendored
@@ -12,6 +12,8 @@ on:
|
||||
- 'cf-pages'
|
||||
paths:
|
||||
- 'apps/web/**'
|
||||
- 'bitwarden_license/bit-common/**'
|
||||
- 'bitwarden_license/bit-web/**'
|
||||
- 'libs/**'
|
||||
- '*'
|
||||
- '!*.md'
|
||||
@@ -24,6 +26,8 @@ on:
|
||||
- 'hotfix-rc-web'
|
||||
paths:
|
||||
- 'apps/web/**'
|
||||
- 'bitwarden_license/bit-common/**'
|
||||
- 'bitwarden_license/bit-web/**'
|
||||
- 'libs/**'
|
||||
- '*'
|
||||
- '!*.md'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2025.3.2",
|
||||
"version": "2025.4.0",
|
||||
"scripts": {
|
||||
"build": "npm run build:chrome",
|
||||
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"appName": {
|
||||
"message": "Bitwarden"
|
||||
},
|
||||
"appLogoLabel": {
|
||||
"message": "Bitwarden logo"
|
||||
},
|
||||
"extName": {
|
||||
"message": "Bitwarden Password Manager",
|
||||
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
[showBackButton]="showBackButton"
|
||||
[pageTitle]="''"
|
||||
>
|
||||
<bit-icon *ngIf="showLogo" class="tw-inline-flex" [icon]="logo"></bit-icon>
|
||||
<bit-icon
|
||||
*ngIf="showLogo"
|
||||
class="tw-inline-flex"
|
||||
[icon]="logo"
|
||||
[ariaLabel]="'appLogoLabel' | i18n"
|
||||
></bit-icon>
|
||||
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Icon, IconModule, Translation } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
@@ -36,6 +37,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
|
||||
AnonLayoutComponent,
|
||||
CommonModule,
|
||||
CurrentAccountComponent,
|
||||
I18nPipe,
|
||||
IconModule,
|
||||
PopOutComponent,
|
||||
PopupPageComponent,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { html, TemplateResult } from "lit";
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { themes } from "../../../content/components/constants/styles";
|
||||
import { Business, Users } from "../../../content/components/icons";
|
||||
import { Business, Family } from "../../../content/components/icons";
|
||||
|
||||
import { OrganizationCategories, OrganizationCategory } from "./types";
|
||||
|
||||
@@ -13,7 +13,7 @@ const cipherIndicatorIconsMap: Record<
|
||||
(args: { color: string; theme: Theme }) => TemplateResult
|
||||
> = {
|
||||
[OrganizationCategories.business]: Business,
|
||||
[OrganizationCategories.family]: Users,
|
||||
[OrganizationCategories.family]: Family,
|
||||
};
|
||||
|
||||
export function CipherInfoIndicatorIcons({
|
||||
|
||||
@@ -4,14 +4,14 @@ import { html } from "lit";
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Collection({ color, disabled, theme }: IconProps) {
|
||||
export function CollectionShared({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
|
||||
<path
|
||||
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||
d="M3.5.75A.75.75 0 0 1 4.25 0h5.5a.75.75 0 0 1 0 1.5h-5.5A.75.75 0 0 1 3.5.75ZM2.25 2a.75.75 0 0 0 0 1.5h9.5a.75.75 0 0 0 0-1.5h-9.5Z"
|
||||
d="M3.5.75A.75.75 0 0 1 4.25 0h5.5a.75.75 0 0 1 0 1.5h-5.5A.75.75 0 0 1 3.5.75ZM2.25 2a.75.75 0 0 0 0 1.5h9.5a.75.75 0 0 0 0-1.5h-9.5ZM6 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM10 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM7 11.46a1.928 1.928 0 0 0-.586-1.386 2.035 2.035 0 0 0-2.828 0A1.928 1.928 0 0 0 3 11.461c0 .298.241.539.54.539h2.92a.54.54 0 0 0 .54-.54ZM8 11.46a2.928 2.928 0 0 0-.371-1.426A2.005 2.005 0 0 1 9 9.5a2.035 2.035 0 0 1 1.414.574A1.928 1.928 0 0 1 11 11.461a.54.54 0 0 1-.54.539H7.904c.063-.168.097-.35.097-.54Z"
|
||||
/>
|
||||
<path
|
||||
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||
19
apps/browser/src/autofill/content/components/icons/family.ts
Normal file
19
apps/browser/src/autofill/content/components/icons/family.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Family({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||
fill-rule="evenodd"
|
||||
d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.47 6.47 0 0 1-.932 3.356 3.732 3.732 0 0 0-1.106-.784 3.547 3.547 0 0 0-.516-.19 2 2 0 1 0-3.444-1.297c-.323-.216-.681-.4-1.069-.536a2.5 2.5 0 1 0-3.065-.155 5.405 5.405 0 0 0-1.59.674 3.912 3.912 0 0 0-.977.893A6.5 6.5 0 1 1 14.5 8ZM2.531 11.514a.75.75 0 0 0 .103-.13c.276-.436.552-.801.942-1.047a3.837 3.837 0 0 1 1.177-.492 5.243 5.243 0 0 1 .845-.095h.007l.022.001h.023c.436 0 .865.07 1.262.205.381.13.733.335 1.037.584.175.143.324.3.448.465l.164.226a4.13 4.13 0 0 0-1.035 1.565 4.407 4.407 0 0 0-.276 1.537c0 .043.004.085.01.125a6.5 6.5 0 0 1-4.729-2.944Zm10.033.964.07.08a6.481 6.481 0 0 1-3.894 1.9.757.757 0 0 0 .01-.125c0-.35.062-.694.181-1.013a2.63 2.63 0 0 1 .505-.842c.213-.237.462-.42.73-.543.267-.123.55-.185.834-.185.284 0 .567.062.835.185.267.123.516.306.729.543ZM7 6.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM11 9a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
@@ -3,12 +3,12 @@ export { AngleUp } from "./angle-up";
|
||||
export { BrandIconContainer } from "./brand-icon-container";
|
||||
export { Business } from "./business";
|
||||
export { Close } from "./close";
|
||||
export { Collection } from "./collection";
|
||||
export { CollectionShared } from "./collection-shared";
|
||||
export { ExclamationTriangle } from "./exclamation-triangle";
|
||||
export { ExternalLink } from "./external-link";
|
||||
export { Family } from "./family";
|
||||
export { Folder } from "./folder";
|
||||
export { Globe } from "./globe";
|
||||
export { PencilSquare } from "./pencil-square";
|
||||
export { Shield } from "./shield";
|
||||
export { User } from "./user";
|
||||
export { Users } from "./users";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { html } from "lit";
|
||||
|
||||
import { IconProps } from "../common-types";
|
||||
import { buildIconColorRule, ruleNames, themes } from "../constants/styles";
|
||||
|
||||
export function Users({ color, disabled, theme }: IconProps) {
|
||||
const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main;
|
||||
|
||||
return html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 14" fill="none">
|
||||
<path
|
||||
class=${css(buildIconColorRule(shapeColor, ruleNames.fill))}
|
||||
d="M6.5 2.5c0 .375-.082.73-.23 1.049A2.986 2.986 0 0 1 8 3c.644 0 1.241.203 1.73.549a2.5 2.5 0 1 1 3.925.825 4 4 0 0 1 1.173.846c.372.387.667.847.867 1.352.201.506.305 1.047.305 1.595 0 .46-.373.833-.833.833H11a4.987 4.987 0 0 1 1.62 2.087A5 5 0 0 1 13 13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1 5 5 0 0 1 2-4H.833A.833.833 0 0 1 0 8.167c0-.548.103-1.09.304-1.595.202-.505.496-.965.868-1.352.339-.353.736-.64 1.173-.846A2.5 2.5 0 1 1 6.5 2.5ZM4 3.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm1.401 4a2.986 2.986 0 0 1-.389-1.771A2.404 2.404 0 0 0 4 5.5c-.32 0-.638.065-.936.194-.3.13-.575.32-.81.565A2.682 2.682 0 0 0 1.579 7.5h3.822Zm5.198 0h3.822a2.682 2.682 0 0 0-.674-1.24 2.493 2.493 0 0 0-.81-.566 2.362 2.362 0 0 0-1.95.035 2.987 2.987 0 0 1-.39 1.771ZM12 3.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-4 4a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm3.464 5a3.5 3.5 0 0 0-6.928 0h6.928Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
@@ -43,26 +43,23 @@ const createIconStory = (iconName: keyof typeof Icons): StoryObj<Args> => {
|
||||
render: (args) => Template(args, Icons[iconName]),
|
||||
} as StoryObj<Args>;
|
||||
|
||||
if (iconName !== "BrandIconContainer") {
|
||||
story.argTypes = {
|
||||
iconLink: { table: { disable: true } },
|
||||
};
|
||||
}
|
||||
story.argTypes = {
|
||||
iconLink: { table: { disable: true } },
|
||||
};
|
||||
|
||||
return story;
|
||||
};
|
||||
|
||||
export const AngleDownIcon = createIconStory("AngleDown");
|
||||
export const AngleUpIcon = createIconStory("AngleUp");
|
||||
export const BrandIcon = createIconStory("BrandIconContainer");
|
||||
export const BusinessIcon = createIconStory("Business");
|
||||
export const CloseIcon = createIconStory("Close");
|
||||
export const CollectionIcon = createIconStory("Collection");
|
||||
export const CollectionSharedIcon = createIconStory("CollectionShared");
|
||||
export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle");
|
||||
export const ExternalLinkIcon = createIconStory("ExternalLink");
|
||||
export const FamilyIcon = createIconStory("Family");
|
||||
export const FolderIcon = createIconStory("Folder");
|
||||
export const GlobeIcon = createIconStory("Globe");
|
||||
export const PencilSquareIcon = createIconStory("PencilSquare");
|
||||
export const ShieldIcon = createIconStory("Shield");
|
||||
export const UserIcon = createIconStory("User");
|
||||
export const UsersIcon = createIconStory("Users");
|
||||
|
||||
@@ -4,14 +4,14 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { Theme } from "@bitwarden/common/platform/enums";
|
||||
|
||||
import { Option, OrgView, FolderView } from "../common-types";
|
||||
import { Business, Users, Folder, User } from "../icons";
|
||||
import { Business, Family, Folder, User } from "../icons";
|
||||
import { ButtonRow } from "../rows/button-row";
|
||||
|
||||
function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] {
|
||||
switch (productTierType) {
|
||||
case ProductTierType.Free:
|
||||
case ProductTierType.Families:
|
||||
return Users;
|
||||
return Family;
|
||||
case ProductTierType.Teams:
|
||||
case ProductTierType.Enterprise:
|
||||
case ProductTierType.TeamsStarter:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
@Component({
|
||||
selector: "app-remove-password",
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.3.2",
|
||||
"version": "2025.4.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.3.2",
|
||||
"version": "2025.4.0",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -18,7 +18,11 @@ export class BackgroundCommunicationBackend implements CommunicationBackend {
|
||||
return;
|
||||
}
|
||||
|
||||
void this.queue.enqueue({ ...message.message, source: { Web: { id: sender.tab.id } } });
|
||||
void this.queue.enqueue(
|
||||
new IncomingMessage(message.message.payload, message.message.destination, {
|
||||
Web: { id: sender.tab.id },
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
ExtensionAnonLayoutWrapperData,
|
||||
} from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
@@ -65,6 +64,7 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma
|
||||
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component";
|
||||
@@ -593,6 +593,7 @@ const routes: Routes = [
|
||||
path: "intro-carousel",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
canActivate: [],
|
||||
data: { elevation: 0, doNotSaveUrl: true } satisfies RouteDataProperties,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
|
||||
@@ -25,13 +25,13 @@ import {
|
||||
import { AccountComponent } from "../auth/popup/account-switching/account.component";
|
||||
import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component";
|
||||
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { PopOutComponent } from "../platform/popup/components/pop-out.component";
|
||||
import { HeaderComponent } from "../platform/popup/header.component";
|
||||
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
(click)="navigateToLogin()"
|
||||
class="tw-w-full tw-mt-4"
|
||||
class="tw-w-full tw-mt-2"
|
||||
>
|
||||
{{ "logIn" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/cli",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.3.0",
|
||||
"version": "2025.4.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -34,18 +34,22 @@
|
||||
"dist:oss:mac": "npm run build:oss:prod && npm run clean && npm run package:oss:mac",
|
||||
"dist:oss:mac-arm64": "npm run build:oss:prod && npm run clean && npm run package:oss:mac-arm64",
|
||||
"dist:oss:lin": "npm run build:oss:prod && npm run clean && npm run package:oss:lin",
|
||||
"dist:oss:lin-arm64": "npm run build:oss:prod && npm run clean && npm run package:oss:lin-arm64",
|
||||
"dist:bit:win": "npm run build:bit:prod && npm run clean && npm run package:bit:win",
|
||||
"dist:bit:mac": "npm run build:bit:prod && npm run clean && npm run package:bit:mac",
|
||||
"dist:bit:mac-arm64": "npm run build:bit:prod && npm run clean && npm run package:bit:mac-arm64",
|
||||
"dist:bit:lin": "npm run build:bit:prod && npm run clean && npm run package:bit:lin",
|
||||
"dist:bit:lin-arm64": "npm run build:bit:prod && npm run clean && npm run package:bit:lin-arm64",
|
||||
"package:oss:win": "pkg . --targets win-x64 --output ./dist/oss/windows/bw.exe",
|
||||
"package:oss:mac": "pkg . --targets macos-x64 --output ./dist/oss/macos/bw",
|
||||
"package:oss:mac-arm64": "pkg . --targets macos-arm64 --output ./dist/oss/macos-arm64/bw",
|
||||
"package:oss:lin": "pkg . --targets linux-x64 --output ./dist/oss/linux/bw",
|
||||
"package:oss:lin-arm64": "pkg . --targets linux-arm64 --output ./dist/oss/linux-arm64/bw",
|
||||
"package:bit:win": "pkg . --targets win-x64 --output ./dist/bit/windows/bw.exe",
|
||||
"package:bit:mac": "pkg . --targets macos-x64 --output ./dist/bit/macos/bw",
|
||||
"package:bit:mac-arm64": "pkg . --targets macos-arm64 --output ./dist/bit/macos-arm64/bw",
|
||||
"package:bit:lin": "pkg . --targets linux-x64 --output ./dist/bit/linux/bw",
|
||||
"package:bit:lin-arm64": "pkg . --targets linux-arm64 --output ./dist/bit/linux-arm64/bw",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:watch:all": "jest --watchAll"
|
||||
@@ -72,7 +76,7 @@
|
||||
"inquirer": "8.2.6",
|
||||
"jsdom": "26.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"koa": "2.15.4",
|
||||
"koa": "2.16.1",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
"koa-json": "2.0.2",
|
||||
"lowdb": "1.0.0",
|
||||
|
||||
@@ -17,7 +17,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ConvertToKeyConnectorCommand } from "../../commands/convert-to-key-connector.command";
|
||||
import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command";
|
||||
import { Response } from "../../models/response";
|
||||
import { MessageResponse } from "../../models/response/message.response";
|
||||
import { CliUtils } from "../../utils";
|
||||
|
||||
@@ -10,8 +10,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
ExportFormat,
|
||||
@@ -30,7 +28,6 @@ export class ExportCommand {
|
||||
private policyService: PolicyService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async run(options: OptionValues): Promise<Response> {
|
||||
@@ -55,13 +52,6 @@ export class ExportCommand {
|
||||
const format =
|
||||
password && options.format == "json" ? "encrypted_json" : (options.format ?? "csv");
|
||||
|
||||
if (
|
||||
format == "zip" &&
|
||||
!(await this.configService.getFeatureFlag(FeatureFlag.ExportAttachments))
|
||||
) {
|
||||
return Response.badRequest("Exporting attachments is not supported in this environment.");
|
||||
}
|
||||
|
||||
if (!this.isSupportedExportFormat(format)) {
|
||||
return Response.badRequest(
|
||||
`'${format}' is not a supported export format. Supported formats: ${EXPORT_FORMATS.join(
|
||||
|
||||
@@ -464,7 +464,7 @@ export class VaultProgram extends BaseProgram {
|
||||
|
||||
private exportCommand(): Command {
|
||||
return new Command("export")
|
||||
.description("Export vault data to a CSV or JSON file.")
|
||||
.description("Export vault data to a CSV, JSON or ZIP file.")
|
||||
.option("--output <output>", "Output directory or filename.")
|
||||
.option("--format <format>", "Export file format.")
|
||||
.option(
|
||||
@@ -476,7 +476,7 @@ export class VaultProgram extends BaseProgram {
|
||||
writeLn("\n Notes:");
|
||||
writeLn("");
|
||||
writeLn(
|
||||
" Valid formats are `csv`, `json`, and `encrypted_json`. Default format is `csv`.",
|
||||
" Valid formats are `csv`, `json`, `encrypted_json` and zip. Default format is `csv`.",
|
||||
);
|
||||
writeLn("");
|
||||
writeLn(
|
||||
@@ -504,7 +504,6 @@ export class VaultProgram extends BaseProgram {
|
||||
this.serviceContainer.policyService,
|
||||
this.serviceContainer.eventCollectionService,
|
||||
this.serviceContainer.accountService,
|
||||
this.serviceContainer.configService,
|
||||
);
|
||||
const response = await command.run(options);
|
||||
this.processResponse(response);
|
||||
|
||||
71
apps/desktop/desktop_native/Cargo.lock
generated
71
apps/desktop/desktop_native/Cargo.lock
generated
@@ -410,26 +410,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.71.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
@@ -573,15 +553,6 @@ dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -622,17 +593,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.31"
|
||||
@@ -987,6 +947,7 @@ dependencies = [
|
||||
"base64",
|
||||
"desktop_core",
|
||||
"hex",
|
||||
"log",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
@@ -1492,15 +1453,6 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.14"
|
||||
@@ -2302,16 +2254,6 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.2.0"
|
||||
@@ -2455,9 +2397,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.6"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
|
||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
@@ -2490,12 +2432,6 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3735,7 +3671,6 @@ checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
name = "windows_plugin_authenticator"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"hex",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
|
||||
@@ -38,7 +38,7 @@ oslog = "=0.2.0"
|
||||
pin-project = "=1.1.8"
|
||||
pkcs8 = "=0.10.2"
|
||||
rand = "=0.8.5"
|
||||
rsa = "=0.9.6"
|
||||
rsa = "=0.9.8"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
security-framework = "=3.1.0"
|
||||
|
||||
@@ -3,6 +3,10 @@ const child_process = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const process = require("process");
|
||||
const args = process.argv.slice(2); // Get arguments passed to the script
|
||||
const mode = args.includes("--release") ? "release" : "debug";
|
||||
const targetArg = args.find(arg => arg.startsWith("--target="));
|
||||
const target = targetArg ? targetArg.split("=")[1] : null;
|
||||
|
||||
let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platform";
|
||||
|
||||
@@ -18,10 +22,17 @@ function buildProxyBin(target, release = true) {
|
||||
return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")});
|
||||
}
|
||||
|
||||
if (!crossPlatform) {
|
||||
console.log("Building native modules in debug mode for the native architecture");
|
||||
buildNapiModule(false, false);
|
||||
buildProxyBin(false, false);
|
||||
if (!crossPlatform && !target) {
|
||||
console.log(`Building native modules in ${mode} mode for the native architecture`);
|
||||
buildNapiModule(false, mode === "release");
|
||||
buildProxyBin(false, mode === "release");
|
||||
return;
|
||||
}
|
||||
|
||||
if (target) {
|
||||
console.log(`Building for target: ${target} in ${mode} mode`);
|
||||
buildNapiModule(target, mode === "release");
|
||||
buildProxyBin(target, mode === "release");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +58,8 @@ switch (process.platform) {
|
||||
|
||||
default:
|
||||
targets = [
|
||||
['x86_64-unknown-linux-musl', 'x64']
|
||||
['x86_64-unknown-linux-musl', 'x64'],
|
||||
['aarch64-unknown-linux-musl', 'arm64']
|
||||
];
|
||||
|
||||
process.env["PKG_CONFIG_ALLOW_CROSS"] = "1";
|
||||
|
||||
@@ -18,6 +18,7 @@ base64 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
desktop_core = { path = "../core" }
|
||||
log = { workspace = true }
|
||||
napi = { workspace = true, features = ["async"] }
|
||||
napi-derive = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
10
apps/desktop/desktop_native/napi/index.d.ts
vendored
10
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -185,3 +185,13 @@ export declare namespace crypto {
|
||||
export declare namespace passkey_authenticator {
|
||||
export function register(): void
|
||||
}
|
||||
export declare namespace logging {
|
||||
export const enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4
|
||||
}
|
||||
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
|
||||
}
|
||||
|
||||
@@ -807,3 +807,61 @@ pub mod passkey_authenticator {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod logging {
|
||||
use log::{Level, Metadata, Record};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
};
|
||||
use std::sync::OnceLock;
|
||||
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
|
||||
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
|
||||
|
||||
#[napi]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl From<Level> for LogLevel {
|
||||
fn from(level: Level) -> Self {
|
||||
match level {
|
||||
Level::Trace => LogLevel::Trace,
|
||||
Level::Debug => LogLevel::Debug,
|
||||
Level::Info => LogLevel::Info,
|
||||
Level::Warn => LogLevel::Warn,
|
||||
Level::Error => LogLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
|
||||
let _ = JS_LOGGER.0.set(js_log_fn);
|
||||
let _ = log::set_logger(&JS_LOGGER);
|
||||
log::set_max_level(log::LevelFilter::Debug);
|
||||
}
|
||||
|
||||
impl log::Log for JsLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= log::max_level()
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
let Some(logger) = self.0.get() else {
|
||||
return;
|
||||
};
|
||||
let msg = (record.level().into(), record.args().to_string());
|
||||
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
bindgen = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { workspace = true, features = ["Win32_Foundation", "Win32_Security", "Win32_System_Com", "Win32_System_LibraryLoader" ] }
|
||||
windows-core = { workspace = true }
|
||||
|
||||
@@ -2,22 +2,6 @@
|
||||
|
||||
This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's.
|
||||
|
||||
This crate is very much a WIP and is not ready for internal use.
|
||||
|
||||
You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn).
|
||||
|
||||
## Building
|
||||
|
||||
To build this crate, set the following environment variables:
|
||||
|
||||
- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang))
|
||||
|
||||
### Bash Example
|
||||
|
||||
```
|
||||
export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
|
||||
### PowerShell Example
|
||||
|
||||
```
|
||||
$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows() {
|
||||
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("pluginauthenticator.hpp")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.allowlist_type("DWORD")
|
||||
.allowlist_type("PBYTE")
|
||||
.allowlist_type("EXPERIMENTAL.*")
|
||||
.allowlist_function(".*EXPERIMENTAL.*")
|
||||
.allowlist_function("WebAuthNGetApiVersionNumber")
|
||||
.generate()
|
||||
.expect("Unable to generate bindings.");
|
||||
|
||||
bindings
|
||||
.write_to_file(format!(
|
||||
"{}\\windows_plugin_authenticator_bindings.rs",
|
||||
out_dir
|
||||
))
|
||||
.expect("Couldn't write bindings.");
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
/*
|
||||
Bitwarden's pluginauthenticator.hpp
|
||||
|
||||
Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h
|
||||
|
||||
This is a C++ header file, so the extension has been manually
|
||||
changed from `.h` to `.hpp`, so bindgen will automatically
|
||||
generate the correct C++ bindings.
|
||||
|
||||
More Info: https://rust-lang.github.io/rust-bindgen/cpp.html
|
||||
*/
|
||||
|
||||
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
|
||||
|
||||
/* File created by MIDL compiler version 8.01.0628 */
|
||||
/* @@MIDL_FILE_HEADING( ) */
|
||||
|
||||
/* verify that the <rpcndr.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCNDR_H_VERSION__
|
||||
#define __REQUIRED_RPCNDR_H_VERSION__ 501
|
||||
#endif
|
||||
|
||||
/* verify that the <rpcsal.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCSAL_H_VERSION__
|
||||
#define __REQUIRED_RPCSAL_H_VERSION__ 100
|
||||
#endif
|
||||
|
||||
#include "rpc.h"
|
||||
#include "rpcndr.h"
|
||||
|
||||
#ifndef __RPCNDR_H_VERSION__
|
||||
#error this stub requires an updated version of <rpcndr.h>
|
||||
#endif /* __RPCNDR_H_VERSION__ */
|
||||
|
||||
#ifndef COM_NO_WINDOWS_H
|
||||
#include "windows.h"
|
||||
#include "ole2.h"
|
||||
#endif /*COM_NO_WINDOWS_H*/
|
||||
|
||||
#ifndef __pluginauthenticator_h__
|
||||
#define __pluginauthenticator_h__
|
||||
|
||||
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
|
||||
#pragma once
|
||||
#endif
|
||||
|
||||
#ifndef DECLSPEC_XFGVIRT
|
||||
#if defined(_CONTROL_FLOW_GUARD_XFG)
|
||||
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
|
||||
#else
|
||||
#define DECLSPEC_XFGVIRT(base, func)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Forward Declarations */
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */
|
||||
|
||||
/* header files for imported files */
|
||||
#include "oaidl.h"
|
||||
#include "webauthn.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"{
|
||||
#endif
|
||||
|
||||
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
|
||||
/* [local] */
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
{
|
||||
HWND hWnd;
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
DWORD cbEncodedRequest;
|
||||
/* [size_is] */ byte *pbEncodedRequest;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
|
||||
{
|
||||
DWORD cbEncodedResponse;
|
||||
/* [size_is] */ byte *pbEncodedResponse;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
|
||||
{
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
|
||||
/* interface EXPERIMENTAL_IPluginAuthenticator */
|
||||
/* [unique][version][uuid][object] */
|
||||
|
||||
EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#if defined(__cplusplus) && !defined(CINTERFACE)
|
||||
|
||||
MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")
|
||||
EXPERIMENTAL_IPluginAuthenticator : public IUnknown
|
||||
{
|
||||
public:
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
|
||||
|
||||
};
|
||||
|
||||
#else /* C style interface */
|
||||
|
||||
typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl
|
||||
{
|
||||
BEGIN_INTERFACE
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
|
||||
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in REFIID riid,
|
||||
/* [annotation][iid_is][out] */
|
||||
_COM_Outptr_ void **ppvObject);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, AddRef)
|
||||
ULONG ( STDMETHODCALLTYPE *AddRef )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, Release)
|
||||
ULONG ( STDMETHODCALLTYPE *Release )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
|
||||
|
||||
END_INTERFACE
|
||||
} EXPERIMENTAL_IPluginAuthenticatorVtbl;
|
||||
|
||||
interface EXPERIMENTAL_IPluginAuthenticator
|
||||
{
|
||||
CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
#ifdef COBJMACROS
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
|
||||
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \
|
||||
( (This)->lpVtbl -> AddRef(This) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \
|
||||
( (This)->lpVtbl -> Release(This) )
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) )
|
||||
|
||||
#endif /* COBJMACROS */
|
||||
|
||||
#endif /* C style interface */
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */
|
||||
|
||||
/* Additional Prototypes for ALL interfaces */
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
/* end of Additional Prototypes */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -2,15 +2,6 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
mod pa;
|
||||
|
||||
use pa::{
|
||||
DWORD, EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST,
|
||||
EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST,
|
||||
EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE,
|
||||
EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE,
|
||||
EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, PBYTE,
|
||||
};
|
||||
use std::ffi::c_uchar;
|
||||
use std::ptr;
|
||||
use windows::Win32::Foundation::*;
|
||||
@@ -23,11 +14,53 @@ const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator";
|
||||
const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
|
||||
const RPID: &str = "bitwarden.com";
|
||||
|
||||
/// Returns the current Windows WebAuthN version.
|
||||
pub fn get_version_number() -> u32 {
|
||||
unsafe { pa::WebAuthNGetApiVersionNumber() }
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST {
|
||||
pub transactionId: GUID,
|
||||
pub cbRequestSignature: Dword,
|
||||
pub pbRequestSignature: *mut byte,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST {
|
||||
pub hWnd: HWND,
|
||||
pub transactionId: GUID,
|
||||
pub cbRequestSignature: Dword,
|
||||
pub pbRequestSignature: *mut byte,
|
||||
pub cbEncodedRequest: Dword,
|
||||
pub pbEncodedRequest: *mut byte,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE {
|
||||
pub cbOpSignPubKey: Dword,
|
||||
pub pbOpSignPubKey: PByte,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE {
|
||||
pub cbEncodedResponse: Dword,
|
||||
pub pbEncodedResponse: *mut byte,
|
||||
}
|
||||
|
||||
type Dword = u32;
|
||||
type Byte = u8;
|
||||
type byte = u8;
|
||||
pub type PByte = *mut Byte;
|
||||
|
||||
type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST =
|
||||
*const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
pub type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST =
|
||||
*const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE =
|
||||
*mut EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE =
|
||||
*mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE;
|
||||
|
||||
/// Handles initialization and registration for the Bitwarden desktop app as a
|
||||
/// plugin authenticator with Windows.
|
||||
/// For now, also adds the authenticator
|
||||
@@ -123,9 +156,9 @@ fn add_authenticator() -> std::result::Result<(), String> {
|
||||
pbAuthenticatorInfo: authenticator_info_bytes.as_mut_ptr(),
|
||||
};
|
||||
|
||||
let plugin_signing_public_key_byte_count: DWORD = 0;
|
||||
let plugin_signing_public_key_byte_count: Dword = 0;
|
||||
let mut plugin_signing_public_key: c_uchar = 0;
|
||||
let plugin_signing_public_key_ptr: PBYTE = &mut plugin_signing_public_key;
|
||||
let plugin_signing_public_key_ptr: PByte = &mut plugin_signing_public_key;
|
||||
|
||||
let mut add_response = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE {
|
||||
cbOpSignPubKey: plugin_signing_public_key_byte_count,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/*
|
||||
The 'pa' (plugin authenticator) module will contain the generated
|
||||
bindgen code.
|
||||
|
||||
The attributes below will suppress warnings from the generated code.
|
||||
*/
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
#![allow(clippy::all)]
|
||||
#![allow(warnings)]
|
||||
|
||||
include!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/windows_plugin_authenticator_bindings.rs"
|
||||
));
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.4.2",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -36,6 +36,7 @@
|
||||
"pack:dir": "npm run clean:dist && electron-builder --dir -p never",
|
||||
"pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snapcraft pack ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/",
|
||||
"pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .",
|
||||
"pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never",
|
||||
"pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never",
|
||||
"pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never",
|
||||
@@ -45,6 +46,7 @@
|
||||
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never",
|
||||
"dist:dir": "npm run build && npm run pack:dir",
|
||||
"dist:lin": "npm run build && npm run pack:lin",
|
||||
"dist:lin:arm64": "npm run build && npm run pack:lin:arm64",
|
||||
"dist:mac": "npm run build && npm run pack:mac",
|
||||
"dist:mac:mas": "npm run build && npm run pack:mac:mas",
|
||||
"dist:mac:masdev": "npm run build && npm run pack:mac:masdev",
|
||||
|
||||
@@ -50,9 +50,9 @@ import {
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
|
||||
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||
|
||||
@@ -13,11 +13,11 @@ import { DecryptionFailureDialogComponent } from "@bitwarden/vault";
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/app/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/app/vault/attachments.component";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
@Component({
|
||||
selector: "app-remove-password",
|
||||
@@ -292,11 +292,7 @@ export class WindowMain {
|
||||
this.win.maximize();
|
||||
}
|
||||
|
||||
// Show it later since it might need to be maximized.
|
||||
// use once event to avoid flash on unstyled content.
|
||||
this.win.once("ready-to-show", () => {
|
||||
this.win.show();
|
||||
});
|
||||
this.win.show();
|
||||
|
||||
if (template === "full-app") {
|
||||
// and load the index.html of the app.
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.4.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.4.2",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.4.2",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -7,6 +7,7 @@ import log from "electron-log/main";
|
||||
|
||||
import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.enum";
|
||||
import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { logging } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { isDev } from "../../utils";
|
||||
|
||||
@@ -30,6 +31,29 @@ export class ElectronLogMainService extends BaseLogService {
|
||||
ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => {
|
||||
this.write(level, message, ...optionalParams);
|
||||
});
|
||||
|
||||
logging.initNapiLog((error, level, message) => this.writeNapiLog(level, message));
|
||||
}
|
||||
|
||||
private writeNapiLog(level: logging.LogLevel, message: string) {
|
||||
let levelType: LogLevelType;
|
||||
|
||||
switch (level) {
|
||||
case logging.LogLevel.Debug:
|
||||
levelType = LogLevelType.Debug;
|
||||
break;
|
||||
case logging.LogLevel.Warn:
|
||||
levelType = LogLevelType.Warning;
|
||||
break;
|
||||
case logging.LogLevel.Error:
|
||||
levelType = LogLevelType.Error;
|
||||
break;
|
||||
default:
|
||||
levelType = LogLevelType.Info;
|
||||
break;
|
||||
}
|
||||
|
||||
this.write(levelType, "[NAPI] " + message);
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {
|
||||
|
||||
@@ -5,6 +5,14 @@ jest.mock("electron", () => ({
|
||||
ipcMain: { handle: jest.fn(), on: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock("@bitwarden/desktop-napi", () => {
|
||||
return {
|
||||
logging: {
|
||||
initNapiLog: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("ElectronLogMainService", () => {
|
||||
it("sets dev based on electron method", () => {
|
||||
process.env.ELECTRON_IS_DEV = "1";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2025.4.0",
|
||||
"version": "2025.4.1",
|
||||
"scripts": {
|
||||
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => {
|
||||
|
||||
it("permits navigation if the user has permissions", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockImplementation((_org) => true);
|
||||
permissionsCallback.mockReturnValue(true);
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId }));
|
||||
expect(permissionsCallback).toHaveBeenCalledTimes(1);
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("handles a Promise returned from the callback", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockReturnValue(Promise.resolve(true));
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(() =>
|
||||
organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalledTimes(1);
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("handles an Observable returned from the callback", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockReturnValue(of(true));
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(() =>
|
||||
organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalledTimes(1);
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
describe("if the user does not have permissions", () => {
|
||||
it("and there is no Item ID, block navigation", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockImplementation((_org) => false);
|
||||
permissionsCallback.mockReturnValue(false);
|
||||
|
||||
state = mock<RouterStateSnapshot>({
|
||||
root: mock<ActivatedRouteSnapshot>({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { inject } from "@angular/core";
|
||||
import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core";
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivateFn,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from "@angular/router";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
canAccessOrgAdmin,
|
||||
@@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components";
|
||||
* proceeds as expected.
|
||||
*/
|
||||
export function organizationPermissionsGuard(
|
||||
permissionsCallback?: (organization: Organization) => boolean,
|
||||
permissionsCallback?: (
|
||||
organization: Organization,
|
||||
) => boolean | Promise<boolean> | Observable<boolean>,
|
||||
): CanActivateFn {
|
||||
return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
const router = inject(Router);
|
||||
@@ -51,6 +53,7 @@ export function organizationPermissionsGuard(
|
||||
const i18nService = inject(I18nService);
|
||||
const syncService = inject(SyncService);
|
||||
const accountService = inject(AccountService);
|
||||
const environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
// TODO: We need to fix issue once and for all.
|
||||
if ((await syncService.getLastSync()) == null) {
|
||||
@@ -78,7 +81,22 @@ export function organizationPermissionsGuard(
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
const hasPermissions = permissionsCallback == null || permissionsCallback(org);
|
||||
if (permissionsCallback == null) {
|
||||
// No additional permission checks required, allow navigation
|
||||
return true;
|
||||
}
|
||||
|
||||
const callbackResult = runInInjectionContext(environmentInjector, () =>
|
||||
permissionsCallback(org),
|
||||
);
|
||||
|
||||
const hasPermissions = isObservable(callbackResult)
|
||||
? await firstValueFrom(callbackResult) // handles observables
|
||||
: await Promise.resolve(callbackResult); // handles promises and boolean values
|
||||
|
||||
if (hasPermissions !== true && hasPermissions !== false) {
|
||||
throw new Error("Permission callback did not resolve to a boolean.");
|
||||
}
|
||||
|
||||
if (!hasPermissions) {
|
||||
// Handle linkable ciphers for organizations the user only has view access to
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<bit-nav-item
|
||||
[text]="'policies' | i18n"
|
||||
route="settings/policies"
|
||||
*ngIf="organization.canManagePolicies"
|
||||
*ngIf="canShowPoliciesTab$ | async"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'twoStepLogin' | i18n"
|
||||
|
||||
@@ -22,6 +22,7 @@ import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -68,6 +69,7 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
showAccountDeprovisioningBanner$: Observable<boolean>;
|
||||
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
|
||||
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
|
||||
protected canShowPoliciesTab$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -79,6 +81,7 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
protected bannerService: AccountDeprovisioningBannerService,
|
||||
private accountService: AccountService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -148,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
))
|
||||
? "claimedDomains"
|
||||
: "domainVerification";
|
||||
|
||||
this.canShowPoliciesTab$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService
|
||||
.isBreadcrumbingPoliciesEnabled$(organization)
|
||||
.pipe(
|
||||
map(
|
||||
(isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
canShowVaultTab(organization: Organization): boolean {
|
||||
|
||||
@@ -3,9 +3,10 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { SponsoredFamiliesComponent } from "../../../billing/settings/sponsored-families.component";
|
||||
import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component";
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard";
|
||||
import { MembersComponent } from "./members.component";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -19,8 +20,8 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "sponsored-families",
|
||||
component: SponsoredFamiliesComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessMembersTab)],
|
||||
component: FreeBitwardenFamiliesComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessMembersTab), canAccessSponsoredFamilies],
|
||||
data: {
|
||||
titleId: "sponsoredFamilies",
|
||||
},
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
<app-header></app-header>
|
||||
<app-header>
|
||||
@let organization = organization$ | async;
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="changePlan(organization)"
|
||||
*ngIf="isBreadcrumbingEnabled$ | async"
|
||||
slot="title-suffix"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<bit-container>
|
||||
<ng-container *ngIf="loading">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, lastValueFrom } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
ChangePlanDialogResultType,
|
||||
openChangePlanDialog,
|
||||
} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
|
||||
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
import { PolicyListService } from "../../core/policy-list.service";
|
||||
import { BasePolicy } from "../policies";
|
||||
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
|
||||
|
||||
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
|
||||
|
||||
@@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit {
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: BasePolicy[];
|
||||
organization: Organization;
|
||||
protected organization$: Observable<Organization>;
|
||||
|
||||
private orgPolicies: PolicyResponse[];
|
||||
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private organizationService: OrganizationService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyListService: PolicyListService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
@@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
this.organization$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId));
|
||||
this.policies = this.policyListService.getPolicies();
|
||||
|
||||
await this.load();
|
||||
@@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit {
|
||||
this.orgPolicies.forEach((op) => {
|
||||
this.policiesEnabledMap.set(op.type, op.enabled);
|
||||
});
|
||||
|
||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
||||
),
|
||||
);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit {
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === PolicyEditDialogResult.Saved) {
|
||||
await this.load();
|
||||
switch (result) {
|
||||
case PolicyEditDialogResult.Saved:
|
||||
await this.load();
|
||||
break;
|
||||
case PolicyEditDialogResult.UpgradePlan:
|
||||
await this.changePlan(await firstValueFrom(this.organization$));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly CollectionDialogTabType = CollectionDialogTabType;
|
||||
protected readonly All = All;
|
||||
|
||||
protected async changePlan(organization: Organization) {
|
||||
const reference = openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
subscription: null,
|
||||
productTierType: organization.productTierType,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(reference.closed);
|
||||
|
||||
if (result === ChangePlanDialogResultType.Closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
|
||||
<ng-container bitDialogTitle>
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="upgradePlan()"
|
||||
*ngIf="isBreadcrumbingEnabled$ | async"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "planNameEnterprise" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
@@ -16,6 +28,7 @@
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="!(isBreadcrumbingEnabled$ | async); else breadcrumbing"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="saveDisabled$ | async"
|
||||
@@ -24,6 +37,11 @@
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<ng-template #breadcrumbing>
|
||||
<button bitButton buttonType="primary" bitFormButton type="button" (click)="upgradePlan()">
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -9,12 +9,20 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { Observable, map } from "rxjs";
|
||||
import { map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
@@ -35,6 +43,7 @@ export type PolicyEditDialogData = {
|
||||
|
||||
export enum PolicyEditDialogResult {
|
||||
Saved = "saved",
|
||||
UpgradePlan = "upgrade-plan",
|
||||
}
|
||||
@Component({
|
||||
selector: "app-policy-edit",
|
||||
@@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
loading = true;
|
||||
enabled = false;
|
||||
saveDisabled$: Observable<boolean>;
|
||||
defaultTypes: any[];
|
||||
policyComponent: BasePolicyComponent;
|
||||
|
||||
private policyResponse: PolicyResponse;
|
||||
formGroup = this.formBuilder.group({
|
||||
enabled: [this.enabled],
|
||||
});
|
||||
protected organization$: Observable<Organization>;
|
||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
|
||||
private accountService: AccountService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private organizationService: OrganizationService,
|
||||
private i18nService: I18nService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<PolicyEditDialogResult>,
|
||||
private toastService: ToastService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
) {}
|
||||
|
||||
get policy(): BasePolicy {
|
||||
return this.data.policy;
|
||||
}
|
||||
@@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.organization$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.organizationService.organizations$(userId)),
|
||||
getOrganizationById(this.data.organizationId),
|
||||
);
|
||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
@@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
|
||||
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
|
||||
};
|
||||
|
||||
protected upgradePlan(): void {
|
||||
this.dialogRef.close(PolicyEditDialogResult.UpgradePlan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { NgModule, inject } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
|
||||
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
|
||||
@@ -41,7 +43,14 @@ const routes: Routes = [
|
||||
{
|
||||
path: "policies",
|
||||
component: PoliciesComponent,
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)],
|
||||
canActivate: [
|
||||
organizationPermissionsGuard((o: Organization) => {
|
||||
const organizationBillingService = inject(OrganizationBillingServiceAbstraction);
|
||||
return organizationBillingService
|
||||
.isBreadcrumbingPoliciesEnabled$(o)
|
||||
.pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled));
|
||||
}),
|
||||
],
|
||||
data: {
|
||||
titleId: "policies",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="tw-mt-10 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n">
|
||||
</bit-icon>
|
||||
<div class="tw-flex tw-justify-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -43,6 +43,8 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
|
||||
value.plan = PlanType.FamiliesAnnually;
|
||||
value.productTier = ProductTierType.Families;
|
||||
value.acceptingSponsorship = true;
|
||||
value.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise;
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this));
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
|
||||
import { I18nService } from "../../core/i18n.service";
|
||||
|
||||
import {
|
||||
@@ -200,11 +199,6 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
encryptedString: "encryptedString",
|
||||
} as EncString);
|
||||
|
||||
jest.mock("../../admin-console/organizations/manage/organization-trust.component");
|
||||
OrganizationTrustComponent.open = jest.fn().mockReturnValue({
|
||||
closed: new BehaviorSubject(true),
|
||||
});
|
||||
|
||||
await globalState.update(() => invite);
|
||||
|
||||
policyService.getResetPasswordPolicyOptions.mockReturnValue([
|
||||
@@ -217,7 +211,6 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(OrganizationTrustComponent.open).toHaveBeenCalled();
|
||||
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
|
||||
{ key: "userKey" },
|
||||
Utils.fromB64ToArray("publicKey"),
|
||||
|
||||
@@ -31,8 +31,6 @@ import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
|
||||
|
||||
import { OrganizationInvite } from "./organization-invite";
|
||||
|
||||
// We're storing the organization invite for 2 reasons:
|
||||
@@ -189,15 +187,6 @@ export class AcceptOrganizationInviteService {
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
const dialogRef = OrganizationTrustComponent.open(this.dialogService, {
|
||||
name: invite.organizationName,
|
||||
orgId: invite.organizationId,
|
||||
publicKey,
|
||||
});
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
if (result !== true) {
|
||||
throw new Error("Organization not trusted, aborting user key rotation");
|
||||
}
|
||||
|
||||
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router";
|
||||
import { firstValueFrom, switchMap, filter } from "rxjs";
|
||||
|
||||
import { OrganizationService } 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 { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
|
||||
|
||||
export const canAccessSponsoredFamilies: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
|
||||
const freeFamiliesPolicyService = inject(FreeFamiliesPolicyService);
|
||||
const organizationService = inject(OrganizationService);
|
||||
const accountService = inject(AccountService);
|
||||
|
||||
const org = accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => organizationService.organizations$(userId)),
|
||||
getById(route.params.organizationId),
|
||||
filter((org): org is Organization => org !== undefined),
|
||||
);
|
||||
|
||||
return await firstValueFrom(freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(org));
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<form>
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ "addSponsorship" | i18n }}</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
<form [formGroup]="sponsorshipForm">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}:</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="email"
|
||||
formControlName="sponsorshipEmail"
|
||||
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "notes" | i18n }}:</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="text"
|
||||
formControlName="sponsorshipNote"
|
||||
[attr.aria-invalid]="sponsorshipNoteControl.invalid"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="button" buttonType="primary" (click)="save()">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" [bitDialogClose]="false">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,135 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogModule, DialogService, FormFieldModule } from "@bitwarden/components";
|
||||
|
||||
interface RequestSponsorshipForm {
|
||||
sponsorshipEmail: FormControl<string | null>;
|
||||
sponsorshipNote: FormControl<string | null>;
|
||||
}
|
||||
|
||||
export interface AddSponsorshipDialogResult {
|
||||
action: AddSponsorshipDialogAction;
|
||||
value: Partial<AddSponsorshipFormValue> | null;
|
||||
}
|
||||
|
||||
interface AddSponsorshipFormValue {
|
||||
sponsorshipEmail: string;
|
||||
sponsorshipNote: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
enum AddSponsorshipDialogAction {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "add-sponsorship-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
],
|
||||
})
|
||||
export class AddSponsorshipDialogComponent {
|
||||
sponsorshipForm: FormGroup<RequestSponsorshipForm>;
|
||||
loading = false;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef<AddSponsorshipDialogResult>,
|
||||
private formBuilder: FormBuilder,
|
||||
private accountService: AccountService,
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
|
||||
sponsorshipEmail: new FormControl<string | null>("", {
|
||||
validators: [Validators.email, Validators.required],
|
||||
asyncValidators: [this.validateNotCurrentUserEmail.bind(this)],
|
||||
updateOn: "change",
|
||||
}),
|
||||
sponsorshipNote: new FormControl<string | null>("", {}),
|
||||
});
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService): DialogRef<AddSponsorshipDialogResult> {
|
||||
return dialogService.open<AddSponsorshipDialogResult>(AddSponsorshipDialogComponent);
|
||||
}
|
||||
|
||||
protected async save() {
|
||||
if (this.sponsorshipForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
// TODO: This is a mockup implementation - needs to be updated with actual API integration
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
const formValue = this.sponsorshipForm.getRawValue();
|
||||
const dialogValue: Partial<AddSponsorshipFormValue> = {
|
||||
status: "Sent",
|
||||
sponsorshipEmail: formValue.sponsorshipEmail ?? "",
|
||||
sponsorshipNote: formValue.sponsorshipNote ?? "",
|
||||
};
|
||||
|
||||
this.dialogRef.close({
|
||||
action: AddSponsorshipDialogAction.Saved,
|
||||
value: dialogValue,
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
protected close = () => {
|
||||
this.dialogRef.close({ action: AddSponsorshipDialogAction.Canceled, value: null });
|
||||
};
|
||||
|
||||
get sponsorshipEmailControl() {
|
||||
return this.sponsorshipForm.controls.sponsorshipEmail;
|
||||
}
|
||||
|
||||
get sponsorshipNoteControl() {
|
||||
return this.sponsorshipForm.controls.sponsorshipNote;
|
||||
}
|
||||
|
||||
private async validateNotCurrentUserEmail(
|
||||
control: AbstractControl,
|
||||
): Promise<ValidationErrors | null> {
|
||||
const value = control.value;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentUserEmail = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email ?? "")),
|
||||
);
|
||||
|
||||
if (!currentUserEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.toLowerCase() === currentUserEmail.toLowerCase()) {
|
||||
return { currentUserEmail: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<app-header>
|
||||
<button type="button" (click)="addSponsorship()" bitButton buttonType="primary">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "addSponsorship" | i18n }}
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex">
|
||||
<bit-tab [label]="'sponsoredBitwardenFamilies' | i18n">
|
||||
<app-organization-sponsored-families
|
||||
[sponsoredFamilies]="sponsoredFamilies"
|
||||
(removeSponsorshipEvent)="removeSponsorhip($event)"
|
||||
></app-organization-sponsored-families>
|
||||
</bit-tab>
|
||||
|
||||
<bit-tab [label]="'memberFamilies' | i18n">
|
||||
<app-organization-member-families
|
||||
[memberFamilies]="sponsoredFamilies"
|
||||
></app-organization-member-families>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
|
||||
<p class="tw-px-4" bitTypography="body2">{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}</p>
|
||||
@@ -0,0 +1,62 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
|
||||
|
||||
import {
|
||||
AddSponsorshipDialogComponent,
|
||||
AddSponsorshipDialogResult,
|
||||
} from "./add-sponsorship-dialog.component";
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-free-bitwarden-families",
|
||||
templateUrl: "free-bitwarden-families.component.html",
|
||||
})
|
||||
export class FreeBitwardenFamiliesComponent implements OnInit {
|
||||
tabIndex = 0;
|
||||
sponsoredFamilies: SponsoredFamily[] = [];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private dialogService: DialogService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.preventAccessToFreeFamiliesPage();
|
||||
}
|
||||
|
||||
async addSponsorship() {
|
||||
const addSponsorshipDialogRef: DialogRef<AddSponsorshipDialogResult> =
|
||||
AddSponsorshipDialogComponent.open(this.dialogService);
|
||||
|
||||
const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed);
|
||||
|
||||
if (dialogRef?.value) {
|
||||
this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies];
|
||||
}
|
||||
}
|
||||
|
||||
removeSponsorhip(sponsorship: any) {
|
||||
const index = this.sponsoredFamilies.findIndex(
|
||||
(e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail,
|
||||
);
|
||||
this.sponsoredFamilies.splice(index, 1);
|
||||
}
|
||||
|
||||
private async preventAccessToFreeFamiliesPage() {
|
||||
const showFreeFamiliesPage = await firstValueFrom(
|
||||
this.freeFamiliesPolicyService.showFreeFamilies$,
|
||||
);
|
||||
|
||||
if (!showFreeFamiliesPage) {
|
||||
await this.router.navigate(["/"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "membersWithSponsoredFamilies" | i18n }}
|
||||
</p>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "memberFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && memberFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "member" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of memberFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noMemberFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noMemberFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-member-families",
|
||||
templateUrl: "organization-member-families.component.html",
|
||||
})
|
||||
export class OrganizationMemberFamiliesComponent implements OnInit, OnDestroy {
|
||||
tabIndex = 0;
|
||||
loading = false;
|
||||
|
||||
@Input() memberFamilies: SponsoredFamily[] = [];
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "sponsorFreeBitwardenFamilies" | i18n }}
|
||||
</p>
|
||||
<div bitTypography="body1">
|
||||
{{ "sponsoredFamiliesInclude" | i18n }}:
|
||||
<ul class="tw-list-outside">
|
||||
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
|
||||
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "sponsoredBitwardenFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && sponsoredFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "recipient" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "notes" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of sponsoredFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
<td bitCell>{{ o.sponsorshipNote }}</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
|
||||
<hr class="m-0" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'revokeAccount' | i18n"
|
||||
(click)="remove(o)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-close tw-text-danger"></i>
|
||||
<span class="tw-text-danger pl-1">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noSponsoredFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noSponsoredFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-sponsored-families",
|
||||
templateUrl: "organization-sponsored-families.component.html",
|
||||
})
|
||||
export class OrganizationSponsoredFamiliesComponent implements OnInit, OnDestroy {
|
||||
loading = false;
|
||||
tabIndex = 0;
|
||||
|
||||
@Input() sponsoredFamilies: SponsoredFamily[] = [];
|
||||
@Output() removeSponsorshipEvent = new EventEmitter<SponsoredFamily>();
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
remove(sponsorship: SponsoredFamily) {
|
||||
this.removeSponsorshipEvent.emit(sponsorship);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SponsoredFamily {
|
||||
sponsorshipEmail?: string;
|
||||
sponsorshipNote?: string;
|
||||
status?: string;
|
||||
}
|
||||
@@ -34,7 +34,12 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import {
|
||||
PaymentMethodType,
|
||||
PlanSponsorshipType,
|
||||
PlanType,
|
||||
ProductTierType,
|
||||
} from "@bitwarden/common/billing/enums";
|
||||
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
|
||||
@@ -83,6 +88,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
@Input() showFree = true;
|
||||
@Input() showCancel = false;
|
||||
@Input() acceptingSponsorship = false;
|
||||
@Input() planSponsorshipType?: PlanSponsorshipType;
|
||||
@Input() currentPlan: PlanResponse;
|
||||
|
||||
selectedFile: File;
|
||||
@@ -682,11 +688,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private refreshSalesTax(): void {
|
||||
if (this.formGroup.controls.plan.value == PlanType.Free) {
|
||||
this.estimatedTax = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.taxComponent.validate()) {
|
||||
return;
|
||||
}
|
||||
@@ -696,6 +697,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
passwordManager: {
|
||||
additionalStorage: this.formGroup.controls.additionalStorage.value,
|
||||
plan: this.formGroup.controls.plan.value,
|
||||
sponsoredPlan: this.planSponsorshipType,
|
||||
seats: this.formGroup.controls.additionalSeats.value,
|
||||
},
|
||||
taxInformation: {
|
||||
|
||||
@@ -76,7 +76,7 @@ export class AddCreditDialogComponent implements OnInit {
|
||||
async ngOnInit() {
|
||||
if (this.organizationId != null) {
|
||||
if (this.creditAmount == null) {
|
||||
this.creditAmount = "20.00";
|
||||
this.creditAmount = "0.00";
|
||||
}
|
||||
this.ppButtonCustomField = "organization_id:" + this.organizationId;
|
||||
const userId = await firstValueFrom(
|
||||
@@ -93,7 +93,7 @@ export class AddCreditDialogComponent implements OnInit {
|
||||
}
|
||||
} else {
|
||||
if (this.creditAmount == null) {
|
||||
this.creditAmount = "10.00";
|
||||
this.creditAmount = "0.00";
|
||||
}
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
@Component({
|
||||
selector: "app-remove-password",
|
||||
@@ -12,7 +12,7 @@
|
||||
<h1
|
||||
bitTypography="h1"
|
||||
noMargin
|
||||
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex"
|
||||
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
|
||||
[title]="title || (routeData.titleId | i18n)"
|
||||
>
|
||||
<div class="tw-truncate">
|
||||
|
||||
@@ -58,7 +58,6 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login
|
||||
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
|
||||
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
|
||||
import { RemovePasswordComponent } from "./auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "./auth/set-password.component";
|
||||
import { AccountComponent } from "./auth/settings/account/account.component";
|
||||
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
|
||||
@@ -73,6 +72,7 @@ import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/com
|
||||
import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver";
|
||||
import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component";
|
||||
import { RouteDataProperties } from "./core";
|
||||
import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component";
|
||||
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "./layouts/user-layout.component";
|
||||
import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component";
|
||||
|
||||
@@ -19,7 +19,13 @@ export class WebCommunicationProvider implements CommunicationBackend {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.queue.enqueue({ ...message.message, source: "BrowserBackground" });
|
||||
void this.queue.enqueue(
|
||||
new IncomingMessage(
|
||||
message.message.payload,
|
||||
message.message.destination,
|
||||
"BrowserBackground",
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/
|
||||
import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
|
||||
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { AccountComponent } from "../auth/settings/account/account.component";
|
||||
import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component";
|
||||
@@ -42,6 +41,7 @@ import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-famili
|
||||
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
|
||||
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
|
||||
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
|
||||
@@ -62,6 +62,9 @@ import { OrganizationBadgeModule } from "../vault/individual-vault/organization-
|
||||
import { PipesModule } from "../vault/individual-vault/pipes/pipes.module";
|
||||
import { PurgeVaultComponent } from "../vault/settings/purge-vault.component";
|
||||
|
||||
import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component";
|
||||
import { OrganizationMemberFamiliesComponent } from "./../billing/members/organization-member-families.component";
|
||||
import { OrganizationSponsoredFamiliesComponent } from "./../billing/members/organization-sponsored-families.component";
|
||||
import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module";
|
||||
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
|
||||
import { SharedModule } from "./shared.module";
|
||||
@@ -128,6 +131,9 @@ import { SharedModule } from "./shared.module";
|
||||
SelectableAvatarComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
OrganizationSponsoredFamiliesComponent,
|
||||
OrganizationMemberFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
UpdatePasswordComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
@@ -175,6 +181,9 @@ import { SharedModule } from "./shared.module";
|
||||
SelectableAvatarComponent,
|
||||
SetPasswordComponent,
|
||||
SponsoredFamiliesComponent,
|
||||
OrganizationSponsoredFamiliesComponent,
|
||||
OrganizationMemberFamiliesComponent,
|
||||
FreeBitwardenFamiliesComponent,
|
||||
SponsoringOrgRowComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UpdatePasswordComponent,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<bit-label>{{ "username" | i18n }}</bit-label>
|
||||
<input id="username" type="text" formControlName="username" bitInput />
|
||||
</bit-form-field>
|
||||
<small class="form-text text-muted tw-mb-4">{{ "breachCheckUsernameEmail" | i18n }}</small>
|
||||
<small class="tw-mb-4 tw-block tw-text-muted">{{ "breachCheckUsernameEmail" | i18n }}</small>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="loading">
|
||||
{{ "checkBreaches" | i18n }}
|
||||
</button>
|
||||
@@ -21,32 +21,33 @@
|
||||
<bit-callout type="danger" title="{{ 'breachFound' | i18n }}" *ngIf="breachedAccounts.length">
|
||||
{{ "breachUsernameFound" | i18n: checkedUsername : breachedAccounts.length }}
|
||||
</bit-callout>
|
||||
<ul class="list-group list-group-breach" *ngIf="breachedAccounts.length">
|
||||
<li *ngFor="let a of breachedAccounts" class="list-group-item min-height-fix">
|
||||
<div class="row">
|
||||
<div class="col-2 tw-text-center">
|
||||
<img [src]="a.logoPath" alt="" class="img-fluid" />
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<h3 class="tw-text-lg">{{ a.title }}</h3>
|
||||
<p [innerHTML]="a.description"></p>
|
||||
<p class="tw-mb-1">{{ "compromisedData" | i18n }}:</p>
|
||||
<ul>
|
||||
<li *ngFor="let d of a.dataClasses">{{ d }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<dl>
|
||||
<dt>{{ "website" | i18n }}</dt>
|
||||
<dd>{{ a.domain }}</dd>
|
||||
<dt>{{ "affectedUsers" | i18n }}</dt>
|
||||
<dd>{{ a.pwnCount | number }}</dd>
|
||||
<dt>{{ "breachOccurred" | i18n }}</dt>
|
||||
<dd>{{ a.breachDate | date: "mediumDate" }}</dd>
|
||||
<dt>{{ "breachReported" | i18n }}</dt>
|
||||
<dd>{{ a.addedDate | date: "mediumDate" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<ul
|
||||
class="tw-list-none tw-flex-col tw-divide-x-0 tw-divide-y tw-divide-solid tw-divide-secondary-300 tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-p-0"
|
||||
*ngIf="breachedAccounts.length"
|
||||
>
|
||||
<li *ngFor="let a of breachedAccounts" class="tw-flex tw-gap-4 tw-p-4">
|
||||
<div class="tw-w-32 tw-flex-none">
|
||||
<img [src]="a.logoPath" alt="" class="tw-max-w-32 tw-items-stretch" />
|
||||
</div>
|
||||
<div class="tw-flex-auto">
|
||||
<h3 class="tw-text-lg">{{ a.title }}</h3>
|
||||
<p [innerHTML]="a.description"></p>
|
||||
<p class="tw-mb-1">{{ "compromisedData" | i18n }}:</p>
|
||||
<ul>
|
||||
<li *ngFor="let d of a.dataClasses">{{ d }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tw-w-48 tw-flex-none">
|
||||
<dl>
|
||||
<dt>{{ "website" | i18n }}</dt>
|
||||
<dd>{{ a.domain }}</dd>
|
||||
<dt>{{ "affectedUsers" | i18n }}</dt>
|
||||
<dd>{{ a.pwnCount | number }}</dd>
|
||||
<dt>{{ "breachOccurred" | i18n }}</dt>
|
||||
<dd>{{ a.breachDate | date: "mediumDate" }}</dd>
|
||||
<dt>{{ "breachReported" | i18n }}</dt>
|
||||
<dd>{{ a.addedDate | date: "mediumDate" }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
12
apps/web/src/images/search.svg
Normal file
12
apps/web/src/images/search.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="97" height="97" viewBox="0 0 97 97" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M62.5 12.4604C63.6046 12.4604 64.5 13.3559 64.5 14.4604L64.5 90.4604C64.5 91.565 63.6046 92.4604 62.5 92.4604L6.5 92.4604C5.39543 92.4604 4.5 91.565 4.5 90.4604L4.5 14.4604C4.5 13.3559 5.39544 12.4604 6.5 12.4604L62.5 12.4604Z" fill="#99BAF4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.5 90.4604L62.5 14.4604L6.5 14.4604L6.5 90.4604L62.5 90.4604ZM64.5 14.4604C64.5 13.3559 63.6046 12.4604 62.5 12.4604L6.5 12.4604C5.39544 12.4604 4.5 13.3559 4.5 14.4604L4.5 90.4604C4.5 91.565 5.39543 92.4604 6.5 92.4604L62.5 92.4604C63.6046 92.4604 64.5 91.565 64.5 90.4604L64.5 14.4604Z" fill="#0E3781"/>
|
||||
<path d="M72.5 82.4604L72.5 6.46045C72.5 5.35588 71.6046 4.46045 70.5 4.46045L27.8284 4.46045C27.298 4.46045 26.7939 4.66655 26.4188 5.04162L13.0835 18.3769C12.7085 18.752 12.5 19.2584 12.5 19.7889L12.5 82.4604C12.5 83.565 13.3954 84.4604 14.5 84.4604L70.5 84.4604C71.6046 84.4604 72.5 83.565 72.5 82.4604Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.5 82.4604L70.5 6.46045L27.8284 6.46045L14.5 19.7889L14.5 82.4604L70.5 82.4604ZM27.833 6.45583C27.833 6.45583 27.8329 6.45595 27.8327 6.45617L27.833 6.45583ZM72.5 6.46045L72.5 82.4604C72.5 83.565 71.6046 84.4604 70.5 84.4604L14.5 84.4604C13.3954 84.4604 12.5 83.565 12.5 82.4604L12.5 19.7889C12.5 19.2584 12.7085 18.752 13.0835 18.3769L26.4188 5.04162C26.7939 4.66655 27.298 4.46045 27.8284 4.46045L70.5 4.46045C71.6046 4.46045 72.5 5.35588 72.5 6.46045Z" fill="#0E3781"/>
|
||||
<path d="M84.5 48.4604C84.5 59.5061 75.5457 68.4604 64.5 68.4604C53.4543 68.4604 44.5 59.5061 44.5 48.4604C44.5 37.4148 53.4543 28.4604 64.5 28.4604C75.5457 28.4604 84.5 37.4148 84.5 48.4604Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.5 66.4604C74.4411 66.4604 82.5 58.4016 82.5 48.4604C82.5 38.5193 74.4411 30.4604 64.5 30.4604C54.5589 30.4604 46.5 38.5193 46.5 48.4604C46.5 58.4016 54.5589 66.4604 64.5 66.4604ZM64.5 68.4604C75.5457 68.4604 84.5 59.5061 84.5 48.4604C84.5 37.4148 75.5457 28.4604 64.5 28.4604C53.4543 28.4604 44.5 37.4148 44.5 48.4604C44.5 59.5061 53.4543 68.4604 64.5 68.4604Z" fill="#0E3781"/>
|
||||
<path d="M79.5 48.4604C79.5 56.7447 72.7843 63.4604 64.5 63.4604C56.2157 63.4604 49.5 56.7447 49.5 48.4604C49.5 40.1762 56.2157 33.4604 64.5 33.4604C72.7843 33.4604 79.5 40.1762 79.5 48.4604Z" fill="#99BAF4"/>
|
||||
<path d="M95.5038 77.5474L79 61.9604L77 63.9604L92.587 80.4643C93.3607 81.2836 94.6583 81.3021 95.4552 80.5053L95.5448 80.4156C96.3417 79.6188 96.3231 78.3212 95.5038 77.5474Z" fill="#0E3781"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.5 5.46045C29.0523 5.46045 29.5 5.90816 29.5 6.46045V21.4604H14.5C13.9477 21.4604 13.5 21.0127 13.5 20.4604C13.5 19.9082 13.9477 19.4604 14.5 19.4604H27.5V6.46045C27.5 5.90816 27.9477 5.46045 28.5 5.46045Z" fill="#0E3781"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5 28.4604C19.9477 28.4604 19.5 28.9082 19.5 29.4604C19.5 30.0127 19.9477 30.4604 20.5 30.4604H30.5C31.0523 30.4604 31.5 30.0127 31.5 29.4604C31.5 28.9082 31.0523 28.4604 30.5 28.4604H20.5ZM34.5 28.4604C33.9477 28.4604 33.5 28.9082 33.5 29.4604C33.5 30.0127 33.9477 30.4604 34.5 30.4604H44.5C45.0523 30.4604 45.5 30.0127 45.5 29.4604C45.5 28.9082 45.0523 28.4604 44.5 28.4604H34.5ZM51.483 33.2759C51.3964 32.8118 50.9892 32.4604 50.5 32.4604H40.5C39.9477 32.4604 39.5 32.9082 39.5 33.4604C39.5 34.0127 39.9477 34.4604 40.5 34.4604H50.2171C50.6218 34.0477 51.0441 33.6524 51.483 33.2759ZM44.5246 49.4604C44.5579 50.1365 44.6247 50.8037 44.7235 51.4604H40.5C39.9477 51.4604 39.5 51.0127 39.5 50.4604C39.5 49.9082 39.9477 49.4604 40.5 49.4604H44.5246ZM44.7235 45.4605C44.6247 46.1172 44.5579 46.7844 44.5246 47.4605L37.5 47.4604C36.9477 47.4604 36.5 47.0127 36.5 46.4604C36.5 45.9082 36.9477 45.4604 37.5 45.4604L44.7235 45.4605ZM48.4985 36.4604C48.0192 37.0986 47.5772 37.7663 47.1756 38.4604L38.5 38.4604C37.9477 38.4604 37.5 38.0127 37.5 37.4604C37.5 36.9082 37.9477 36.4604 38.5 36.4604L48.4985 36.4604ZM64.5 28.4604C61.3707 28.4604 58.4093 29.1791 55.7717 30.4604L54.5 30.4604C53.9477 30.4604 53.5 30.0127 53.5 29.4604C53.5 28.9082 53.9477 28.4604 54.5 28.4604H64.5ZM48.5 28.4604C47.9477 28.4604 47.5 28.9082 47.5 29.4604C47.5 30.0127 47.9477 30.4604 48.5 30.4604H50.5C51.0523 30.4604 51.5 30.0127 51.5 29.4604C51.5 28.9082 51.0523 28.4604 50.5 28.4604H48.5ZM37.5 33.4604C37.5 32.9082 37.0523 32.4604 36.5 32.4604H34.5C33.9477 32.4604 33.5 32.9082 33.5 33.4604C33.5 34.0127 33.9477 34.4604 34.5 34.4604H36.5C37.0523 34.4604 37.5 34.0127 37.5 33.4604ZM34.5 38.4604C35.0523 38.4604 35.5 38.0127 35.5 37.4604C35.5 36.9082 35.0523 36.4604 34.5 36.4604H30.5C29.9477 36.4604 29.5 36.9082 29.5 37.4604C29.5 38.0127 29.9477 38.4604 30.5 38.4604H34.5ZM34.5 46.4604C34.5 47.0127 34.0523 47.4604 33.5 47.4604H27.5C26.9477 47.4604 26.5 47.0127 26.5 46.4604C26.5 45.9082 26.9477 45.4604 27.5 45.4604H33.5C34.0523 45.4604 34.5 45.9082 34.5 46.4604ZM36.5 51.4604C37.0523 51.4604 37.5 51.0127 37.5 50.4604C37.5 49.9082 37.0523 49.4604 36.5 49.4604H20.5C19.9477 49.4604 19.5 49.9082 19.5 50.4604C19.5 51.0127 19.9477 51.4604 20.5 51.4604H36.5ZM31.5 33.4604C31.5 32.9082 31.0523 32.4604 30.5 32.4604L20.5 32.4605C19.9477 32.4605 19.5 32.9082 19.5 33.4605C19.5 34.0127 19.9477 34.4605 20.5 34.4605L30.5 34.4604C31.0523 34.4604 31.5 34.0127 31.5 33.4604ZM26.5 38.4604C27.0523 38.4604 27.5 38.0127 27.5 37.4604C27.5 36.9082 27.0523 36.4604 26.5 36.4604H20.5C19.9477 36.4604 19.5 36.9082 19.5 37.4604C19.5 38.0127 19.9477 38.4604 20.5 38.4604H26.5ZM24.5 46.4604C24.5 47.0127 24.0523 47.4604 23.5 47.4604H20.5C19.9477 47.4604 19.5 47.0127 19.5 46.4604C19.5 45.9082 19.9477 45.4604 20.5 45.4604H23.5C24.0523 45.4604 24.5 45.9082 24.5 46.4604Z" fill="#FFBF00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -2,6 +2,9 @@
|
||||
"allApplications": {
|
||||
"message": "All applications"
|
||||
},
|
||||
"appLogoLabel": {
|
||||
"message": "Bitwarden logo"
|
||||
},
|
||||
"criticalApplications": {
|
||||
"message": "Critical applications"
|
||||
},
|
||||
@@ -6306,6 +6309,21 @@
|
||||
"sponsoredFamilies": {
|
||||
"message": "Free Bitwarden Families"
|
||||
},
|
||||
"sponsoredBitwardenFamilies": {
|
||||
"message": "Sponsored families"
|
||||
},
|
||||
"noSponsoredFamilies": {
|
||||
"message": "No sponsored families"
|
||||
},
|
||||
"noSponsoredFamiliesDescription": {
|
||||
"message": "Sponsored non-member families plans will display here"
|
||||
},
|
||||
"sponsorFreeBitwardenFamilies": {
|
||||
"message": "Members of your organization are eligible for Free Bitwarden Families. You can sponsor Free Bitwarden Families for employees who are not a member of your Bitwarden organization. Sponsoring a non-member requires an available seat within your organization."
|
||||
},
|
||||
"sponsoredFamiliesRemoveActiveSponsorship": {
|
||||
"message": "When you remove an active sponsorship, a seat within your organization will be available after the renewal date of the sponsored organization."
|
||||
},
|
||||
"sponsoredFamiliesEligible": {
|
||||
"message": "You and your family are eligible for Free Bitwarden Families. Redeem with your personal email to keep your data secure even when you are not at work."
|
||||
},
|
||||
@@ -6321,6 +6339,18 @@
|
||||
"sponsoredFamiliesSharedCollections": {
|
||||
"message": "Shared collections for Family secrets"
|
||||
},
|
||||
"memberFamilies": {
|
||||
"message": "Member families"
|
||||
},
|
||||
"noMemberFamilies": {
|
||||
"message": "No member families"
|
||||
},
|
||||
"noMemberFamiliesDescription": {
|
||||
"message": "Members who have redeemed family plans will display here"
|
||||
},
|
||||
"membersWithSponsoredFamilies": {
|
||||
"message": "Members of your organization are eligible for Free Bitwarden Families. Here you can see members who have sponsored a Families organization."
|
||||
},
|
||||
"badToken": {
|
||||
"message": "The link is no longer valid. Please have the sponsor resend the offer."
|
||||
},
|
||||
@@ -7984,6 +8014,9 @@
|
||||
"inviteMember": {
|
||||
"message": "Invite member"
|
||||
},
|
||||
"addSponsorship": {
|
||||
"message": "Add sponsorship"
|
||||
},
|
||||
"needsConfirmation": {
|
||||
"message": "Needs confirmation"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<div class="tw-mt-5 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
|
||||
<bit-icon
|
||||
class="tw-w-72 tw-block tw-mb-4"
|
||||
[icon]="logo"
|
||||
[ariaLabel]="'appLogoLabel' | i18n"
|
||||
></bit-icon>
|
||||
<p class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<div class="tw-mt-12 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
|
||||
<bit-icon
|
||||
class="tw-w-72 tw-block tw-mb-4"
|
||||
[icon]="logo"
|
||||
[ariaLabel]="'appLogoLabel' | i18n"
|
||||
></bit-icon>
|
||||
<p class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<div class="tw-mt-12 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="bitwardenLogo"></bit-icon>
|
||||
<bit-icon
|
||||
class="tw-w-72 tw-block tw-mb-4"
|
||||
[icon]="bitwardenLogo"
|
||||
[ariaLabel]="'appLogoLabel' | i18n"
|
||||
></bit-icon>
|
||||
<p class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -229,3 +229,41 @@ export const memberAccessReportsMock: MemberAccessResponse[] = [
|
||||
],
|
||||
} as MemberAccessResponse,
|
||||
];
|
||||
|
||||
export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] = [
|
||||
{
|
||||
userName: "Alice Smith",
|
||||
email: "asmith@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "1234",
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "Alice Group 1",
|
||||
itemCount: 10,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
],
|
||||
} as MemberAccessResponse,
|
||||
{
|
||||
userName: "Robert Brown",
|
||||
email: "rbrown@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "5678",
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [] as MemberAccessDetails[],
|
||||
} as MemberAccessResponse,
|
||||
];
|
||||
|
||||
@@ -4,7 +4,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { MemberAccessReportApiService } from "./member-access-report-api.service";
|
||||
import { memberAccessReportsMock } from "./member-access-report.mock";
|
||||
import {
|
||||
memberAccessReportsMock,
|
||||
memberAccessWithoutAccessDetailsReportsMock,
|
||||
} from "./member-access-report.mock";
|
||||
import { MemberAccessReportService } from "./member-access-report.service";
|
||||
describe("ImportService", () => {
|
||||
const mockOrganizationId = "mockOrgId" as OrganizationId;
|
||||
@@ -112,5 +115,34 @@ describe("ImportService", () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should generate user report export items and include users with no access", async () => {
|
||||
reportApiService.getMemberAccessData.mockImplementation(() =>
|
||||
Promise.resolve(memberAccessWithoutAccessDetailsReportsMock),
|
||||
);
|
||||
const result =
|
||||
await memberAccessReportService.generateUserReportExportItems(mockOrganizationId);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: "asmith@email.com",
|
||||
name: "Alice Smith",
|
||||
twoStepLogin: "memberAccessReportTwoFactorEnabledTrue",
|
||||
accountRecovery: "memberAccessReportAuthenticationEnabledTrue",
|
||||
group: "Alice Group 1",
|
||||
totalItems: "10",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
email: "rbrown@email.com",
|
||||
name: "Robert Brown",
|
||||
twoStepLogin: "memberAccessReportTwoFactorEnabledFalse",
|
||||
accountRecovery: "memberAccessReportAuthenticationEnabledFalse",
|
||||
group: "memberAccessReportNoGroup",
|
||||
totalItems: "0",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,26 @@ export class MemberAccessReportService {
|
||||
}
|
||||
|
||||
const exportItems = memberAccessReports.flatMap((report) => {
|
||||
// to include users without access details
|
||||
// which means a user has no groups, collections or items
|
||||
if (report.accessDetails.length === 0) {
|
||||
return [
|
||||
{
|
||||
email: report.email,
|
||||
name: report.userName,
|
||||
twoStepLogin: report.twoFactorEnabled
|
||||
? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"),
|
||||
accountRecovery: report.accountRecoveryEnabled
|
||||
? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"),
|
||||
group: this.i18nService.t("memberAccessReportNoGroup"),
|
||||
collection: this.i18nService.t("memberAccessReportNoCollection"),
|
||||
collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"),
|
||||
totalItems: "0",
|
||||
},
|
||||
];
|
||||
}
|
||||
const userDetails = report.accessDetails.map((detail) => {
|
||||
const collectionName = collectionNameMap.get(detail.collectionName.encryptedString);
|
||||
return {
|
||||
|
||||
@@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [
|
||||
I18nServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
SyncService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[routerLink]="['/']"
|
||||
class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top"
|
||||
>
|
||||
<bit-icon [icon]="logo"></bit-icon>
|
||||
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
|
||||
</a>
|
||||
|
||||
<div
|
||||
|
||||
@@ -536,6 +536,10 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
if (storedEmail) {
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
// If we load an email into the form, we need to initialize it for the login process as well
|
||||
// so that other login components can use it.
|
||||
// We do this here as it's possible that a user doesn't edit the email field before submitting.
|
||||
this.loginEmailService.setLoginEmail(storedEmail);
|
||||
} else {
|
||||
this.formGroup.controls.rememberEmail.setValue(false);
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ export class OrganizationSponsorshipCreateRequest {
|
||||
sponsoredEmail: string;
|
||||
planSponsorshipType: PlanSponsorshipType;
|
||||
friendlyName: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export class DeviceResponse extends BaseResponse {
|
||||
creationDate: string;
|
||||
revisionDate: string;
|
||||
isTrusted: boolean;
|
||||
encryptedUserKey: string | null;
|
||||
encryptedPublicKey: string | null;
|
||||
|
||||
devicePendingAuthRequest: DevicePendingAuthRequest | null;
|
||||
|
||||
constructor(response: any) {
|
||||
@@ -27,6 +30,8 @@ export class DeviceResponse extends BaseResponse {
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
this.isTrusted = this.getResponseProperty("IsTrusted");
|
||||
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
|
||||
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
|
||||
this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||
import { InitiationPath } from "../../models/request/reference-event.request";
|
||||
@@ -59,4 +62,10 @@ export abstract class OrganizationBillingServiceAbstraction {
|
||||
organizationId: string,
|
||||
subscription: SubscriptionInformation,
|
||||
) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria.
|
||||
* @param organization
|
||||
*/
|
||||
abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlanType } from "../../enums";
|
||||
import { PlanSponsorshipType, PlanType } from "../../enums";
|
||||
|
||||
export class PreviewOrganizationInvoiceRequest {
|
||||
organizationId?: string;
|
||||
@@ -21,6 +21,7 @@ export class PreviewOrganizationInvoiceRequest {
|
||||
|
||||
class PasswordManager {
|
||||
plan: PlanType;
|
||||
sponsoredPlan?: PlanSponsorshipType;
|
||||
seats: number;
|
||||
additionalStorage: number;
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
describe("BillingAccountProfileStateService", () => {
|
||||
let apiService: jest.Mocked<ApiService>;
|
||||
let billingApiService: jest.Mocked<BillingApiServiceAbstraction>;
|
||||
let keyService: jest.Mocked<KeyService>;
|
||||
let encryptService: jest.Mocked<EncryptService>;
|
||||
let i18nService: jest.Mocked<I18nService>;
|
||||
let organizationApiService: jest.Mocked<OrganizationApiService>;
|
||||
let syncService: jest.Mocked<SyncService>;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
|
||||
let sut: OrganizationBillingService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
billingApiService = mock<BillingApiServiceAbstraction>();
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
organizationApiService = mock<OrganizationApiService>();
|
||||
syncService = mock<SyncService>();
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
sut = new OrganizationBillingService(
|
||||
apiService,
|
||||
billingApiService,
|
||||
keyService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
organizationApiService,
|
||||
syncService,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("isBreadcrumbingPoliciesEnabled", () => {
|
||||
it("returns false when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when organization belongs to a provider", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: true,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when cannot edit subscription", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: false,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Teams", ProductTierType.Teams],
|
||||
["TeamsStarter", ProductTierType.TeamsStarter],
|
||||
])("returns true when all conditions are met with %s tier", async (_, productTierType) => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: productTierType,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(true);
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM12276_BreadcrumbEventLogs,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false when product tier is not supported", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Enterprise,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("handles all conditions false correctly", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
const org = {
|
||||
isProviderUser: true,
|
||||
canEditSubscription: false,
|
||||
productTierType: ProductTierType.Free,
|
||||
} as Organization;
|
||||
|
||||
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(actual).toBe(false);
|
||||
});
|
||||
|
||||
it("verifies feature flag is only called once", async () => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
const org = {
|
||||
isProviderUser: false,
|
||||
canEditSubscription: true,
|
||||
productTierType: ProductTierType.Teams,
|
||||
} as Organization;
|
||||
|
||||
await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
|
||||
expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user