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

Merge branch 'main' into platform/pm-11936/remove-token-refresh-before-sync

This commit is contained in:
Todd Martin
2025-02-03 11:13:49 -05:00
committed by GitHub
67 changed files with 4542 additions and 6217 deletions

3
.github/CODEOWNERS vendored
View File

@@ -97,12 +97,15 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
.github/workflows/scan.yml @bitwarden/team-platform-dev
.github/workflows/test.yml @bitwarden/team-platform-dev
.github/workflows/version-auto-bump.yml @bitwarden/team-platform-dev
# ESLint custom rules
libs/eslint @bitwarden/team-platform-dev
## Autofill team files ##
apps/browser/src/autofill @bitwarden/team-autofill-dev
apps/desktop/src/autofill @bitwarden/team-autofill-dev
libs/common/src/autofill @bitwarden/team-autofill-dev
apps/desktop/macos/autofill-extension @bitwarden/team-autofill-dev
apps/desktop/desktop_native/windows-plugin-authenticator @bitwarden/team-autofill-dev
# DuckDuckGo integration
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-dev

View File

@@ -211,6 +211,8 @@
"@storybook/angular",
"@storybook/manager-api",
"@storybook/theming",
"@typescript-eslint/utils",
"@typescript-eslint/rule-tester",
"@types/react",
"autoprefixer",
"bootstrap",

View File

@@ -88,7 +88,7 @@ jobs:
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
- name: Upload results to codecov.io
uses: codecov/test-results-action@9739113ad922ea0a9abb4b2c0f8bf6a4aa8ef820 # v1.0.1
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
"logInRequestSent": {
"message": "Request sent"
},
"exposedMasterPassword": {
"message": "Exposed Master Password"
},

View File

