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:
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
@@ -211,6 +211,8 @@
|
||||
"@storybook/angular",
|
||||
"@storybook/manager-api",
|
||||
"@storybook/theming",
|
||||
"@typescript-eslint/utils",
|
||||
"@typescript-eslint/rule-tester",
|
||||
"@types/react",
|
||||
"autoprefixer",
|
||||
"bootstrap",
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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}
|
||||
/>;
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -437,7 +437,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "loginInitiated",
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
|
||||
1
apps/desktop/desktop_native/.gitignore
vendored
1
apps/desktop/desktop_native/.gitignore
vendored
@@ -5,3 +5,4 @@ index.node
|
||||
npm-debug.log*
|
||||
*.node
|
||||
dist
|
||||
windows_pluginauthenticator_bindings.rs
|
||||
|
||||
72
apps/desktop/desktop_native/Cargo.lock
generated
72
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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'
|
||||
```
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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!();
|
||||
}
|
||||
@@ -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"
|
||||
));
|
||||
@@ -224,7 +224,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "loginInitiated",
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ const routes: Routes = [
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "loginInitiated",
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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$
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
93
libs/common/src/platform/misc/reference-counting/rc.spec.ts
Normal file
93
libs/common/src/platform/misc/reference-counting/rc.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
76
libs/common/src/platform/misc/reference-counting/rc.ts
Normal file
76
libs/common/src/platform/misc/reference-counting/rc.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
11
libs/common/src/platform/misc/using-required.ts
Normal file
11
libs/common/src/platform/misc/using-required.ts
Normal 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 {}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
@@ -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
2
libs/eslint/empty.ts
Normal 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.
|
||||
10
libs/eslint/jest.config.js
Normal file
10
libs/eslint/jest.config.js
Normal 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"],
|
||||
};
|
||||
3
libs/eslint/platform/index.mjs
Normal file
3
libs/eslint/platform/index.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import requiredUsing from "./required-using.mjs";
|
||||
|
||||
export default { rules: { "required-using": requiredUsing } };
|
||||
83
libs/eslint/platform/required-using.mjs
Normal file
83
libs/eslint/platform/required-using.mjs
Normal 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,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
98
libs/eslint/platform/required-using.spec.mjs
Normal file
98
libs/eslint/platform/required-using.spec.mjs
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
8
libs/eslint/test.setup.mjs
Normal file
8
libs/eslint/test.setup.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
import { clearImmediate, setImmediate } from "node:timers";
|
||||
|
||||
Object.defineProperties(globalThis, {
|
||||
clearImmediate: { value: clearImmediate },
|
||||
setImmediate: { value: setImmediate },
|
||||
});
|
||||
5
libs/eslint/tsconfig.json
Normal file
5
libs/eslint/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../shared/tsconfig",
|
||||
"compilerOptions": {},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
libs/eslint/tsconfig.spec.json
Normal file
3
libs/eslint/tsconfig.spec.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
8881
package-lock.json
generated
8881
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user