@@ -7,13 +7,20 @@
<div class="content login-page">
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<div>
<p class="lead">{{ "loginInitiated" | i18n }}</p>
<p class="lead">{{ "logInRequestSent" | i18n }}</p>
<div>
<p>{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
{{ "notificationSentDevicePart1" | i18n }}
<a
bitLink
linkType="primary"
class="tw-cursor-pointer"
[href]="deviceManagementUrl"
target="_blank"
rel="noreferrer"
>{{ "notificationSentDeviceAnchor" | i18n }}</a
>. {{ "notificationSentDevicePart2" | i18n }}
</p>
</div>

View File

@@ -8,7 +8,7 @@ const getAbsolutePath = (value: string): string =>
dirname(require.resolve(join(value, "package.json")));
const config: StorybookConfig = {
stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)"],
stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)", "../lit-stories/**/*.mdx"],
addons: [
getAbsolutePath("@storybook/addon-links"),
getAbsolutePath("@storybook/addon-essentials"),

View File

@@ -0,0 +1,64 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./action-button.lit-stories";
<Meta title="Components/Buttons/Action Button" of={stories} />
## Action Button
The `ActionButton` component is a customizable button built using the `lit` library and styled with
`@emotion/css`. This component supports themes, handles click events, and includes a disabled state.
It is designed with accessibility and responsive design in mind.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| -------------- | -------------------------- | ------------ | ----------------------------------------------------------- |
| `buttonAction` | `(e: Event) => void` | Yes | The function to execute when the button is clicked. |
| `buttonText` | `string` | Yes | The text to display on the button. |
| `disabled` | `boolean` (default: false) | No | Disables the button when set to `true`. |
| `theme` | `Theme` | Yes | The theme to style the button. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.
2. Pass the required props to the component when rendering:
- `buttonAction`: A function that handles the click event.
- `buttonText`: The text displayed on the button.
- `disabled` (optional): A boolean indicating whether the button is disabled.
- `theme`: The theme to style the button (must be a valid `Theme`).
## Accessibility (WCAG) Compliance
The `ActionButton` component follows the
[W3C ARIA button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/). Below is a breakdown of
key accessibility considerations:
### Keyboard Accessibility
- The button supports keyboard interaction through the `@click` event.
- Users can activate the button using the `Enter` or `Space` key.
### Screen Reader Compatibility
- The `title` attribute is dynamically set to the button's text (`buttonText`), ensuring it is read
by screen readers.
- The semantic `<button>` element is used, which is natively recognized by assistive technologies.
### Focus Management
- Ensure proper focus management when interacting with the button, especially when navigating using
a keyboard.
### Visual Feedback
- The button provides clear visual states for hover and disabled states:
- **Hover:** Changes background color, border color, and text contrast.
- **Disabled:** Muted background and text colors with no pointer events.

View File

@@ -0,0 +1,64 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./badge-button.lit-stories";
<Meta title="Components/Buttons/Badge Button" of={stories} />
## Badge Button
The `BadgeButton` component is a compact, styled button built using the `lit` library and styled
with `@emotion/css`. It is designed to display concise text and supports themes, click event
handling, and a disabled state. The component is optimized for accessibility and responsive design.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| -------------- | -------------------------- | ------------ | ----------------------------------------------------------- |
| `buttonAction` | `(e: Event) => void` | Yes | The function to execute when the button is clicked. |
| `buttonText` | `string` | Yes | The text to display on the badge button. |
| `disabled` | `boolean` (default: false) | No | Disables the button when set to `true`. |
| `theme` | `Theme` | Yes | The theme to style the button. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.
2. Pass the required props to the component when rendering:
- `buttonAction`: A function that handles the click event.
- `buttonText`: The text displayed on the badge button.
- `disabled` (optional): A boolean indicating whether the button is disabled.
- `theme`: The theme to style the button (must be a valid `Theme`).
## Accessibility (WCAG) Compliance
The `BadgeButton` component follows the
[W3C ARIA button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/). Below is a breakdown of
key accessibility considerations:
### Keyboard Accessibility
- The button supports keyboard interaction through the `@click` event.
- Users can activate the button using the `Enter` or `Space` key.
### Screen Reader Compatibility
- The `title` attribute is dynamically set to the button's text (`buttonText`), ensuring it is read
by screen readers.
- The semantic `<button>` element is used, which is natively recognized by assistive technologies.
### Focus Management
- Ensure proper focus management when interacting with the button, especially when navigating using
a keyboard.
### Visual Feedback
- The button provides clear visual states for hover and disabled states:
- **Hover:** Changes background color, border color, and text contrast.
- **Disabled:** Muted background and text colors with no pointer events.

View File

@@ -0,0 +1,70 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./body.lit-stories";
<Meta title="Components/Notifications/Notification Body" of={stories} />
## Notification Body
The `NotificationBody` component displays a detailed notification with a list of associated ciphers,
notification type, and styling based on the selected theme. It is a flexible component for
presenting actionable information.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ------------------ | ------------------ | ------------ | --------------------------------------------------------------------------------------------------------- |
| `ciphers` | `CipherData[]` | Yes | An array of cipher data objects. Each cipher includes metadata such as ID, name, type, and login details. |
| `notificationType` | `NotificationType` | Yes | Specifies the type of notification, such as `add`, `change`, `unlock`, or `fileless-import`. |
| `theme` | `Theme` | Yes | Defines the theme used for styling the notification. Must match the `Theme` enum. |
---
## Usage Example
```tsx
import { NotificationBody } from "../../notification/body";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<NotificationBody
ciphers={[
{
id: "1",
name: "Example Cipher",
type: "Login",
favorite: false,
reprompt: "None",
icon: {
imageEnabled: true,
image: "",
fallbackImage: "https://example.com/fallback.png",
icon: "icon-class",
},
login: { username: "user@example.com", passkey: null },
},
]}
notificationType="add"
theme={ThemeTypes.Dark}
/>;
```
### Accessibility (WCAG) Compliance
The NotificationBody component is designed to be accessible and adheres to WCAG guidelines:
## Screen Reader Compatibility
- Ciphers are presented with clear labeling to ensure context for assistive technologies.
- Icons include fallback options for better accessibility.
## Visual Feedback
- `notificationType` adjusts the visual presentation for contextually relevant feedback.
### Notes
- ciphers: Ensure the array includes well-defined cipher objects to avoid rendering issues.
- notificationType: This prop influences the messaging and appearance of the notification body.

View File

@@ -0,0 +1,83 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./cipher-action.lit-stories";
<Meta title="Components/Ciphers/Cipher Action" of={stories} />
## Cipher Action
The `CipherAction` component is a functional UI element that handles actions related to ciphers in a
secure environment. Built with the `lit` library and styled for consistency across themes, it
provides flexibility and accessibility while supporting various notification types.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ------------------ | --------------------------------------------------- | ------------ | -------------------------------------------------------------- |
| `handleAction` | `(e: Event) => void` | No | Function to execute when an action is triggered. |
| `notificationType` | `NotificationTypes.Change \| NotificationTypes.Add` | Yes | Specifies the type of notification associated with the action. |
| `theme` | `Theme` | Yes | The theme to style the component. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure the necessary dependencies are installed:
- `lit`: Used to render the component.
2. Pass the required props when rendering the component:
- `handleAction`: Optional function to handle the triggered action.
- `notificationType`: Mandatory type from `NotificationTypes` to define the action context.
- `theme`: The styling theme (must be a valid `Theme` enum value).
## Accessibility (WCAG) Compliance
The `CipherAction` component is designed to be accessible, ensuring usability across diverse user
bases. Below are the key considerations for accessibility:
### Keyboard Accessibility
- Fully navigable using the keyboard.
- The action can be triggered using the `Enter` or `Space` key for users relying on keyboard
interaction.
### Screen Reader Compatibility
- The semantic elements used in the `CipherAction` component ensure that assistive technologies can
interpret the component correctly.
- Text associated with the `notificationType` is programmatically linked, providing clarity for
screen reader users.
### Focus Management
- The component includes focus styles to ensure visibility during navigation.
- Proper focus management ensures the component works seamlessly with keyboard navigation.
### Visual Feedback
- Provides distinct visual states for different themes and states:
- **Hover:** Adjustments to background, border, and text for enhanced visibility.
- **Active:** Highlights the button with a focus state when activated.
- **Disabled:** Grays out the component to indicate inactivity.
## Usage Example
Here's an example of how to integrate the `CipherAction` component:
```ts
import { CipherAction } from "../../cipher/cipher-action";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
import { NotificationTypes } from "../../../../notification/abstractions/notification-bar";
const handleAction = (e: Event) => {
console.log("Cipher action triggered!", e);
};
<CipherAction
handleAction={handleAction}
notificationType={NotificationTypes.Change}
theme={ThemeTypes.Dark}
/>;
```

View File

@@ -0,0 +1,90 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./cipher-icon.lit-stories";
<Meta title="Components/Ciphers/Cipher Icon" of={stories} />
## Cipher Icon
The `CipherIcon` component is a versatile icon renderer designed for secure environments. It
dynamically supports custom icons provided via URIs or displays a default icon (`Globe`) styled
based on the theme and provided properties.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| -------- | ------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `color` | `string` | Yes | A contextual color override applied when the `uri` is not provided, ensuring consistent styling of the default icon. |
| `size` | `string` | Yes | A valid CSS `width` value representing the width basis of the graphic. The height adjusts to maintain the original aspect ratio of the graphic. |
| `theme` | `Theme` | Yes | The styling theme for the icon, matching the `Theme` enum. |
| `uri` | `string` (optional) | No | A URL to an external graphic. If provided, the component displays this icon. If omitted, a default icon (`Globe`) styled with the provided `color` and `theme`. |
## Installation and Setup
1. Ensure the necessary dependencies are installed:
- `lit`: Renders the component.
- `@emotion/css`: Styles the component.
2. Pass the necessary props when using the component:
- `color`: Used when no `uri` is provided to style the default icon.
- `size`: Defines the width of the icon. Height maintains aspect ratio.
- `theme`: Specifies the theme for styling.
- `uri` (optional): If provided, this URI is used to display a custom icon.
## Accessibility (WCAG) Compliance
The `CipherIcon` component ensures accessible and user-friendly interactions through thoughtful
design:
### Semantic Rendering
- When the `uri` is provided, the component renders an `<img>` element, which is semantically
appropriate for external graphics.
- If no `uri` is provided, the default icon is wrapped in a `<span>`, ensuring proper context for
screen readers.
### Visual Feedback
- The component visually adjusts based on the `size`, `color`, and `theme`, ensuring the icon
remains clear and legible across different environments.
### Keyboard and Screen Reader Support
- Ensure that any container or parent component provides appropriate `alt` text or labeling when
`uri` is used with an `<img>` tag for additional accessibility.
## Usage Example
Here's an example of how to integrate the `CipherIcon` component:
```ts
import { CipherIcon } from "./cipher-icon";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<CipherIcon
color="blue"
size="32px"
theme={ThemeTypes.Light}
uri="https://example.com/icon.png"
/>;
```
This configuration displays a custom icon from the provided URI with a width of 32px, styled for the
light theme. If the URI is omitted, the Globe icon is used as the fallback, colored in blue.
### Default Styles
- The default styles ensure responsive and clean design:
- Width: Defined by the size prop.
- Height: Automatically adjusts to maintain the aspect ratio.
- Fit Content: Ensures the icon does not overflow or distort its container.
### Notes
- Always validate the uri provided to ensure it points to a secure and accessible location.
- Use the color and theme props for consistent fallback styling when uri is not provided.

View File

@@ -0,0 +1,81 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./cipher-indicator-icon.lit-stories";
<Meta title="Components/Ciphers/Cipher Indicator Icon" of={stories} />
## Cipher Info Indicator Icons
The `CipherInfoIndicatorIcons` component displays a set of icons indicating specific attributes
related to cipher information. It supports business and family organization indicators, styled
dynamically based on the provided theme.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| --------------- | --------- | ------------ | ----------------------------------------------------------------------- |
| `isBusinessOrg` | `boolean` | No | Displays the business organization icon when set to `true`. |
| `isFamilyOrg` | `boolean` | No | Displays the family organization icon when set to `true`. |
| `theme` | `Theme` | Yes | Defines the theme used to style the icons. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure the necessary dependencies are installed:
- `lit`: Renders the component.
- `@emotion/css`: Used for styling.
2. Pass the required props when using the component:
- `isBusinessOrg`: A boolean that, when `true`, displays the business icon.
- `isFamilyOrg`: A boolean that, when `true`, displays the family icon.
- `theme`: Specifies the theme for styling the icons.
## Accessibility (WCAG) Compliance
The `CipherInfoIndicatorIcons` component ensures accessibility and usability through its design:
### Screen Reader Compatibility
- Icons are rendered as `<svg>` elements, and parent components should provide appropriate labeling
or descriptions to convey their meaning to screen readers.
### Visual Feedback
- Icons are styled dynamically based on the `theme` to ensure visual clarity and contrast in all
supported themes.
- The size of the icons is fixed at `12px` in height to maintain a consistent visual appearance.
## Usage Example
Here's an example of how to integrate the `CipherInfoIndicatorIcons` component:
```ts
import { CipherInfoIndicatorIcons } from "./cipher-info-indicator-icons";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<CipherInfoIndicatorIcons
isBusinessOrg={true}
isFamilyOrg={false}
theme={ThemeTypes.Dark}
/>;
```
This example displays the business organization icon, styled for the dark theme, and omits the
family organization icon.
### Styling Details
- The component includes the following styles:
- Icons: Rendered as SVGs with a height of 12px and a width that adjusts to maintain their aspect
ratio.
- Color: Icons are dynamically styled based on the theme, using muted text colors for a subtle
appearance.
### Notes
- If neither isBusinessOrg nor isFamilyOrg is set to true, the component renders nothing. This
behavior should be handled by the parent component.

View File

@@ -0,0 +1,55 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./close-button.lit-stories";
<Meta title="Components/Buttons/Close Button" of={stories} />
## Close Button
The `CloseButton` component is a lightweight, themeable button used for closing notifications or
dismissing elements. It is built using the `lit` library, styled with `@emotion/css`, and integrates
a close icon for visual clarity. The component is designed to be intuitive and accessible.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ------------------------- | -------------------- | ------------ | ----------------------------------------------------------- |
| `handleCloseNotification` | `(e: Event) => void` | Yes | The function to execute when the button is clicked. |
| `theme` | `Theme` | Yes | The theme to style the button. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.
2. Pass the required props to the component when rendering:
- `handleCloseNotification`: A function that handles the click event for closing.
- `theme`: The theme to style the button (must be a valid `Theme`).
## Accessibility (WCAG) Compliance
The `CloseButton` component follows the
[W3C ARIA button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/). Below is a breakdown of
key accessibility considerations:
### Keyboard Accessibility
- The button supports keyboard interaction through the `@click` event.
- Users can activate the button using the `Enter` or `Space` key.
### Screen Reader Compatibility
- The button uses a semantic `<button>` element, ensuring it is natively recognized by assistive
technologies.
### Focus Management
- Ensure the button receives proper focus, especially when navigating using a keyboard.
### Visual Feedback
- The button provides hover feedback by changing the border color for better user interaction.

View File

@@ -0,0 +1,61 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./edit-button.lit-stories";
<Meta title="Components/Buttons/Edit Button" of={stories} />
## Edit Button
The `EditButton` component is a small, themeable button that integrates an editable pencil icon to
represent edit functionality. It is built using the `lit` library and styled with `@emotion/css`.
The component supports themes, click events, and a disabled state, making it ideal for use in forms
or settings where inline editing is required.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| -------------- | -------------------------- | ------------ | ----------------------------------------------------------- |
| `buttonAction` | `(e: Event) => void` | Yes | The function to execute when the button is clicked. |
| `buttonText` | `string` | Yes | The text displayed as the button's tooltip. |
| `disabled` | `boolean` (default: false) | No | Disables the button when set to `true`. |
| `theme` | `Theme` | Yes | The theme to style the button. Must match the `Theme` enum. |
## Installation and Setup
1. Ensure you have the necessary dependencies installed:
- `lit`: Used to render the component.
- `@emotion/css`: Used for styling the component.
2. Pass the required props to the component when rendering:
- `buttonAction`: A function that handles the click event.
- `buttonText`: The text displayed as a tooltip for the button.
- `disabled` (optional): A boolean indicating whether the button is disabled.
- `theme`: The theme to style the button (must be a valid `Theme`).
## Accessibility (WCAG) Compliance
The `EditButton` component follows the
[W3C ARIA button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/). Below is a breakdown of
key accessibility considerations:
### Keyboard Accessibility
- The button supports keyboard interaction through the `@click` event.
- Users can activate the button using the `Enter` or `Space` key.
### Screen Reader Compatibility
- The `title` attribute is dynamically set to the button's text (`buttonText`), ensuring it is read
by screen readers.
- The semantic `<button>` element is used, which is natively recognized by assistive technologies.
### Focus Management
- Ensure the button receives proper focus, especially when navigating using a keyboard.
### Visual Feedback
- The button provides hover feedback by changing the border color for better user interaction.

View File

@@ -0,0 +1,45 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./footer.lit-stories";
<Meta title="Components/Notifications/Notification Footer" of={stories} />
## Notification Footer
The `NotificationFooter` component is used to display a footer for notifications, allowing dynamic
customization based on the `theme` and `notificationType`.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ------------------ | ------------------ | ------------ | -------------------------------------------------------------------------------------------------- |
| `notificationType` | `NotificationType` | Yes | The type of notification footer to display. Options: `add`, `change`, `unlock`, `fileless-import`. |
| `theme` | `Theme` | Yes | Defines the theme of the notification footer. Must match the `Theme` enum. |
---
## Usage Example
```tsx
import { NotificationFooter } from "../../notification/footer";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<NotificationFooter notificationType="add" theme={ThemeTypes.Dark} />;
```
## Accessibility (WCAG) Compliance
The `NotificationFooter` component has been designed with accessibility in mind:
### Screen Reader Compatibility
- Ensures that the notification type and relevant information are accessible to assistive
technologies.
## Visual Feedback
- The theme prop dynamically adjusts colors and contrast for light and dark modes.
- Provides clear visual separation to enhance readability.

View File

@@ -0,0 +1,56 @@
import { Meta, Controls, Primary } from "@storybook/addon-docs";
import * as stories from "./header.lit-stories";
<Meta title="Components/Notifications/Notification Header" of={stories} />
## Notification Header
The `NotificationHeader` component is used to display a notification banner with a message, theme,
and an optional close button. This component is versatile and can be styled dynamically based on the
`theme` and other provided props.
<Primary />
<Controls />
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ------------------------- | -------------------- | ------------ | ------------------------------------------------------------------- |
| `message` | `string` | Yes | The text message to be displayed in the notification. |
| `standalone` | `boolean` | No | Determines if the notification is displayed independently. |
| `theme` | `Theme` | Yes | Defines the theme of the notification. Must match the `Theme` enum. |
| `handleCloseNotification` | `(e: Event) => void` | No | A callback function triggered when the close button is clicked. |
---
## Usage Example
```tsx
import { NotificationHeader } from "../../notification/header";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<NotificationHeader
message="This is a sample notification."
standalone={true}
theme={ThemeTypes.Dark}
handleCloseNotification={() => console.log("Notification closed!")}
/>;
```
## Accessibility (WCAG) Compliance
The `NotificationHeader` component is designed with accessibility in mind:
### Screen Reader Compatibility
- The message is rendered in a way that ensures visibility to assistive technologies.
### Visual Feedback
- The theme prop dynamically adjusts colors and contrast for light and dark modes.
- The component provides clear visual separation to ensure readability in all themes.
### Notes
- If handleCloseNotification is not provided, the close button will not trigger any action.

View File

@@ -0,0 +1,69 @@
import { Meta, Controls } from "@storybook/addon-docs";
import * as stories from "./icons.lit-stories";
<Meta title="Components/Icons/Icons" of={stories} />
## Icon Stories
The `Icons` component suite demonstrates various icon components styled dynamically based on props
like size, color, and theme. Each story is an example of how a specific icon can be rendered.
<Controls />
### Icons
| | |
| ------------------------- | ------------------ |
| `AngleDownIcon` | `FolderIcon` |
| `BusinessIcon` | `GlobeIcon` |
| `BrandIcon` | `PartyHornIcon` |
| `CloseIcon` | `PencilSquareIcon` |
| `ExclamationTriangleIcon` | `ShieldIcon` |
| `FamilyIcon` | `UserIcon` |
## Props
| **Prop** | **Type** | **Required** | **Description** |
| ---------- | --------- | ------------ | --------------------------------------------------------------------------------- |
| `iconLink` | `URL` | No | Defines an external URL associated with the icon, prop exclusive to `Brand Icon`. |
| `color` | `string` | No | Sets the color of the icon. |
| `disabled` | `boolean` | No | Disables the icon visually and functionally. |
| `theme` | `Theme` | Yes | Defines the theme used to style the icons. Must match the `Theme` enum. |
| `size` | `number` | Yes | Sets the width and height of the icon in pixels. |
---
## Standard Icon Usage Example
```tsx
import { AngleDownIcon } from "./Icons";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<AngleDownIcon size={50} color="#000000" theme={ThemeTypes.Light} />;
```
## Brand Icon Usage Example
```tsx
import { BrandIconContainer } from "./Icons";
import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
<BrandIconContainer
size={50}
color="#000000"
theme={ThemeTypes.Light}
iconLink="https://bitwarden.com"
/>;
```
## Accessibility (WCAG) Compliance
Icons in this suite are designed with accessibility and usability in mind:
### Screen Reader Compatibility
- Icons are rendered as `<svg>` elements.
### Visual Feedback
- The `disabled` prop adjusts the icon's visual appearance, ensuring clarity.

View File

@@ -437,7 +437,7 @@ const routes: Routes = [
data: {
pageIcon: DevicesIcon,
pageTitle: {
key: "loginInitiated",
key: "logInRequestSent",
},
pageSubtitle: {
key: "aNotificationWasSentToYourDevice",

View File

@@ -5,3 +5,4 @@ index.node
npm-debug.log*
*.node
dist
windows_pluginauthenticator_bindings.rs

View File

@@ -410,6 +410,26 @@ 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"
@@ -553,6 +573,15 @@ 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"
@@ -593,6 +622,17 @@ 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.27"
@@ -1458,6 +1498,15 @@ 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"
@@ -2259,6 +2308,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
@@ -2437,6 +2496,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -3447,6 +3512,13 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-plugin-authenticator"
version = "0.0.0"
dependencies = [
"bindgen",
]
[[package]]
name = "windows-registry"
version = "0.4.0"

View File

@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["napi", "core", "proxy", "macos_provider"]
members = ["napi", "core", "proxy", "macos_provider", "windows-plugin-authenticator"]
[workspace.dependencies]
anyhow = "=1.0.94"

View File

@@ -0,0 +1,9 @@
[package]
name = "windows-plugin-authenticator"
version = "0.0.0"
edition = "2021"
license = "GPL-3.0"
publish = false
[target.'cfg(target_os = "windows")'.build-dependencies]
bindgen = "0.71.1"

View File

@@ -0,0 +1,23 @@
# windows-plugin-authenticator
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.
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'
```

View File

@@ -0,0 +1,22 @@
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()))
.generate()
.expect("Unable to generate bindings.");
bindings
.write_to_file(format!(
"{}\\windows_pluginauthenticator_bindings.rs",
out_dir
))
.expect("Couldn't write bindings.");
}

View File

@@ -0,0 +1,231 @@
/*
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

View File

@@ -0,0 +1,11 @@
#![cfg(target_os = "windows")]
mod pa;
pub fn get_version_number() -> u64 {
unsafe { pa::WebAuthNGetApiVersionNumber() }.into()
}
pub fn add_authenticator() {
unimplemented!();
}

View File

@@ -0,0 +1,15 @@
/*
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_pluginauthenticator_bindings.rs"
));

View File

@@ -224,7 +224,7 @@ const routes: Routes = [
data: {
pageIcon: DevicesIcon,
pageTitle: {
key: "loginInitiated",
key: "logInRequestSent",
},
pageSubtitle: {
key: "aNotificationWasSentToYourDevice",

View File

@@ -51,15 +51,15 @@ describe("DesktopLoginApprovalComponentService", () => {
it("calls ipc.auth.loginRequest with correct parameters when window is not visible", async () => {
const title = "Log in requested";
const email = "test@bitwarden.com";
const message = `Confirm login attempt for ${email}`;
const message = `Confirm access attempt for ${email}`;
const closeText = "Close";
const loginApprovalComponent = { email } as LoginApprovalComponent;
i18nService.t.mockImplementation((key: string) => {
switch (key) {
case "logInRequested":
case "accountAccessRequested":
return title;
case "confirmLoginAtemptForMail":
case "confirmAccessAttempt":
return message;
case "close":
return closeText;

View File

@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular";
@@ -15,12 +13,12 @@ export class DesktopLoginApprovalComponentService
super();
}
async showLoginRequestedAlertIfWindowNotVisible(email: string): Promise<void> {
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
const isVisible = await ipc.platform.isWindowVisible();
if (!isVisible) {
await ipc.auth.loginRequest(
this.i18nService.t("logInRequested"),
this.i18nService.t("confirmLoginAtemptForMail", email),
this.i18nService.t("accountAccessRequested"),
this.i18nService.t("confirmAccessAttempt", email),
this.i18nService.t("close"),
);
}

View File

@@ -3,15 +3,23 @@
<img class="logo-image" alt="Bitwarden" />
<ng-container *ngIf="state == StateEnum.StandardAuthRequest">
<p class="lead text-center">{{ "loginInitiated" | i18n }}</p>
<p class="lead text-center">{{ "logInRequestSent" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="section">
<p class="section">{{ "notificationSentDevice" | i18n }}</p>
<p>
{{ "fingerprintMatchInfo" | i18n }}
<p class="section">
{{ "notificationSentDevicePart1" | i18n }}
<a
bitLink
linkType="primary"
class="tw-cursor-pointer"
[href]="deviceManagementUrl"
target="_blank"
rel="noreferrer"
>{{ "notificationSentDeviceAnchor" | i18n }}</a
>. {{ "notificationSentDevicePart2" | i18n }}
</p>
</div>

View File

@@ -2745,14 +2745,23 @@
"loginInitiated": {
"message": "Login initiated"
},
"logInRequestSent": {
"message": "Request sent"
},
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the "
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"needAnotherOptionV1": {
"message": "Need another option?"
@@ -2782,11 +2791,11 @@
"message": "Toggle character count",
"description": "'Character count' describes a feature that displays a number next to each character of the password."
},
"areYouTryingtoLogin": {
"message": "Are you trying to log in?"
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"logInAttemptBy": {
"message": "Login attempt by $EMAIL$",
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
@@ -2803,11 +2812,11 @@
"time": {
"message": "Time"
},
"confirmLogIn": {
"message": "Confirm login"
"confirmAccess": {
"message": "Confirm access"
},
"denyLogIn": {
"message": "Deny login"
"denyAccess": {
"message": "Deny access"
},
"logInConfirmedForEmailOnDevice": {
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
@@ -2843,8 +2852,8 @@
"thisRequestIsNoLongerValid": {
"message": "This request is no longer valid."
},
"confirmLoginAtemptForMail": {
"message": "Confirm login attempt for $EMAIL$",
"confirmAccessAttempt": {
"message": "Confirm access attempt for $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
@@ -2855,6 +2864,9 @@
"logInRequested": {
"message": "Log in requested"
},
"accountAccessRequested": {
"message": "Account access requested"
},
"creatingAccountOn": {
"message": "Creating account on"
},

View File

@@ -77,10 +77,11 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return (await this.getApplicationVersion()).split(/[+|-]/)[0].trim();
}
// Temporarily restricted to only Windows until https://github.com/electron/electron/pull/28349
// has been merged and an updated electron build is available.
// Restricted to Windows and Mac. Mac is missing support for pin entry, and Linux is missing support entirely and has to be implemented in another way.
supportsWebAuthn(win: Window): boolean {
return this.getDevice() === DeviceType.WindowsDesktop;
return (
this.getDevice() === DeviceType.WindowsDesktop || this.getDevice() === DeviceType.MacOsDesktop
);
}
supportsDuo(): boolean {

View File

@@ -106,7 +106,7 @@ export class AccountComponent implements OnInit, OnDestroy {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.configService
.getFeatureFlag$(FeatureFlag.limitItemDeletion)
.getFeatureFlag$(FeatureFlag.LimitItemDeletion)
.pipe(takeUntil(this.destroy$))
.subscribe((isAble) => (this.limitItemDeletionFeatureFlagIsEnabled = isAble));

View File

@@ -1,5 +1,3 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable tailwindcss/no-custom-classname -->
<div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
>
@@ -14,15 +12,11 @@
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "loginInitiated" | i18n }}</h2>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInRequestSent" | i18n }}</h2>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
<p class="tw-mb-6">
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<p class="tw-mb-6">
{{ "notificationSentDeviceComplete" | i18n }}
</p>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
@@ -39,7 +33,7 @@
<hr />
<div class="tw-text-light tw-mt-3">
<div class="tw-mt-3">
{{ "loginWithDeviceEnabledNote" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
@@ -52,7 +46,7 @@
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "adminApprovalRequested" | i18n }}</h2>
<div class="tw-text-light">
<div>
<p class="tw-mb-6">{{ "adminApprovalRequestSentToAdmins" | i18n }}</p>
<p class="tw-mb-6">{{ "youWillBeNotifiedOnceApproved" | i18n }}</p>
</div>
@@ -66,7 +60,7 @@
<hr />
<div class="tw-text-light tw-mt-3">
<div class="tw-mt-3">
{{ "troubleLoggingIn" | i18n }}
<a routerLink="/login-initiated">{{ "viewAllLoginOptions" | i18n }}</a>
</div>

View File

@@ -14,10 +14,7 @@
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
<p bitTypography="body1">{{ "twoFactorWebAuthnWarning" | i18n }}</p>
<ul class="tw-mb-0">
<li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li>
</ul>
<p bitTypography="body1">{{ "twoFactorWebAuthnWarning1" | i18n }}</p>
</app-callout>
<img class="tw-float-right tw-ml-5 mfaType7" alt="FIDO2 WebAuthn logo" />
<ul class="bwi-ul">

View File

@@ -1045,10 +1045,12 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
this.estimatedTax = invoice.taxAmount;
})
.catch((error) => {
const translatedMessage = this.i18nService.t(error.message);
this.toastService.showToast({
title: "",
variant: "error",
message: this.i18nService.t(error.message),
message:
!translatedMessage || translatedMessage === "" ? error.message : translatedMessage,
});
});
}

View File

@@ -187,7 +187,7 @@ const routes: Routes = [
data: {
pageIcon: DevicesIcon,
pageTitle: {
key: "loginInitiated",
key: "logInRequestSent",
},
pageSubtitle: {
key: "aNotificationWasSentToYourDevice",

View File

@@ -1203,6 +1203,9 @@
"logInInitiated": {
"message": "Log in initiated"
},
"logInRequestSent": {
"message": "Request sent"
},
"submit": {
"message": "Submit"
},
@@ -1392,12 +1395,39 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the "
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Confirm access"
},
"denyAccess": {
"message": "Deny access"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"notificationSentDeviceComplete": {
"message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"versionNumber": {
"message": "Version $VERSION_NUMBER$",
"placeholders": {
@@ -2401,11 +2431,8 @@
"twoFactorU2fProblemReadingTryAgain": {
"message": "There was a problem reading the security key. Try again."
},
"twoFactorWebAuthnWarning": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:"
},
"twoFactorWebAuthnSupportWeb": {
"message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)."
"twoFactorWebAuthnWarning1": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used."
},
"twoFactorRecoveryYourCode": {
"message": "Your Bitwarden two-step login recovery code"

View File

@@ -11,6 +11,8 @@ import rxjs from "eslint-plugin-rxjs";
import angularRxjs from "eslint-plugin-rxjs-angular";
import storybook from "eslint-plugin-storybook";
import platformPlugins from "./libs/eslint/platform/index.mjs";
export default tseslint.config(
...storybook.configs["flat/recommended"],
{
@@ -28,6 +30,7 @@ export default tseslint.config(
plugins: {
rxjs: rxjs,
"rxjs-angular": angularRxjs,
"@bitwarden/platform": platformPlugins,
},
languageOptions: {
parserOptions: {
@@ -66,7 +69,7 @@ export default tseslint.config(
"@angular-eslint/no-outputs-metadata-property": 0,
"@angular-eslint/use-lifecycle-interface": "error",
"@angular-eslint/use-pipe-transform-interface": 0,
"@bitwarden/platform/required-using": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
"@typescript-eslint/no-floating-promises": "error",

View File

@@ -30,6 +30,7 @@ module.exports = {
"<rootDir>/libs/billing/jest.config.js",
"<rootDir>/libs/common/jest.config.js",
"<rootDir>/libs/components/jest.config.js",
"<rootDir>/libs/eslint/jest.config.js",
"<rootDir>/libs/tools/export/vault-export/vault-export-core/jest.config.js",
"<rootDir>/libs/tools/generator/core/jest.config.js",
"<rootDir>/libs/tools/generator/components/jest.config.js",

View File

@@ -64,11 +64,12 @@ export class LoginViaAuthRequestComponentV1
protected StateEnum = State;
protected state = State.StandardAuthRequest;
protected webVaultUrl: string;
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
private resendTimeout = 12000;
protected deviceManagementUrl: string;
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
@@ -95,6 +96,12 @@ export class LoginViaAuthRequestComponentV1
) {
super(environmentService, i18nService, platformUtilsService, toastService);
// Get the web vault URL from the environment service
environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => {
this.webVaultUrl = env.getWebVaultUrl();
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
});
// Gets signalR push notification
// Only fires on approval to prevent enumeration
this.authRequestService.authRequestPushNotification$

View File

@@ -1,5 +1,5 @@
<bit-dialog>
<span bitDialogTitle>{{ "areYouTryingtoLogin" | i18n }}</span>
<span bitDialogTitle>{{ "areYouTryingToAccessYourAccount" | i18n }}</span>
<ng-container bitDialogContent>
<ng-container *ngIf="loading">
<div class="tw-flex tw-items-center tw-justify-center" *ngIf="loading">
@@ -8,7 +8,7 @@
</ng-container>
<ng-container *ngIf="!loading">
<h4 class="tw-mb-3">{{ "logInAttemptBy" | i18n: email }}</h4>
<h4 class="tw-mb-3">{{ "accessAttemptBy" | i18n: email }}</h4>
<div>
<b>{{ "fingerprintPhraseHeader" | i18n }}</b>
<p class="tw-text-code">{{ fingerprintPhrase }}</p>
@@ -35,7 +35,7 @@
[bitAction]="approveLogin"
[disabled]="loading"
>
{{ "confirmLogIn" | i18n }}
{{ "confirmAccess" | i18n }}
</button>
<button
bitButton
@@ -44,7 +44,7 @@
[bitAction]="denyLogin"
[disabled]="loading"
>
{{ "denyLogIn" | i18n }}
{{ "denyAccess" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -85,7 +85,7 @@ export class LoginApprovalComponent implements OnInit, OnDestroy {
}
const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey);
this.email = await await firstValueFrom(
this.email = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
);
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(

View File

@@ -1,6 +1,20 @@
<div class="tw-text-center">
<ng-container *ngIf="flow === Flow.StandardAuthRequest">
<p>{{ "makeSureYourAccountIsUnlockedAndTheFingerprintEtc" | i18n }}</p>
<p *ngIf="clientType !== ClientType.Web">
{{ "notificationSentDevicePart1" | i18n }}
<a
bitLink
linkType="primary"
class="tw-cursor-pointer"
[href]="deviceManagementUrl"
target="_blank"
rel="noreferrer"
>{{ "notificationSentDeviceAnchor" | i18n }}</a
>. {{ "notificationSentDevicePart2" | i18n }}
</p>
<p *ngIf="clientType === ClientType.Web">
{{ "notificationSentDeviceComplete" | i18n }}
</p>
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
<code class="tw-text-code">{{ fingerprintPhrase }}</code>

View File

@@ -29,6 +29,7 @@ import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -71,6 +72,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
protected showResendNotification = false;
protected Flow = Flow;
protected flow = Flow.StandardAuthRequest;
protected webVaultUrl: string;
protected deviceManagementUrl: string;
constructor(
private accountService: AccountService,
@@ -81,6 +84,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
private authService: AuthService,
private cryptoFunctionService: CryptoFunctionService,
private deviceTrustService: DeviceTrustServiceAbstraction,
private environmentService: EnvironmentService,
private i18nService: I18nService,
private logService: LogService,
private loginEmailService: LoginEmailServiceAbstraction,
@@ -109,6 +113,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
this.logService.error("Failed to use approved auth request: " + e.message);
});
});
// Get the web vault URL from the environment service
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
this.webVaultUrl = env.getWebVaultUrl();
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
});
}
async ngOnInit(): Promise<void> {

View File

@@ -9,6 +9,7 @@ export enum FeatureFlag {
AccountDeprovisioning = "pm-10308-account-deprovisioning",
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
PM14505AdminConsoleIntegrationPage = "pm-14505-admin-console-integration-page",
LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
/* Autofill */
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
@@ -48,7 +49,6 @@ export enum FeatureFlag {
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
NewDeviceVerification = "new-device-verification",
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
limitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@@ -68,6 +68,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.AccountDeprovisioning]: FALSE,
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
[FeatureFlag.PM14505AdminConsoleIntegrationPage]: FALSE,
[FeatureFlag.LimitItemDeletion]: FALSE,
/* Autofill */
[FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE,
@@ -107,7 +108,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
[FeatureFlag.NewDeviceVerification]: FALSE,
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
[FeatureFlag.limitItemDeletion]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;

View File

@@ -3,6 +3,7 @@ import { Observable } from "rxjs";
import { BitwardenClient } from "@bitwarden/sdk-internal";
import { UserId } from "../../../types/guid";
import { Rc } from "../../misc/reference-counting/rc";
export abstract class SdkService {
/**
@@ -27,5 +28,5 @@ export abstract class SdkService {
*
* @param userId
*/
abstract userClient$(userId: UserId): Observable<BitwardenClient | undefined>;
abstract userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined>;
}

View File

@@ -0,0 +1,93 @@
// Temporary workaround for Symbol.dispose
// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released*
const disposeSymbol: unique symbol = Symbol("Symbol.dispose");
const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose");
(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"];
(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"];
// Import needs to be after the workaround
import { Rc } from "./rc";
export class FreeableTestValue {
isFreed = false;
free() {
this.isFreed = true;
}
}
describe("Rc", () => {
let value: FreeableTestValue;
let rc: Rc<FreeableTestValue>;
beforeEach(() => {
value = new FreeableTestValue();
rc = new Rc(value);
});
it("should increase refCount when taken", () => {
rc.take();
expect(rc["refCount"]).toBe(1);
});
it("should return value on take", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
expect(reference.value).toBe(value);
});
it("should decrease refCount when disposing reference", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
reference[Symbol.dispose]();
expect(rc["refCount"]).toBe(0);
});
it("should automatically decrease refCount when reference goes out of scope", () => {
{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using reference = rc.take();
}
expect(rc["refCount"]).toBe(0);
});
it("should not free value when refCount reaches 0 if not marked for disposal", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
reference[Symbol.dispose]();
expect(value.isFreed).toBe(false);
});
it("should free value when refCount reaches 0 and rc is marked for disposal", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
rc.markForDisposal();
reference[Symbol.dispose]();
expect(value.isFreed).toBe(true);
});
it("should free value when marked for disposal if refCount is 0", () => {
// eslint-disable-next-line @bitwarden/platform/required-using
const reference = rc.take();
reference[Symbol.dispose]();
rc.markForDisposal();
expect(value.isFreed).toBe(true);
});
it("should throw error when trying to take a disposed reference", () => {
rc.markForDisposal();
expect(() => rc.take()).toThrow();
});
});

View File

@@ -0,0 +1,76 @@
import { UsingRequired } from "../using-required";
export type Freeable = { free: () => void };
/**
* Reference counted disposable value.
* This class is used to manage the lifetime of a value that needs to be
* freed of at a specific time but might still be in-use when that happens.
*/
export class Rc<T extends Freeable> {
private markedForDisposal = false;
private refCount = 0;
private value: T;
constructor(value: T) {
this.value = value;
}
/**
* Use this function when you want to use the underlying object.
* This will guarantee that you have a reference to the object
* and that it won't be freed until your reference goes out of scope.
*
* This function must be used with the `using` keyword.
*
* @example
* ```typescript
* function someFunction(rc: Rc<SomeValue>) {
* using reference = rc.take();
* reference.value.doSomething();
* // reference is automatically disposed here
* }
* ```
*
* @returns The value.
*/
take(): Ref<T> {
if (this.markedForDisposal) {
throw new Error("Cannot take a reference to a value marked for disposal");
}
this.refCount++;
return new Ref(() => this.release(), this.value);
}
/**
* Mark this Rc for disposal. When the refCount reaches 0, the value
* will be freed.
*/
markForDisposal() {
this.markedForDisposal = true;
this.freeIfPossible();
}
private release() {
this.refCount--;
this.freeIfPossible();
}
private freeIfPossible() {
if (this.refCount === 0 && this.markedForDisposal) {
this.value.free();
}
}
}
export class Ref<T extends Freeable> implements UsingRequired {
constructor(
private readonly release: () => void,
readonly value: T,
) {}
[Symbol.dispose]() {
this.release();
}
}

View File

@@ -0,0 +1,11 @@
export type Disposable = { [Symbol.dispose]: () => void };
/**
* Types implementing this type must be used together with the `using` keyword
*
* @example using ref = rc.take();
*/
// We want to use `interface` here because it creates a separate type.
// Type aliasing would not expose `UsingRequired` to the linter.
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UsingRequired extends Disposable {}

View File

@@ -10,6 +10,7 @@ import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
import { Rc } from "../../misc/reference-counting/rc";
import { EncryptedString } from "../../models/domain/enc-string";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
@@ -75,15 +76,14 @@ describe("DefaultSdkService", () => {
});
it("creates an SDK client when called the first time", async () => {
const result = await firstValueFrom(service.userClient$(userId));
await firstValueFrom(service.userClient$(userId));
expect(result).toBe(mockClient);
expect(sdkClientFactory.createSdkClient).toHaveBeenCalled();
});
it("does not create an SDK client when called the second time with same userId", async () => {
const subject_1 = new BehaviorSubject(undefined);
const subject_2 = new BehaviorSubject(undefined);
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
// Use subjects to ensure the subscription is kept alive
service.userClient$(userId).subscribe(subject_1);
@@ -92,14 +92,14 @@ describe("DefaultSdkService", () => {
// Wait for the next tick to ensure all async operations are done
await new Promise(process.nextTick);
expect(subject_1.value).toBe(mockClient);
expect(subject_2.value).toBe(mockClient);
expect(subject_1.value.take().value).toBe(mockClient);
expect(subject_2.value.take().value).toBe(mockClient);
expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1);
});
it("destroys the SDK client when all subscriptions are closed", async () => {
const subject_1 = new BehaviorSubject(undefined);
const subject_2 = new BehaviorSubject(undefined);
const subject_1 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subject_2 = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
const subscription_1 = service.userClient$(userId).subscribe(subject_1);
const subscription_2 = service.userClient$(userId).subscribe(subject_2);
await new Promise(process.nextTick);
@@ -107,6 +107,7 @@ describe("DefaultSdkService", () => {
subscription_1.unsubscribe();
subscription_2.unsubscribe();
await new Promise(process.nextTick);
expect(mockClient.free).toHaveBeenCalledTimes(1);
});
@@ -114,7 +115,7 @@ describe("DefaultSdkService", () => {
const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey);
keyService.userKey$.calledWith(userId).mockReturnValue(userKey$);
const subject = new BehaviorSubject(undefined);
const subject = new BehaviorSubject<Rc<BitwardenClient> | undefined>(undefined);
service.userClient$(userId).subscribe(subject);
await new Promise(process.nextTick);

View File

@@ -30,10 +30,11 @@ import { PlatformUtilsService } from "../../abstractions/platform-utils.service"
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
import { SdkService } from "../../abstractions/sdk/sdk.service";
import { compareValues } from "../../misc/compare-values";
import { Rc } from "../../misc/reference-counting/rc";
import { EncryptedString } from "../../models/domain/enc-string";
export class DefaultSdkService implements SdkService {
private sdkClientCache = new Map<UserId, Observable<BitwardenClient>>();
private sdkClientCache = new Map<UserId, Observable<Rc<BitwardenClient>>>();
client$ = this.environmentService.environment$.pipe(
concatMap(async (env) => {
@@ -58,7 +59,7 @@ export class DefaultSdkService implements SdkService {
private userAgent: string = null,
) {}
userClient$(userId: UserId): Observable<BitwardenClient | undefined> {
userClient$(userId: UserId): Observable<Rc<BitwardenClient> | undefined> {
// TODO: Figure out what happens when the user logs out
if (this.sdkClientCache.has(userId)) {
return this.sdkClientCache.get(userId);
@@ -88,32 +89,31 @@ export class DefaultSdkService implements SdkService {
// switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value.
switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => {
// Create our own observable to be able to implement clean-up logic
return new Observable<BitwardenClient>((subscriber) => {
let client: BitwardenClient;
return new Observable<Rc<BitwardenClient>>((subscriber) => {
const createAndInitializeClient = async () => {
if (privateKey == null || userKey == null) {
return undefined;
}
const settings = this.toSettings(env);
client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info);
await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys);
return client;
};
let client: Rc<BitwardenClient>;
createAndInitializeClient()
.then((c) => {
client = c;
subscriber.next(c);
client = c === undefined ? undefined : new Rc(c);
subscriber.next(client);
})
.catch((e) => {
subscriber.error(e);
});
return () => client?.free();
return () => client?.markForDisposal();
});
}),
tap({

View File

@@ -6,14 +6,26 @@
bitTypography="body2"
class="tw-text-main tw-truncate tw-inline-flex tw-items-center tw-gap-1.5 tw-w-full"
>
<div class="tw-truncate">
<div
[ngClass]="{
'tw-truncate': truncate,
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
}"
>
<ng-content></ng-content>
</div>
<div class="tw-flex-grow">
<ng-content select="[slot=default-trailing]"></ng-content>
</div>
</div>
<div bitTypography="helper" class="tw-text-muted tw-w-full tw-truncate">
<div
bitTypography="helper"
class="tw-text-muted tw-w-full"
[ngClass]="{
'tw-truncate': truncate,
'tw-text-wrap tw-overflow-auto tw-break-words': !truncate,
}"
>
<ng-content select="[slot=secondary]"></ng-content>
</div>
</div>

View File

@@ -6,6 +6,7 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
signal,
ViewChild,
} from "@angular/core";
@@ -32,6 +33,13 @@ export class ItemContentComponent implements AfterContentChecked {
protected endSlotHasChildren = signal(false);
/**
* Determines whether text will truncate or wrap.
*
* Default behavior is truncation.
*/
@Input() truncate = true;
ngAfterContentChecked(): void {
this.endSlotHasChildren.set(this.endSlot?.nativeElement.childElementCount > 0);
}

View File

@@ -111,6 +111,30 @@ Actions are commonly icon buttons or badge buttons.
</bit-item>
```
## Text Overflow Behavior
The default behavior for long text is to truncate it. However, you have the option of changing it to
wrap instead if that is what the design calls for.
This can be changed by passing `[truncate]="false"` to the `bit-item-content`.
```html
<bit-item>
<bit-item-content [truncate]="false">
Long text goes here!
<ng-container slot="secondary">This could also be very long text</ng-container>
</bit-item-content>
</bit-item>
```
### Truncation (Default)
<Story of={stories.TextOverflowTruncate} />
### Wrap
<Story of={stories.TextOverflowWrap} />
## Item Groups
Groups of items can be associated by wrapping them in the `<bit-item-group>`.

View File

@@ -135,7 +135,7 @@ export const ContentTypes: Story = {
}),
};
export const TextOverflow: Story = {
export const TextOverflowTruncate: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
@@ -158,6 +158,29 @@ export const TextOverflow: Story = {
}),
};
export const TextOverflowWrap: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<bit-item>
<bit-item-content [truncate]="false">
<i slot="start" class="bwi bwi-globe tw-text-2xl tw-text-muted" aria-hidden="true"></i>
Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!
<ng-container slot="secondary">Worlddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd!</ng-container>
</bit-item-content>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitIconButton="bwi-clone" size="small"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v" size="small"></button>
</bit-item-action>
</ng-container>
</bit-item>
`,
}),
};
const multipleActionListTemplate = /*html*/ `
<bit-item-group aria-label="Multiple Action List">
<bit-item>

2
libs/eslint/empty.ts Normal file
View File

@@ -0,0 +1,2 @@
// This file is used to avoid TS errors. This package only uses `tsconfig.json` for dynamically generated test files but
// TS doesn't know that in the CI.

View File

@@ -0,0 +1,10 @@
const sharedConfig = require("../../libs/shared/jest.config.angular");
/** @type {import('jest').Config} */
module.exports = {
...sharedConfig,
testMatch: ["**/+(*.)+(spec).+(mjs)"],
displayName: "libs/eslint tests",
preset: "jest-preset-angular",
setupFilesAfterEnv: ["<rootDir>/test.setup.mjs"],
};

View File

@@ -0,0 +1,3 @@
import requiredUsing from "./required-using.mjs";
export default { rules: { "required-using": requiredUsing } };

View File

@@ -0,0 +1,83 @@
import { ESLintUtils } from "@typescript-eslint/utils";
export const errorMessage = "'using' keyword is required but not used";
export default {
meta: {
type: "problem",
docs: {
description: "Ensure objects implementing UsingRequired are used with the using keyword",
category: "Best Practices",
recommended: false,
},
schema: [],
},
create(context) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
// Function to check if a type implements the `UsingRequired` interface
function implementsUsingRequired(type) {
const symbol = type.getSymbol();
if (!symbol) {
return false;
}
const declarations = symbol.getDeclarations() || [];
for (const declaration of declarations) {
const heritageClauses = declaration.heritageClauses || [];
for (const clause of heritageClauses) {
if (
clause.types.some(
(typeExpression) =>
checker.typeToString(checker.getTypeAtLocation(typeExpression.expression)) ===
"UsingRequired",
)
) {
return true;
}
}
}
return false;
}
// Function to check if a function call returns a `UsingRequired`
function returnsUsingRequired(node) {
if (node.type === "CallExpression") {
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const returnType = checker.getTypeAtLocation(tsNode);
return implementsUsingRequired(returnType);
}
return false;
}
return {
VariableDeclarator(node) {
// Skip if `using` is already present
if (node.parent.type === "VariableDeclaration" && node.parent.kind === "using") {
return;
}
// Check if the initializer returns a `UsingRequired`
if (node.init && returnsUsingRequired(node.init)) {
context.report({
node,
message: errorMessage,
});
}
},
AssignmentExpression(node) {
// Check if the right-hand side returns a `UsingRequired`
if (returnsUsingRequired(node.right)) {
context.report({
node,
message: errorMessage,
});
}
},
};
},
};

View File

@@ -0,0 +1,98 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import rule, { errorMessage } from "./required-using.mjs";
const ruleTester = new RuleTester({
languageOptions: {
parserOptions: {
project: [__dirname + "/../tsconfig.spec.json"],
projectService: {
allowDefaultProject: ["*.ts*"],
},
tsconfigRootDir: __dirname + "/..",
},
},
});
const setup = `
interface UsingRequired {}
class Ref implements UsingRequired {}
const rc = {
take(): Ref {
return new Ref();
},
};
`;
ruleTester.run("required-using", rule.default, {
valid: [
{
name: "Direct declaration with `using`",
code: `
${setup}
using client = rc.take();
`,
},
{
name: "Function reference with `using`",
code: `
${setup}
const t = rc.take;
using client = t();
`,
},
],
invalid: [
{
name: "Direct declaration without `using`",
code: `
${setup}
const client = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Assignment without `using`",
code: `
${setup}
let client;
client = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Function reference without `using`",
code: `
${setup}
const t = rc.take;
const client = t();
`,
errors: [
{
message: errorMessage,
},
],
},
{
name: "Destructuring without `using`",
code: `
${setup}
const { value } = rc.take();
`,
errors: [
{
message: errorMessage,
},
],
},
],
});

View File

@@ -0,0 +1,8 @@
/* eslint-disable no-undef */
import { clearImmediate, setImmediate } from "node:timers";
Object.defineProperties(globalThis, {
clearImmediate: { value: clearImmediate },
setImmediate: { value: setImmediate },
});

View File

@@ -0,0 +1,5 @@
{
"extends": "../shared/tsconfig",
"compilerOptions": {},
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}

8881
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,8 @@
"@types/proper-lockfile": "4.1.4",
"@types/retry": "0.12.5",
"@types/zxcvbn": "4.4.5",
"@typescript-eslint/rule-tester": "8.22.0",
"@typescript-eslint/utils": "8.22.0",
"@webcomponents/custom-elements": "1.6.0",
"@yao-pkg/pkg": "5.16.1",
"angular-eslint": "18.4.3",