mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 22:13:32 +00:00
Merge branch 'main' into billing/pm-29602/update-cart-summary
This commit is contained in:
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -221,6 +221,9 @@ apps/web/src/locales/en/messages.json
|
||||
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
|
||||
# Scanning tools
|
||||
.checkmarx/ @bitwarden/team-appsec
|
||||
|
||||
## Overrides
|
||||
# For the time being platform owns tsconfig and jest config
|
||||
# These overrides will be removed after Nx is implemented
|
||||
|
||||
6
.github/workflows/build-browser.yml
vendored
6
.github/workflows/build-browser.yml
vendored
@@ -152,7 +152,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -260,7 +260,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
4
.github/workflows/build-cli.yml
vendored
4
.github/workflows/build-cli.yml
vendored
@@ -130,7 +130,7 @@ jobs:
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -326,7 +326,7 @@ jobs:
|
||||
choco install nasm --no-progress
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
14
.github/workflows/build-desktop.yml
vendored
14
.github/workflows/build-desktop.yml
vendored
@@ -183,7 +183,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/free-disk-space@main
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -487,7 +487,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -755,7 +755,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -1000,7 +1000,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -1240,7 +1240,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
@@ -1515,7 +1515,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
if: steps.get-changed-files-for-chromatic.outputs.storyFiles == 'true'
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
2
.github/workflows/nx.yml
vendored
2
.github/workflows/nx.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
2
.github/workflows/publish-cli.yml
vendored
2
.github/workflows/publish-cli.yml
vendored
@@ -216,7 +216,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
|
||||
@@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@@ -4610,11 +4619,11 @@
|
||||
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||
},
|
||||
"regExAdvancedOptionWarning": {
|
||||
"regExAdvancedOptionWarning": {
|
||||
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||
"description": "Content for dialog which warns a user when selecting 'regular expression' matching strategy as a cipher match strategy"
|
||||
},
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
"startsWithAdvancedOptionWarning": {
|
||||
"message": "\"Starts with\" is an advanced option with increased risk of exposing credentials.",
|
||||
"description": "Content for dialog which warns a user when selecting 'starts with' matching strategy as a cipher match strategy"
|
||||
},
|
||||
@@ -4622,7 +4631,7 @@
|
||||
"message": "More about match detection",
|
||||
"description": "Link to match detection docs on warning dialog for advance match strategy"
|
||||
},
|
||||
"uriAdvancedOption":{
|
||||
"uriAdvancedOption": {
|
||||
"message": "Advanced options",
|
||||
"description": "Advanced option placeholder for uri option component"
|
||||
},
|
||||
@@ -4812,7 +4821,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyFieldCipherName": {
|
||||
"copyFieldCipherName": {
|
||||
"message": "Copy $FIELD$, $CIPHERNAME$",
|
||||
"description": "Title for a button that copies a field value to the clipboard.",
|
||||
"placeholders": {
|
||||
@@ -4844,7 +4853,7 @@
|
||||
"adminConsole": {
|
||||
"message": "Admin Console"
|
||||
},
|
||||
"admin" :{
|
||||
"admin": {
|
||||
"message": "Admin"
|
||||
},
|
||||
"automaticUserConfirmation": {
|
||||
@@ -4853,7 +4862,7 @@
|
||||
"automaticUserConfirmationHint": {
|
||||
"message": "Automatically confirm pending users while this device is unlocked"
|
||||
},
|
||||
"autoConfirmOnboardingCallout":{
|
||||
"autoConfirmOnboardingCallout": {
|
||||
"message": "Save time with automatic user confirmation"
|
||||
},
|
||||
"autoConfirmWarning": {
|
||||
@@ -5793,7 +5802,7 @@
|
||||
"hasItemsVaultNudgeTitle": {
|
||||
"message": "Welcome to your vault!"
|
||||
},
|
||||
"phishingPageTitleV2":{
|
||||
"phishingPageTitleV2": {
|
||||
"message": "Phishing attempt detected"
|
||||
},
|
||||
"phishingPageSummary": {
|
||||
@@ -5813,7 +5822,7 @@
|
||||
"message": ", an open-source list of known phishing sites used for stealing personal and sensitive information.",
|
||||
"description": "This is in multiple parts to allow for bold text in the middle of the sentence. A proper name precedes this."
|
||||
},
|
||||
"phishingPageLearnMore" : {
|
||||
"phishingPageLearnMore": {
|
||||
"message": "Learn more about phishing detection"
|
||||
},
|
||||
"protectedBy": {
|
||||
@@ -5981,7 +5990,7 @@
|
||||
"cardNumberLabel": {
|
||||
"message": "Card number"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector":{
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
@@ -5999,10 +6008,10 @@
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified":{
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified":{
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
@@ -6120,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { WebauthnUtils } from "../utils/webauthn-utils";
|
||||
|
||||
import { MessageTypes } from "./messaging/message";
|
||||
import { Messenger } from "./messaging/messenger";
|
||||
|
||||
(function (globalContext) {
|
||||
if (globalContext.document.currentScript) {
|
||||
if (globalContext.document.currentScript?.parentNode) {
|
||||
globalContext.document.currentScript.parentNode.removeChild(
|
||||
globalContext.document.currentScript,
|
||||
);
|
||||
@@ -86,7 +84,7 @@ import { Messenger } from "./messaging/messenger";
|
||||
*/
|
||||
async function createWebAuthnCredential(
|
||||
options?: CredentialCreationOptions,
|
||||
): Promise<Credential> {
|
||||
): Promise<Credential | null> {
|
||||
if (!isWebauthnCall(options)) {
|
||||
return await browserCredentials.create(options);
|
||||
}
|
||||
@@ -106,13 +104,18 @@ import { Messenger } from "./messaging/messenger";
|
||||
options?.signal,
|
||||
);
|
||||
|
||||
if (response.type !== MessageTypes.CredentialCreationResponse) {
|
||||
if (response.type !== MessageTypes.CredentialCreationResponse || !response.result) {
|
||||
throw new Error("Something went wrong.");
|
||||
}
|
||||
|
||||
return WebauthnUtils.mapCredentialRegistrationResult(response.result);
|
||||
} catch (error) {
|
||||
if (error && error.fallbackRequested && fallbackSupported) {
|
||||
if (
|
||||
fallbackSupported &&
|
||||
error instanceof Object &&
|
||||
"fallbackRequested" in error &&
|
||||
error.fallbackRequested
|
||||
) {
|
||||
await waitForFocus();
|
||||
return await browserCredentials.create(options);
|
||||
}
|
||||
@@ -127,7 +130,9 @@ import { Messenger } from "./messaging/messenger";
|
||||
* @param options Options for creating new credentials.
|
||||
* @returns Promise that resolves to the new credential object.
|
||||
*/
|
||||
async function getWebAuthnCredential(options?: CredentialRequestOptions): Promise<Credential> {
|
||||
async function getWebAuthnCredential(
|
||||
options?: CredentialRequestOptions,
|
||||
): Promise<Credential | null> {
|
||||
if (!isWebauthnCall(options)) {
|
||||
return await browserCredentials.get(options);
|
||||
}
|
||||
@@ -153,7 +158,7 @@ import { Messenger } from "./messaging/messenger";
|
||||
internalAbortController.signal,
|
||||
);
|
||||
internalAbortController.signal.removeEventListener("abort", abortListener);
|
||||
if (response.type !== MessageTypes.CredentialGetResponse) {
|
||||
if (response.type !== MessageTypes.CredentialGetResponse || !response.result) {
|
||||
throw new Error("Something went wrong.");
|
||||
}
|
||||
|
||||
@@ -176,7 +181,7 @@ import { Messenger } from "./messaging/messenger";
|
||||
abortSignal.removeEventListener("abort", abortListener);
|
||||
internalAbortControllers.forEach((controller) => controller.abort());
|
||||
|
||||
return response;
|
||||
return response ?? null;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -188,13 +193,18 @@ import { Messenger } from "./messaging/messenger";
|
||||
options?.signal,
|
||||
);
|
||||
|
||||
if (response.type !== MessageTypes.CredentialGetResponse) {
|
||||
if (response.type !== MessageTypes.CredentialGetResponse || !response.result) {
|
||||
throw new Error("Something went wrong.");
|
||||
}
|
||||
|
||||
return WebauthnUtils.mapCredentialAssertResult(response.result);
|
||||
} catch (error) {
|
||||
if (error && error.fallbackRequested && fallbackSupported) {
|
||||
if (
|
||||
fallbackSupported &&
|
||||
error instanceof Object &&
|
||||
"fallbackRequested" in error &&
|
||||
error.fallbackRequested
|
||||
) {
|
||||
await waitForFocus();
|
||||
return await browserCredentials.get(options);
|
||||
}
|
||||
@@ -203,8 +213,10 @@ import { Messenger } from "./messaging/messenger";
|
||||
}
|
||||
}
|
||||
|
||||
function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) {
|
||||
return options && "publicKey" in options;
|
||||
function isWebauthnCall(
|
||||
options?: CredentialCreationOptions | CredentialRequestOptions,
|
||||
): options is CredentialCreationOptions | CredentialRequestOptions {
|
||||
return options != null && "publicKey" in options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +229,7 @@ import { Messenger } from "./messaging/messenger";
|
||||
*/
|
||||
async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) {
|
||||
try {
|
||||
if (globalContext.top.document.hasFocus()) {
|
||||
if (globalContext.top?.document.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@@ -225,9 +237,14 @@ import { Messenger } from "./messaging/messenger";
|
||||
return await new Promise((resolve) => globalContext.setTimeout(resolve, fallbackWait));
|
||||
}
|
||||
|
||||
if (!globalContext.top) {
|
||||
return await new Promise((resolve) => globalContext.setTimeout(resolve, fallbackWait));
|
||||
}
|
||||
|
||||
const topWindow = globalContext.top;
|
||||
const focusPromise = new Promise<void>((resolve) => {
|
||||
focusListenerHandler = () => resolve();
|
||||
globalContext.top.addEventListener("focus", focusListenerHandler);
|
||||
topWindow.addEventListener("focus", focusListenerHandler);
|
||||
});
|
||||
|
||||
const timeoutPromise = new Promise<void>((_, reject) => {
|
||||
@@ -248,7 +265,7 @@ import { Messenger } from "./messaging/messenger";
|
||||
}
|
||||
|
||||
function clearWaitForFocus() {
|
||||
globalContext.top.removeEventListener("focus", focusListenerHandler);
|
||||
globalContext.top?.removeEventListener("focus", focusListenerHandler);
|
||||
if (waitForFocusTimeout) {
|
||||
globalContext.clearTimeout(waitForFocusTimeout);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Message, MessageTypes } from "./message";
|
||||
|
||||
const SENDER = "bitwarden-webauthn";
|
||||
@@ -25,7 +23,9 @@ type Handler = (
|
||||
* handling aborts and exceptions across separate execution contexts.
|
||||
*/
|
||||
export class Messenger {
|
||||
private messageEventListener: (event: MessageEvent<MessageWithMetadata>) => void | null = null;
|
||||
private messageEventListener:
|
||||
| ((event: MessageEvent<MessageWithMetadata>) => void | Promise<void>)
|
||||
| null = null;
|
||||
private onDestroy = new EventTarget();
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ButtonModule,
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
IconTileComponent,
|
||||
LinkModule,
|
||||
CalloutComponent,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
templateUrl: "phishing-warning.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
JslibModule,
|
||||
LinkModule,
|
||||
FormFieldModule,
|
||||
|
||||
@@ -15,7 +15,7 @@ export class IpcContentScriptManagerService {
|
||||
}
|
||||
|
||||
configService
|
||||
.getFeatureFlag$(FeatureFlag.IpcChannelFramework)
|
||||
.getFeatureFlag$(FeatureFlag.ContentScriptIpcChannelFramework)
|
||||
.pipe(
|
||||
mergeMap(async (enabled) => {
|
||||
if (!enabled) {
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
type="button"
|
||||
role="link"
|
||||
>
|
||||
<bit-icon
|
||||
[icon]="rla.isActive ? button.iconActive : button.icon"
|
||||
<bit-svg
|
||||
[content]="rla.isActive ? button.iconActive : button.icon"
|
||||
aria-hidden="true"
|
||||
class="tw-leading-3"
|
||||
></bit-icon>
|
||||
></bit-svg>
|
||||
<span class="tw-text-sm tw-truncate tw-max-w-full">
|
||||
{{ button.label | i18n }}
|
||||
</span>
|
||||
|
||||
@@ -3,15 +3,15 @@ import { Component, Input } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Icon } from "@bitwarden/assets/svg";
|
||||
import { BitSvg } from "@bitwarden/assets/svg";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { IconModule, LinkModule } from "@bitwarden/components";
|
||||
import { SvgModule, LinkModule } from "@bitwarden/components";
|
||||
|
||||
export type NavButton = {
|
||||
label: string;
|
||||
page: string;
|
||||
icon: Icon;
|
||||
iconActive: Icon;
|
||||
icon: BitSvg;
|
||||
iconActive: BitSvg;
|
||||
showBerry?: boolean;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export type NavButton = {
|
||||
@Component({
|
||||
selector: "popup-tab-navigation",
|
||||
templateUrl: "popup-tab-navigation.component.html",
|
||||
imports: [CommonModule, LinkModule, RouterModule, JslibModule, IconModule],
|
||||
imports: [CommonModule, LinkModule, RouterModule, JslibModule, SvgModule],
|
||||
host: {
|
||||
class: "tw-block tw-size-full tw-flex tw-flex-col",
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[pageTitle]="''"
|
||||
>
|
||||
<div class="tw-w-32">
|
||||
<bit-icon *ngIf="showLogo" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
|
||||
<bit-svg *ngIf="showLogo" [content]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-svg>
|
||||
</div>
|
||||
|
||||
<ng-container slot="end">
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
|
||||
import { Subject, filter, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { BitwardenLogo, Icon } from "@bitwarden/assets/svg";
|
||||
import { BitwardenLogo, BitSvg } from "@bitwarden/assets/svg";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
IconModule,
|
||||
SvgModule,
|
||||
Translation,
|
||||
AnonLayoutComponent,
|
||||
AnonLayoutWrapperData,
|
||||
@@ -38,7 +38,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
|
||||
CommonModule,
|
||||
CurrentAccountComponent,
|
||||
I18nPipe,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
PopOutComponent,
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
@@ -54,7 +54,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected pageTitle: string;
|
||||
protected pageSubtitle: string;
|
||||
protected pageIcon: Icon;
|
||||
protected pageIcon: BitSvg;
|
||||
protected showReadonlyHostname: boolean;
|
||||
protected maxWidth: "md" | "3xl";
|
||||
protected hasLoggedInAccount: boolean = false;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
<bit-svg [content]="sendCreatedIcon"></bit-svg>
|
||||
</div>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/defau
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SendType } from "@bitwarden/common/tools/send/types/send-type";
|
||||
import { ButtonModule, I18nMockService, IconModule, ToastService } from "@bitwarden/components";
|
||||
import { ButtonModule, I18nMockService, SvgModule, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
||||
@@ -76,7 +76,7 @@ describe("SendCreatedComponent", () => {
|
||||
RouterTestingModule,
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
PopOutComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupPageComponent,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { ButtonModule, IconModule, ToastService } from "@bitwarden/components";
|
||||
import { ButtonModule, SvgModule, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
||||
@@ -34,7 +34,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
||||
PopupPageComponent,
|
||||
RouterModule,
|
||||
PopupFooterComponent,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
],
|
||||
})
|
||||
export class SendCreatedComponent {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<vault-carousel-slide [label]="'securityPrioritized' | i18n" [disablePadding]="true">
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-justify-around">
|
||||
<div class="tw-size-32 tw-content-center tw-my-4">
|
||||
<bit-icon [icon]="itemTypes"></bit-icon>
|
||||
<bit-svg [content]="itemTypes"></bit-svg>
|
||||
</div>
|
||||
<h2 bitTypography="h2" class="tw-text-center">{{ "securityPrioritized" | i18n }}</h2>
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "securityPrioritizedBody" | i18n }}</p>
|
||||
@@ -11,7 +11,7 @@
|
||||
<vault-carousel-slide [label]="'quickLogin' | i18n" [disablePadding]="true">
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-justify-around">
|
||||
<div class="tw-size-32 tw-content-center tw-my-4">
|
||||
<bit-icon [icon]="loginCards"></bit-icon>
|
||||
<bit-svg [content]="loginCards"></bit-svg>
|
||||
</div>
|
||||
<h2 bitTypography="h2" class="tw-text-center">{{ "quickLogin" | i18n }}</h2>
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "quickLoginBody" | i18n }}</p>
|
||||
@@ -20,7 +20,7 @@
|
||||
<vault-carousel-slide [label]="'secureUser' | i18n" [disablePadding]="true">
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-justify-around">
|
||||
<div class="tw-size-32 tw-content-center tw-my-4">
|
||||
<bit-icon [icon]="noCredentials"></bit-icon>
|
||||
<bit-svg [content]="noCredentials"></bit-svg>
|
||||
</div>
|
||||
<h2 bitTypography="h2" class="tw-text-center">{{ "secureUser" | i18n }}</h2>
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "secureUserBody" | i18n }}</p>
|
||||
@@ -29,7 +29,7 @@
|
||||
<vault-carousel-slide [label]="'secureDevices' | i18n" [disablePadding]="true">
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-justify-around">
|
||||
<div class="tw-size-32 tw-content-center tw-my-4">
|
||||
<bit-icon [icon]="secureDevices"></bit-icon>
|
||||
<bit-svg [content]="secureDevices"></bit-svg>
|
||||
</div>
|
||||
<h2 bitTypography="h2" class="tw-text-center">{{ "secureDevices" | i18n }}</h2>
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "secureDevicesBody" | i18n }}</p>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Router } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ItemTypes, LoginCards, NoCredentialsIcon, DevicesIcon } from "@bitwarden/assets/svg";
|
||||
import { ButtonModule, DialogModule, IconModule, TypographyModule } from "@bitwarden/components";
|
||||
import { ButtonModule, DialogModule, SvgModule, TypographyModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { VaultCarouselModule } from "@bitwarden/vault";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { IntroCarouselService } from "../../../services/intro-carousel.service";
|
||||
imports: [
|
||||
VaultCarouselModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
DialogModule,
|
||||
TypographyModule,
|
||||
JslibModule,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<bit-section-header class="tw-app-region-drag tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
<bit-svg [content]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-svg>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">
|
||||
{{ "savePasskeyQuestion" | i18n }}
|
||||
@@ -28,7 +28,7 @@
|
||||
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col">
|
||||
<div *ngIf="(ciphers$ | async)?.length === 0; else hasCiphers">
|
||||
<div class="tw-flex tw-items-center tw-flex-col tw-p-12 tw-gap-4">
|
||||
<bit-icon [icon]="Icons.NoResults" class="tw-text-main"></bit-icon>
|
||||
<bit-svg [content]="Icons.NoResults" class="tw-text-main"></bit-svg>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
{{ "noMatchingLoginsForSite" | i18n }}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<bit-section-header class="tw-app-region-drag tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
<bit-svg [content]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-svg>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">
|
||||
{{ "savePasskeyQuestion" | i18n }}
|
||||
@@ -30,7 +30,7 @@
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-start tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-flex-col tw-p-12 tw-gap-4">
|
||||
<bit-icon [icon]="Icons.NoResults" class="tw-text-main"></bit-icon>
|
||||
<bit-svg [content]="Icons.NoResults" class="tw-text-main"></bit-svg>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<b>{{ "passkeyAlreadyExists" | i18n }}</b>
|
||||
{{ "applicationDoesNotSupportDuplicates" | i18n }}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<bit-section-header class="tw-app-region-drag tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
<bit-svg [content]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-svg>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">{{ "passkeyLogin" | i18n }}</h2>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
|
||||
@@ -11,61 +11,66 @@
|
||||
>
|
||||
{{ submitButtonText() }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="edit()"
|
||||
appA11yTitle="{{ 'edit' | i18n }}"
|
||||
*ngIf="!cipher.isDeleted && action === 'view'"
|
||||
>
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="action === 'edit' || action === 'clone' || action === 'add'"
|
||||
type="button"
|
||||
(click)="cancel()"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted && cipher.permissions.restore"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
@if (!cipher.isDeleted && action === "view") {
|
||||
<button type="button" class="primary" (click)="edit()" appA11yTitle="{{ 'edit' | i18n }}">
|
||||
<i class="bwi bwi-pencil bwi-fw bwi-lg" aria-hidden="true" style="pointer-events: none">
|
||||
</i>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (action === "edit" || action === "clone" || action === "add") {
|
||||
<button type="button" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (cipher.isDeleted && cipher.permissions.restore) {
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true" style="pointer-events: none"></i>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showCloneOption) {
|
||||
<button type="button" class="primary" (click)="clone()" appA11yTitle="{{ 'clone' | i18n }}">
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true" style="pointer-events: none"></i>
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
<div class="right" *ngIf="hasFooterAction">
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="showArchiveButton"
|
||||
(click)="archive()"
|
||||
appA11yTitle="{{ 'archiveVerb' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-archive bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="showUnarchiveButton"
|
||||
(click)="unarchive()"
|
||||
appA11yTitle="{{ 'unArchive' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-unarchive bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="danger"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@if (hasFooterAction) {
|
||||
<div class="right">
|
||||
@if (showArchiveButton) {
|
||||
<button type="button" (click)="archive()" appA11yTitle="{{ 'archiveVerb' | i18n }}">
|
||||
<i
|
||||
class="bwi bwi-archive bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
style="pointer-events: none"
|
||||
></i>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (showUnarchiveButton) {
|
||||
<button type="button" (click)="unarchive()" appA11yTitle="{{ 'unArchive' | i18n }}">
|
||||
<i
|
||||
class="bwi bwi-unarchive bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
style="pointer-events: none"
|
||||
></i>
|
||||
</button>
|
||||
}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
class="danger"
|
||||
appA11yTitle="{{ (cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true" style="pointer-events: none"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { TestBed } from "@angular/core/testing";
|
||||
import { ReplaySubject } from "rxjs";
|
||||
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
@@ -46,23 +45,16 @@ describe("PeopleTableDataSource", () => {
|
||||
isCloud: () => false,
|
||||
} as Environment);
|
||||
|
||||
const mockConfigService = {
|
||||
getFeatureFlag$: jest.fn(() => featureFlagSubject.asObservable()),
|
||||
} as any;
|
||||
|
||||
const mockEnvironmentService = {
|
||||
environment$: environmentSubject.asObservable(),
|
||||
} as any;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: EnvironmentService, useValue: mockEnvironmentService },
|
||||
],
|
||||
providers: [{ provide: EnvironmentService, useValue: mockEnvironmentService }],
|
||||
});
|
||||
|
||||
dataSource = TestBed.runInInjectionContext(
|
||||
() => new TestPeopleTableDataSource(mockConfigService, mockEnvironmentService),
|
||||
() => new TestPeopleTableDataSource(mockEnvironmentService),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { computed, Signal } from "@angular/core";
|
||||
import { Signal } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { Observable, Subject, map } from "rxjs";
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
ProviderUserStatusType,
|
||||
} from "@bitwarden/common/admin-console/enums";
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
|
||||
@@ -27,8 +25,7 @@ export type ProviderUser = ProviderUserUserDetailsResponse;
|
||||
export const MaxCheckedCount = 500;
|
||||
|
||||
/**
|
||||
* Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud
|
||||
* feature flag is enabled on cloud environments.
|
||||
* Maximum for bulk reinvite limit in cloud environments.
|
||||
*/
|
||||
export const CloudBulkReinviteLimit = 8000;
|
||||
|
||||
@@ -78,18 +75,15 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
|
||||
confirmedUserCount: number;
|
||||
revokedUserCount: number;
|
||||
|
||||
/** True when increased bulk limit feature is enabled (feature flag + cloud environment) */
|
||||
/** True when increased bulk limit feature is enabled (cloud environment) */
|
||||
readonly isIncreasedBulkLimitEnabled: Signal<boolean>;
|
||||
|
||||
constructor(configService: ConfigService, environmentService: EnvironmentService) {
|
||||
constructor(environmentService: EnvironmentService) {
|
||||
super();
|
||||
|
||||
const featureFlagEnabled = toSignal(
|
||||
configService.getFeatureFlag$(FeatureFlag.IncreaseBulkReinviteLimitForCloud),
|
||||
this.isIncreasedBulkLimitEnabled = toSignal(
|
||||
environmentService.environment$.pipe(map((env) => env.isCloud())),
|
||||
);
|
||||
const isCloud = toSignal(environmentService.environment$.pipe(map((env) => env.isCloud())));
|
||||
|
||||
this.isIncreasedBulkLimitEnabled = computed(() => featureFlagEnabled() && isCloud());
|
||||
}
|
||||
|
||||
override set data(data: T[]) {
|
||||
@@ -224,12 +218,9 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets checked users with optional limiting based on the IncreaseBulkReinviteLimitForCloud feature flag.
|
||||
* Returns checked users in visible order, optionally limited to the specified count.
|
||||
*
|
||||
* When the feature flag is enabled: Returns checked users in visible order, limited to the specified count.
|
||||
* When the feature flag is disabled: Returns all checked users without applying any limit.
|
||||
*
|
||||
* @param limit The maximum number of users to return (only applied when feature flag is enabled)
|
||||
* @param limit The maximum number of users to return
|
||||
* @returns The checked users array
|
||||
*/
|
||||
getCheckedUsersWithLimit(limit: number): T[] {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { BannerModule, IconModule } from "@bitwarden/components";
|
||||
import { BannerModule, SvgModule } from "@bitwarden/components";
|
||||
import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
import { NonIndividualSubscriber } from "@bitwarden/web-vault/app/billing/types";
|
||||
@@ -47,7 +47,7 @@ import { WebLayoutModule } from "../../../layouts/web-layout.module";
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
WebLayoutModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
OrgSwitcherComponent,
|
||||
BannerModule,
|
||||
TaxIdWarningComponent,
|
||||
|
||||
@@ -33,7 +33,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
|
||||
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -124,7 +123,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private organizationMetadataService: OrganizationMetadataServiceAbstraction,
|
||||
private memberExportService: MemberExportService,
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {
|
||||
super(
|
||||
@@ -139,7 +137,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
toastService,
|
||||
);
|
||||
|
||||
this.dataSource = new MembersTableDataSource(this.configService, this.environmentService);
|
||||
this.dataSource = new MembersTableDataSource(this.environmentService);
|
||||
|
||||
const organization$ = this.route.params.pipe(
|
||||
concatMap((params) =>
|
||||
|
||||
@@ -33,7 +33,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
|
||||
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -100,7 +99,6 @@ export class vNextMembersComponent {
|
||||
private policyService = inject(PolicyService);
|
||||
private policyApiService = inject(PolicyApiServiceAbstraction);
|
||||
private organizationMetadataService = inject(OrganizationMetadataServiceAbstraction);
|
||||
private configService = inject(ConfigService);
|
||||
private environmentService = inject(EnvironmentService);
|
||||
private memberExportService = inject(MemberExportService);
|
||||
|
||||
@@ -114,7 +112,7 @@ export class vNextMembersComponent {
|
||||
protected statusToggle = new BehaviorSubject<OrganizationUserStatusType | undefined>(undefined);
|
||||
|
||||
protected readonly dataSource: Signal<MembersTableDataSource> = signal(
|
||||
new MembersTableDataSource(this.configService, this.environmentService),
|
||||
new MembersTableDataSource(this.environmentService),
|
||||
);
|
||||
protected readonly organization: Signal<Organization | undefined>;
|
||||
protected readonly firstLoaded: WritableSignal<boolean> = signal(false);
|
||||
@@ -389,7 +387,7 @@ export class vNextMembersComponent {
|
||||
// Capture the original count BEFORE enforcing the limit
|
||||
const originalInvitedCount = allInvitedUsers.length;
|
||||
|
||||
// When feature flag is enabled, limit invited users and uncheck the excess
|
||||
// In cloud environments, limit invited users and uncheck the excess
|
||||
let filteredUsers: OrganizationUserView[];
|
||||
if (this.dataSource().isIncreasedBulkLimitEnabled()) {
|
||||
filteredUsers = this.dataSource().limitAndUncheckExcess(
|
||||
@@ -418,7 +416,7 @@ export class vNextMembersComponent {
|
||||
this.validationService.showError(result.failed);
|
||||
}
|
||||
|
||||
// When feature flag is enabled, show toast instead of dialog
|
||||
// In cloud environments, show toast instead of dialog
|
||||
if (this.dataSource().isIncreasedBulkLimitEnabled()) {
|
||||
const selectedCount = originalInvitedCount;
|
||||
const invitedCount = filteredUsers.length;
|
||||
@@ -441,7 +439,7 @@ export class vNextMembersComponent {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Feature flag disabled - show legacy dialog
|
||||
// In self-hosted environments, show legacy dialog
|
||||
await this.memberDialogManager.openBulkStatusDialog(
|
||||
users,
|
||||
filteredUsers,
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
@@ -32,7 +31,6 @@ describe("MemberActionsService", () => {
|
||||
let service: MemberActionsService;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let organizationUserService: MockProxy<OrganizationUserService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let organizationMetadataService: MockProxy<OrganizationMetadataServiceAbstraction>;
|
||||
|
||||
const organizationId = newGuid() as OrganizationId;
|
||||
@@ -44,7 +42,6 @@ describe("MemberActionsService", () => {
|
||||
beforeEach(() => {
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
organizationUserService = mock<OrganizationUserService>();
|
||||
configService = mock<ConfigService>();
|
||||
organizationMetadataService = mock<OrganizationMetadataServiceAbstraction>();
|
||||
|
||||
mockOrganization = {
|
||||
@@ -68,7 +65,6 @@ describe("MemberActionsService", () => {
|
||||
MemberActionsService,
|
||||
{ provide: OrganizationUserApiService, useValue: organizationUserApiService },
|
||||
{ provide: OrganizationUserService, useValue: organizationUserService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{
|
||||
provide: OrganizationMetadataServiceAbstraction,
|
||||
useValue: organizationMetadataService,
|
||||
@@ -279,308 +275,247 @@ describe("MemberActionsService", () => {
|
||||
});
|
||||
|
||||
describe("bulkReinvite", () => {
|
||||
const userIds = [newGuid() as UserId, newGuid() as UserId, newGuid() as UserId];
|
||||
it("should process users in a single batch when count equals REQUESTS_PER_BATCH", async () => {
|
||||
const userIdsBatch = Array.from({ length: REQUESTS_PER_BATCH }, () => newGuid() as UserId);
|
||||
const mockResponse = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
describe("when feature flag is false", () => {
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
});
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse);
|
||||
|
||||
it("should successfully reinvite multiple users", async () => {
|
||||
const mockResponse = new ListResponse(
|
||||
{
|
||||
data: userIds.map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIds);
|
||||
|
||||
expect(result.failed).toEqual([]);
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful).toEqual(mockResponse);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith(
|
||||
organizationId,
|
||||
userIds,
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle bulk reinvite errors", async () => {
|
||||
const errorMessage = "Bulk reinvite failed";
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockRejectedValue(
|
||||
new Error(errorMessage),
|
||||
);
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIds);
|
||||
|
||||
expect(result.successful).toBeUndefined();
|
||||
expect(result.failed).toHaveLength(3);
|
||||
expect(result.failed[0]).toEqual({ id: userIds[0], error: errorMessage });
|
||||
});
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(1);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith(
|
||||
organizationId,
|
||||
userIdsBatch,
|
||||
);
|
||||
});
|
||||
|
||||
describe("when feature flag is true (batching behavior)", () => {
|
||||
beforeEach(() => {
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
});
|
||||
it("should process users in a single batch when count equals REQUESTS_PER_BATCH", async () => {
|
||||
const userIdsBatch = Array.from({ length: REQUESTS_PER_BATCH }, () => newGuid() as UserId);
|
||||
const mockResponse = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
it("should process users in multiple batches when count exceeds REQUESTS_PER_BATCH", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 100;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockResolvedValue(mockResponse);
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledWith(
|
||||
organizationId,
|
||||
userIdsBatch,
|
||||
);
|
||||
});
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
|
||||
it("should process users in multiple batches when count exceeds REQUESTS_PER_BATCH", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 100;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(totalUsers);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(2);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
organizationId,
|
||||
userIdsBatch.slice(0, REQUESTS_PER_BATCH),
|
||||
);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
organizationId,
|
||||
userIdsBatch.slice(REQUESTS_PER_BATCH),
|
||||
);
|
||||
});
|
||||
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
it("should aggregate results across multiple successful batches", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 50;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(totalUsers);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
organizationId,
|
||||
userIdsBatch.slice(0, REQUESTS_PER_BATCH),
|
||||
);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
organizationId,
|
||||
userIdsBatch.slice(REQUESTS_PER_BATCH),
|
||||
);
|
||||
});
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
|
||||
it("should aggregate results across multiple successful batches", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 50;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(totalUsers);
|
||||
expect(result.successful?.response.slice(0, REQUESTS_PER_BATCH)).toEqual(mockResponse1.data);
|
||||
expect(result.successful?.response.slice(REQUESTS_PER_BATCH)).toEqual(mockResponse2.data);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
});
|
||||
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
it("should handle mixed individual errors across multiple batches", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 4;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id, index) => ({
|
||||
id,
|
||||
error: index % 10 === 0 ? "Rate limit exceeded" : null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: [
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH], error: null },
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH + 1], error: "Invalid email" },
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH + 2], error: null },
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH + 3], error: "User suspended" },
|
||||
],
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(totalUsers);
|
||||
expect(result.successful?.response.slice(0, REQUESTS_PER_BATCH)).toEqual(
|
||||
mockResponse1.data,
|
||||
);
|
||||
expect(result.successful?.response.slice(REQUESTS_PER_BATCH)).toEqual(mockResponse2.data);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
});
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
|
||||
it("should handle mixed individual errors across multiple batches", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 4;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id, index) => ({
|
||||
id,
|
||||
error: index % 10 === 0 ? "Rate limit exceeded" : null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
// Count expected failures: every 10th index (0, 10, 20, ..., 490) in first batch + 2 explicit in second batch
|
||||
// Indices 0 to REQUESTS_PER_BATCH-1 where index % 10 === 0: that's floor((BATCH_SIZE-1)/10) + 1 values
|
||||
const expectedFailuresInBatch1 = Math.floor((REQUESTS_PER_BATCH - 1) / 10) + 1;
|
||||
const expectedFailuresInBatch2 = 2;
|
||||
const expectedTotalFailures = expectedFailuresInBatch1 + expectedFailuresInBatch2;
|
||||
const expectedSuccesses = totalUsers - expectedTotalFailures;
|
||||
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: [
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH], error: null },
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH + 1], error: "Invalid email" },
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH + 2], error: null },
|
||||
{ id: userIdsBatch[REQUESTS_PER_BATCH + 3], error: "User suspended" },
|
||||
],
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(expectedSuccesses);
|
||||
expect(result.failed).toHaveLength(expectedTotalFailures);
|
||||
expect(result.failed.some((f) => f.error === "Rate limit exceeded")).toBe(true);
|
||||
expect(result.failed.some((f) => f.error === "Invalid email")).toBe(true);
|
||||
expect(result.failed.some((f) => f.error === "User suspended")).toBe(true);
|
||||
});
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
it("should aggregate all failures when all batches fail", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 100;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const errorMessage = "All batches failed";
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockRejectedValue(
|
||||
new Error(errorMessage),
|
||||
);
|
||||
|
||||
// Count expected failures: every 10th index (0, 10, 20, ..., 490) in first batch + 2 explicit in second batch
|
||||
// Indices 0 to REQUESTS_PER_BATCH-1 where index % 10 === 0: that's floor((BATCH_SIZE-1)/10) + 1 values
|
||||
const expectedFailuresInBatch1 = Math.floor((REQUESTS_PER_BATCH - 1) / 10) + 1;
|
||||
const expectedFailuresInBatch2 = 2;
|
||||
const expectedTotalFailures = expectedFailuresInBatch1 + expectedFailuresInBatch2;
|
||||
const expectedSuccesses = totalUsers - expectedTotalFailures;
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(expectedSuccesses);
|
||||
expect(result.failed).toHaveLength(expectedTotalFailures);
|
||||
expect(result.failed.some((f) => f.error === "Rate limit exceeded")).toBe(true);
|
||||
expect(result.failed.some((f) => f.error === "Invalid email")).toBe(true);
|
||||
expect(result.failed.some((f) => f.error === "User suspended")).toBe(true);
|
||||
});
|
||||
expect(result.successful).toBeUndefined();
|
||||
expect(result.failed).toHaveLength(totalUsers);
|
||||
expect(result.failed.every((f) => f.error === errorMessage)).toBe(true);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should aggregate all failures when all batches fail", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 100;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const errorMessage = "All batches failed";
|
||||
it("should handle empty data in batch response", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 50;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockRejectedValue(
|
||||
new Error(errorMessage),
|
||||
);
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: [],
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
|
||||
expect(result.successful).toBeUndefined();
|
||||
expect(result.failed).toHaveLength(totalUsers);
|
||||
expect(result.failed.every((f) => f.error === errorMessage)).toBe(true);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
});
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
|
||||
it("should handle empty data in batch response", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH + 50;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
const mockResponse1 = new ListResponse(
|
||||
{
|
||||
data: userIdsBatch.slice(0, REQUESTS_PER_BATCH).map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
});
|
||||
|
||||
const mockResponse2 = new ListResponse(
|
||||
{
|
||||
data: [],
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
it("should process batches sequentially in order", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH * 2;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const callOrder: number[] = [];
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite
|
||||
.mockResolvedValueOnce(mockResponse1)
|
||||
.mockResolvedValueOnce(mockResponse2);
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockImplementation(
|
||||
async (orgId, ids) => {
|
||||
const batchIndex = ids.includes(userIdsBatch[0]) ? 1 : 2;
|
||||
callOrder.push(batchIndex);
|
||||
|
||||
const result = await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
return new ListResponse(
|
||||
{
|
||||
data: ids.map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.successful).toBeDefined();
|
||||
expect(result.successful?.response).toHaveLength(REQUESTS_PER_BATCH);
|
||||
expect(result.failed).toHaveLength(0);
|
||||
});
|
||||
await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
it("should process batches sequentially in order", async () => {
|
||||
const totalUsers = REQUESTS_PER_BATCH * 2;
|
||||
const userIdsBatch = Array.from({ length: totalUsers }, () => newGuid() as UserId);
|
||||
const callOrder: number[] = [];
|
||||
|
||||
organizationUserApiService.postManyOrganizationUserReinvite.mockImplementation(
|
||||
async (orgId, ids) => {
|
||||
const batchIndex = ids.includes(userIdsBatch[0]) ? 1 : 2;
|
||||
callOrder.push(batchIndex);
|
||||
|
||||
return new ListResponse(
|
||||
{
|
||||
data: ids.map((id) => ({
|
||||
id,
|
||||
error: null,
|
||||
})),
|
||||
continuationToken: null,
|
||||
},
|
||||
OrganizationUserBulkResponse,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await service.bulkReinvite(mockOrganization, userIdsBatch);
|
||||
|
||||
expect(callOrder).toEqual([1, 2]);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(
|
||||
2,
|
||||
);
|
||||
});
|
||||
expect(callOrder).toEqual([1, 2]);
|
||||
expect(organizationUserApiService.postManyOrganizationUserReinvite).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,9 +16,7 @@ import {
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
@@ -45,7 +43,6 @@ export interface BulkActionResult {
|
||||
export class MemberActionsService {
|
||||
private organizationUserApiService = inject(OrganizationUserApiService);
|
||||
private organizationUserService = inject(OrganizationUserService);
|
||||
private configService = inject(ConfigService);
|
||||
private organizationMetadataService = inject(OrganizationMetadataServiceAbstraction);
|
||||
private apiService = inject(ApiService);
|
||||
private dialogService = inject(DialogService);
|
||||
@@ -175,18 +172,9 @@ export class MemberActionsService {
|
||||
async bulkReinvite(organization: Organization, userIds: UserId[]): Promise<BulkActionResult> {
|
||||
this.startProcessing();
|
||||
try {
|
||||
const increaseBulkReinviteLimitForCloud = await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.IncreaseBulkReinviteLimitForCloud),
|
||||
return this.processBatchedOperation(userIds, REQUESTS_PER_BATCH, (batch) =>
|
||||
this.organizationUserApiService.postManyOrganizationUserReinvite(organization.id, batch),
|
||||
);
|
||||
if (increaseBulkReinviteLimitForCloud) {
|
||||
return await this.vNextBulkReinvite(organization, userIds);
|
||||
} else {
|
||||
const result = await this.organizationUserApiService.postManyOrganizationUserReinvite(
|
||||
organization.id,
|
||||
userIds,
|
||||
);
|
||||
return { successful: result, failed: [] };
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
failed: userIds.map((id) => ({ id, error: (error as Error).message ?? String(error) })),
|
||||
@@ -196,15 +184,6 @@ export class MemberActionsService {
|
||||
}
|
||||
}
|
||||
|
||||
async vNextBulkReinvite(
|
||||
organization: Organization,
|
||||
userIds: UserId[],
|
||||
): Promise<BulkActionResult> {
|
||||
return this.processBatchedOperation(userIds, REQUESTS_PER_BATCH, (batch) =>
|
||||
this.organizationUserApiService.postManyOrganizationUserReinvite(organization.id, batch),
|
||||
);
|
||||
}
|
||||
|
||||
allowResetPassword(
|
||||
orgUser: OrganizationUserView,
|
||||
organization: Organization,
|
||||
|
||||
@@ -5,3 +5,4 @@ export { POLICY_EDIT_REGISTER } from "./policy-register-token";
|
||||
export { AutoConfirmPolicy } from "./policy-edit-definitions";
|
||||
export { PolicyEditDialogResult } from "./policy-edit-dialog.component";
|
||||
export * from "./policy-edit-dialogs";
|
||||
export { PolicyOrderPipe } from "./pipes/policy-order.pipe";
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { BasePolicyEditDefinition } from "../base-policy-edit.component";
|
||||
|
||||
/**
|
||||
* Order mapping for policies. Policies are ordered according to this mapping.
|
||||
* Policies not in this mapping will appear at the end, maintaining their relative order.
|
||||
*/
|
||||
const POLICY_ORDER_MAP = new Map<string, number>([
|
||||
["singleOrg", 1],
|
||||
["organizationDataOwnership", 2],
|
||||
["centralizeDataOwnership", 2],
|
||||
["masterPassPolicyTitle", 3],
|
||||
["accountRecoveryPolicy", 4],
|
||||
["requireSso", 5],
|
||||
["automaticAppLoginWithSSO", 6],
|
||||
["twoStepLoginPolicyTitle", 7],
|
||||
["blockClaimedDomainAccountCreation", 8],
|
||||
["sessionTimeoutPolicyTitle", 9],
|
||||
["removeUnlockWithPinPolicyTitle", 10],
|
||||
["passwordGenerator", 11],
|
||||
["uriMatchDetectionPolicy", 12],
|
||||
["activateAutofillPolicy", 13],
|
||||
["sendOptions", 14],
|
||||
["disableSend", 15],
|
||||
["restrictedItemTypePolicy", 16],
|
||||
["freeFamiliesSponsorship", 17],
|
||||
["disableExport", 18],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Default order for policies not in the mapping. This ensures unmapped policies
|
||||
* appear at the end while maintaining their relative order.
|
||||
*/
|
||||
const DEFAULT_ORDER = 999;
|
||||
|
||||
@Pipe({
|
||||
name: "policyOrder",
|
||||
standalone: true,
|
||||
})
|
||||
export class PolicyOrderPipe implements PipeTransform {
|
||||
transform(
|
||||
policies: readonly BasePolicyEditDefinition[] | null | undefined,
|
||||
): BasePolicyEditDefinition[] {
|
||||
if (policies == null || policies.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sortedPolicies = [...policies];
|
||||
|
||||
sortedPolicies.sort((a, b) => {
|
||||
const orderA = POLICY_ORDER_MAP.get(a.name) ?? DEFAULT_ORDER;
|
||||
const orderB = POLICY_ORDER_MAP.get(b.name) ?? DEFAULT_ORDER;
|
||||
|
||||
if (orderA !== orderB) {
|
||||
return orderA - orderB;
|
||||
}
|
||||
|
||||
const indexA = policies.indexOf(a);
|
||||
const indexB = policies.indexOf(b);
|
||||
return indexA - indexB;
|
||||
});
|
||||
|
||||
return sortedPolicies;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
} @else {
|
||||
<bit-table>
|
||||
<ng-template body>
|
||||
@for (p of policies$ | async; track $index) {
|
||||
@for (p of policies$ | async | policyOrder; track $index) {
|
||||
@if (p.display$(organization, configService) | async) {
|
||||
<tr bitRow>
|
||||
<td bitCell ngPreserveWhitespaces>
|
||||
|
||||
@@ -21,13 +21,14 @@ import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { BasePolicyEditDefinition, PolicyDialogComponent } from "./base-policy-edit.component";
|
||||
import { PolicyOrderPipe } from "./pipes/policy-order.pipe";
|
||||
import { PolicyEditDialogComponent } from "./policy-edit-dialog.component";
|
||||
import { PolicyListService } from "./policy-list.service";
|
||||
import { POLICY_EDIT_REGISTER } from "./policy-register-token";
|
||||
|
||||
@Component({
|
||||
templateUrl: "policies.component.html",
|
||||
imports: [SharedModule, HeaderModule],
|
||||
imports: [SharedModule, HeaderModule, PolicyOrderPipe],
|
||||
providers: [
|
||||
safeProvider({
|
||||
provide: PolicyListService,
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<ng-template #step1>
|
||||
<div class="tw-flex tw-justify-center tw-mb-6">
|
||||
<bit-icon class="tw-w-[233px]" [icon]="autoConfirmSvg"></bit-icon>
|
||||
<bit-svg class="tw-w-[233px]" [content]="autoConfirmSvg"></bit-svg>
|
||||
</div>
|
||||
<ol>
|
||||
<li>1. {{ "autoConfirmExtension1" | i18n }}</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="tw-mt-10 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n">
|
||||
</bit-icon>
|
||||
<bit-svg class="tw-w-72 tw-block tw-mb-4" [content]="logo" [ariaLabel]="'appLogoLabel' | i18n">
|
||||
</bit-svg>
|
||||
<div class="tw-flex tw-justify-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BitwardenLogo } from "@bitwarden/assets/svg";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { OrganizationSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/organization-sponsorship.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { IconModule, ToastService } from "@bitwarden/components";
|
||||
import { SvgModule, ToastService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
@@ -22,7 +22,7 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
templateUrl: "accept-family-sponsorship.component.html",
|
||||
imports: [CommonModule, I18nPipe, IconModule],
|
||||
imports: [CommonModule, I18nPipe, SvgModule],
|
||||
})
|
||||
export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent {
|
||||
protected logo = BitwardenLogo;
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
InputModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
@@ -68,7 +68,7 @@ declare global {
|
||||
TypographyModule,
|
||||
CalloutModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
I18nPipe,
|
||||
AsyncActionsModule,
|
||||
JslibModule,
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
@@ -42,7 +42,7 @@ import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-bas
|
||||
InputModule,
|
||||
TypographyModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
I18nPipe,
|
||||
ReactiveFormsModule,
|
||||
AsyncActionsModule,
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
@@ -45,7 +45,7 @@ import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-bas
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
I18nPipe,
|
||||
InputModule,
|
||||
ReactiveFormsModule,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div *ngIf="currentStep === 'credentialCreation'" class="tw-flex tw-flex-col tw-items-center">
|
||||
<div class="tw-size-24 tw-content-center tw-mb-6">
|
||||
<bit-icon [icon]="Icons.TwoFactorAuthSecurityKeyIcon"></bit-icon>
|
||||
<bit-svg [content]="Icons.TwoFactorAuthSecurityKeyIcon"></bit-svg>
|
||||
</div>
|
||||
<h3 bitTypography="h3">{{ "creatingPasskeyLoading" | i18n }}</h3>
|
||||
<p bitTypography="body1">{{ "creatingPasskeyLoadingInfo" | i18n }}</p>
|
||||
@@ -27,7 +27,7 @@
|
||||
class="tw-flex tw-flex-col tw-items-center"
|
||||
>
|
||||
<div class="tw-size-24 tw-content-center tw-mb-6">
|
||||
<bit-icon [icon]="Icons.TwoFactorAuthSecurityKeyFailedIcon"></bit-icon>
|
||||
<bit-svg [content]="Icons.TwoFactorAuthSecurityKeyFailedIcon"></bit-svg>
|
||||
</div>
|
||||
<h3 bitTypography="h3">{{ "errorCreatingPasskey" | i18n }}</h3>
|
||||
<p bitTypography="body1">{{ "errorCreatingPasskeyInfo" | i18n }}</p>
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
<ng-template #organizationIsNotManagedByConsolidatedBillingMSP>
|
||||
<div class="tw-flex tw-flex-col tw-items-center tw-text-info">
|
||||
<div class="tw-size-56 tw-content-center">
|
||||
<bit-icon [icon]="gearIcon" aria-hidden="true"></bit-icon>
|
||||
<bit-svg [content]="gearIcon" aria-hidden="true"></bit-svg>
|
||||
</div>
|
||||
<p class="tw-font-medium">{{ "billingManagedByProvider" | i18n: userOrg.providerName }}</p>
|
||||
<p>{{ "billingContactProviderForAssistance" | i18n }}</p>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { GearIcon } from "@bitwarden/assets/svg";
|
||||
selector: "app-org-subscription-hidden",
|
||||
template: `<div class="tw-flex tw-flex-col tw-items-center tw-text-info">
|
||||
<div class="tw-size-56 tw-content-center">
|
||||
<bit-icon [icon]="gearIcon" aria-hidden="true"></bit-icon>
|
||||
<bit-svg [content]="gearIcon" aria-hidden="true"></bit-svg>
|
||||
</div>
|
||||
<p class="tw-font-medium">{{ "billingManagedByProvider" | i18n: providerName }}</p>
|
||||
<p>{{ "billingContactProviderForAssistance" | i18n }}</p>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h3 bitTypography="h3">{{ "moreFromBitwarden" | i18n }}</h3>
|
||||
<div class="tw-rounded-t tw-bg-background-alt3 tw-p-5">
|
||||
<div class="tw-w-72">
|
||||
<bit-icon [icon]="logo"></bit-icon>
|
||||
<bit-svg [content]="logo"></bit-svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Icon } from "@bitwarden/assets/svg";
|
||||
import { BitSvg } from "@bitwarden/assets/svg";
|
||||
|
||||
import { ReportVariant } from "./report-variant";
|
||||
|
||||
@@ -6,6 +6,6 @@ export type ReportEntry = {
|
||||
title: string;
|
||||
description: string;
|
||||
route: string;
|
||||
icon: Icon;
|
||||
icon: BitSvg;
|
||||
variant: ReportVariant;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[ngClass]="{ 'tw-grayscale': disabled }"
|
||||
>
|
||||
<div class="tw-m-auto tw-size-20 tw-content-center">
|
||||
<bit-icon [icon]="icon" aria-hidden="true"></bit-icon>
|
||||
<bit-svg [content]="icon" aria-hidden="true"></bit-svg>
|
||||
</div>
|
||||
</div>
|
||||
<bit-card-content [ngClass]="{ 'tw-grayscale': disabled }">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { Icon } from "@bitwarden/assets/svg";
|
||||
import { BitSvg } from "@bitwarden/assets/svg";
|
||||
|
||||
import { ReportVariant } from "../models/report-variant";
|
||||
|
||||
@@ -25,7 +25,7 @@ export class ReportCardComponent {
|
||||
@Input() route: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() icon: Icon;
|
||||
@Input() icon: BitSvg;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() variant: ReportVariant;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
BaseCardComponent,
|
||||
CardContentComponent,
|
||||
I18nMockService,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../core/tests";
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
JslibModule,
|
||||
BadgeModule,
|
||||
CardContentComponent,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
RouterTestingModule,
|
||||
PremiumBadgeComponent,
|
||||
BaseCardComponent,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
BadgeModule,
|
||||
BaseCardComponent,
|
||||
CardContentComponent,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../core/tests";
|
||||
@@ -31,7 +31,7 @@ export default {
|
||||
JslibModule,
|
||||
BadgeModule,
|
||||
RouterTestingModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
PremiumBadgeComponent,
|
||||
CardContentComponent,
|
||||
BaseCardComponent,
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
BreadcrumbsModule,
|
||||
ButtonModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
InputModule,
|
||||
MenuModule,
|
||||
TabsModule,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { IconModule } from "@bitwarden/components";
|
||||
import { SvgModule } from "@bitwarden/components";
|
||||
|
||||
import { BillingFreeFamiliesNavItemComponent } from "../billing/shared/billing-free-families-nav-item.component";
|
||||
|
||||
@@ -32,7 +32,7 @@ import { WebLayoutModule } from "./web-layout.module";
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
WebLayoutModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
BillingFreeFamiliesNavItemComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an
|
||||
import { delay, of, startWith } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { LinkModule, IconModule, ProgressModule } from "@bitwarden/components";
|
||||
import { LinkModule, SvgModule, ProgressModule } from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||
|
||||
@@ -16,7 +16,7 @@ export default {
|
||||
component: OnboardingComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, RouterModule, LinkModule, IconModule, ProgressModule],
|
||||
imports: [JslibModule, RouterModule, LinkModule, SvgModule, ProgressModule],
|
||||
declarations: [OnboardingTaskComponent],
|
||||
}),
|
||||
applicationConfig({
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
MultiSelectModule,
|
||||
@@ -63,7 +63,7 @@ import {
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
MultiSelectModule,
|
||||
@@ -99,7 +99,7 @@ import {
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
MultiSelectModule,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@switch (viewState) {
|
||||
@switch (viewState()) {
|
||||
@case ("auth") {
|
||||
<app-send-auth [id]="id" [key]="key" (accessGranted)="onAccessGranted($event)"></app-send-auth>
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<app-send-view
|
||||
[id]="id"
|
||||
[key]="key"
|
||||
[accessToken]="sendAccessToken"
|
||||
[sendResponse]="sendAccessResponse"
|
||||
[accessRequest]="sendAccessRequest"
|
||||
(authRequired)="onAuthRequired()"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { SendAccessToken } from "@bitwarden/common/auth/send-access";
|
||||
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
|
||||
import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response";
|
||||
|
||||
@@ -17,44 +19,45 @@ const SendViewState = Object.freeze({
|
||||
} as const);
|
||||
type SendViewState = (typeof SendViewState)[keyof typeof SendViewState];
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-send-access",
|
||||
templateUrl: "access.component.html",
|
||||
imports: [SendAuthComponent, SendViewComponent, SharedModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccessComponent implements OnInit {
|
||||
viewState: SendViewState = SendViewState.View;
|
||||
readonly viewState = signal<SendViewState>(SendViewState.Auth);
|
||||
id: string;
|
||||
key: string;
|
||||
|
||||
sendAccessToken: SendAccessToken | null = null;
|
||||
sendAccessResponse: SendAccessResponse | null = null;
|
||||
sendAccessRequest: SendAccessRequest = new SendAccessRequest();
|
||||
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private destroyRef: DestroyRef,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.params.subscribe(async (params) => {
|
||||
ngOnInit() {
|
||||
this.route.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
|
||||
this.id = params.sendId;
|
||||
this.key = params.key;
|
||||
|
||||
if (this.id && this.key) {
|
||||
this.viewState = SendViewState.View;
|
||||
this.sendAccessResponse = null;
|
||||
this.sendAccessRequest = new SendAccessRequest();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onAuthRequired() {
|
||||
this.viewState = SendViewState.Auth;
|
||||
this.viewState.set(SendViewState.Auth);
|
||||
}
|
||||
|
||||
onAccessGranted(event: { response: SendAccessResponse; request: SendAccessRequest }) {
|
||||
onAccessGranted(event: {
|
||||
response?: SendAccessResponse;
|
||||
request?: SendAccessRequest;
|
||||
accessToken?: SendAccessToken;
|
||||
}) {
|
||||
this.sendAccessResponse = event.response;
|
||||
this.sendAccessRequest = event.request;
|
||||
this.viewState = SendViewState.View;
|
||||
this.sendAccessToken = event.accessToken;
|
||||
this.viewState.set(SendViewState.View);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
@if (!enterOtp()) {
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input bitInput type="text" [formControl]="email" required appInputVerbatim appAutofocus />
|
||||
</bit-form-field>
|
||||
<div class="tw-flex">
|
||||
<button
|
||||
bitButton
|
||||
bitFormButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
[loading]="loading()"
|
||||
[block]="true"
|
||||
>
|
||||
<span>{{ "sendCode" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input bitInput type="text" [formControl]="otp" required appInputVerbatim appAutofocus />
|
||||
</bit-form-field>
|
||||
<div class="tw-flex">
|
||||
<button
|
||||
bitButton
|
||||
bitFormButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
[loading]="loading()"
|
||||
[block]="true"
|
||||
>
|
||||
<span>{{ "viewSend" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
@Component({
|
||||
selector: "app-send-access-email",
|
||||
templateUrl: "send-access-email.component.html",
|
||||
imports: [SharedModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendAccessEmailComponent implements OnInit, OnDestroy {
|
||||
protected readonly formGroup = input.required<FormGroup>();
|
||||
protected readonly enterOtp = input.required<boolean>();
|
||||
protected email: FormControl;
|
||||
protected otp: FormControl;
|
||||
|
||||
readonly loading = input.required<boolean>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {
|
||||
this.email = new FormControl("", Validators.required);
|
||||
this.otp = new FormControl("", Validators.required);
|
||||
this.formGroup().addControl("email", this.email);
|
||||
this.formGroup().addControl("otp", this.otp);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.formGroup().removeControl("email");
|
||||
this.formGroup().removeControl("otp");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<p class="tw-text-wrap tw-break-all">{{ send.file.fileName }}</p>
|
||||
<p class="tw-text-wrap tw-break-all">{{ send().file.fileName }}</p>
|
||||
<button bitButton type="button" buttonType="primary" [bitAction]="download" [block]="true">
|
||||
<i class="bwi bwi-download" aria-hidden="true"></i>
|
||||
{{ "downloadAttachments" | i18n }} ({{ send.file.sizeName }})
|
||||
{{ "downloadAttachments" | i18n }} ({{ send().file.sizeName }})
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, input } from "@angular/core";
|
||||
|
||||
import { SendAccessToken } from "@bitwarden/common/auth/send-access";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -15,40 +18,39 @@ import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-send-access-file",
|
||||
templateUrl: "send-access-file.component.html",
|
||||
imports: [SharedModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendAccessFileComponent {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() send: SendAccessView;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() decKey: SymmetricCryptoKey;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() accessRequest: SendAccessRequest;
|
||||
readonly send = input<SendAccessView | null>(null);
|
||||
readonly decKey = input<SymmetricCryptoKey | null>(null);
|
||||
readonly accessRequest = input<SendAccessRequest | null>(null);
|
||||
readonly accessToken = input<SendAccessToken | null>(null);
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
private encryptService: EncryptService,
|
||||
private fileDownloadService: FileDownloadService,
|
||||
private sendApiService: SendApiService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
protected download = async () => {
|
||||
if (this.send == null || this.decKey == null) {
|
||||
const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP);
|
||||
const accessToken = this.accessToken();
|
||||
const accessRequest = this.accessRequest();
|
||||
const authMissing = (sendEmailOtp && !accessToken) || (!sendEmailOtp && !accessRequest);
|
||||
if (this.send() == null || this.decKey() == null || authMissing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadData = await this.sendApiService.getSendFileDownloadData(
|
||||
this.send,
|
||||
this.accessRequest,
|
||||
);
|
||||
const downloadData = sendEmailOtp
|
||||
? await this.sendApiService.getSendFileDownloadDataV2(this.send(), accessToken)
|
||||
: await this.sendApiService.getSendFileDownloadData(this.send(), accessRequest);
|
||||
|
||||
if (Utils.isNullOrWhitespace(downloadData.url)) {
|
||||
this.toastService.showToast({
|
||||
@@ -71,9 +73,9 @@ export class SendAccessFileComponent {
|
||||
|
||||
try {
|
||||
const encBuf = await EncArrayBuffer.fromResponse(response);
|
||||
const decBuf = await this.encryptService.decryptFileData(encBuf, this.decKey);
|
||||
const decBuf = await this.encryptService.decryptFileData(encBuf, this.decKey());
|
||||
this.fileDownloadService.download({
|
||||
fileName: this.send.file.fileName,
|
||||
fileName: this.send().file.fileName,
|
||||
blobData: decBuf,
|
||||
downloadMethod: "save",
|
||||
});
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
<p bitTypography="body1">{{ "sendProtectedPassword" | i18n }}</p>
|
||||
<p bitTypography="body1">{{ "sendProtectedPasswordDontKnow" | i18n }}</p>
|
||||
<div class="tw-mb-3" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "password" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="password"
|
||||
required
|
||||
appInputVerbatim
|
||||
appAutofocus
|
||||
/>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<div class="tw-flex">
|
||||
<button
|
||||
bitButton
|
||||
bitFormButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
[loading]="loading"
|
||||
[block]="true"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "password" | i18n }}</bit-label>
|
||||
<input bitInput type="password" [formControl]="password" required appInputVerbatim appAutofocus />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<div class="tw-flex">
|
||||
<button
|
||||
bitButton
|
||||
bitFormButton
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
[loading]="loading()"
|
||||
[block]="true"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }} </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +1,30 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { ChangeDetectionStrategy, Component, input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-send-access-password",
|
||||
templateUrl: "send-access-password.component.html",
|
||||
imports: [SharedModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendAccessPasswordComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
protected formGroup = this.formBuilder.group({
|
||||
password: ["", [Validators.required]],
|
||||
});
|
||||
protected readonly formGroup = input.required<FormGroup>();
|
||||
protected password: FormControl;
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() loading: boolean;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
|
||||
@Output() setPasswordEvent = new EventEmitter<string>();
|
||||
readonly loading = input.required<boolean>();
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
constructor() {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.formGroup.controls.password.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((val) => {
|
||||
this.setPasswordEvent.emit(val);
|
||||
});
|
||||
ngOnInit() {
|
||||
this.password = new FormControl("", Validators.required);
|
||||
this.formGroup().addControl("password", this.password);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.formGroup().removeControl("password");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
<form (ngSubmit)="onSubmit(password)">
|
||||
<div class="tw-text-main tw-text-center" *ngIf="unavailable">
|
||||
<p bitTypography="body1">{{ "sendAccessUnavailable" | i18n }}</p>
|
||||
@if (loading()) {
|
||||
<div class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-text-main tw-text-center" *ngIf="error">
|
||||
<p bitTypography="body1">{{ "unexpectedErrorSend" | i18n }}</p>
|
||||
</div>
|
||||
|
||||
<app-send-access-password
|
||||
*ngIf="!unavailable"
|
||||
(setPasswordEvent)="password = $event"
|
||||
[loading]="loading"
|
||||
></app-send-access-password>
|
||||
}
|
||||
<form [formGroup]="sendAccessForm" (ngSubmit)="onSubmit()">
|
||||
@if (error()) {
|
||||
<div class="tw-text-main tw-text-center">
|
||||
<p bitTypography="body1">{{ "unexpectedErrorSend" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (unavailable()) {
|
||||
<div class="tw-text-main tw-text-center">
|
||||
<p bitTypography="body1">{{ "sendAccessUnavailable" | i18n }}</p>
|
||||
</div>
|
||||
} @else {
|
||||
@switch (sendAuthType()) {
|
||||
@case (authType.Password) {
|
||||
<app-send-access-password
|
||||
[loading]="loading()"
|
||||
[formGroup]="sendAccessForm"
|
||||
></app-send-access-password>
|
||||
}
|
||||
@case (authType.Email) {
|
||||
<app-send-access-email
|
||||
[formGroup]="sendAccessForm"
|
||||
[enterOtp]="enterOtp()"
|
||||
[loading]="loading()"
|
||||
></app-send-access-email>
|
||||
}
|
||||
}
|
||||
}
|
||||
</form>
|
||||
|
||||
@@ -1,86 +1,211 @@
|
||||
import { ChangeDetectionStrategy, Component, input, output } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, input, OnInit, output, signal } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
emailAndOtpRequiredEmailSent,
|
||||
emailInvalid,
|
||||
emailRequired,
|
||||
otpInvalid,
|
||||
passwordHashB64Invalid,
|
||||
passwordHashB64Required,
|
||||
SendAccessDomainCredentials,
|
||||
SendAccessToken,
|
||||
SendHashedPasswordB64,
|
||||
sendIdInvalid,
|
||||
SendOtp,
|
||||
SendTokenService,
|
||||
} from "@bitwarden/common/auth/send-access";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
|
||||
import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response";
|
||||
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { AuthType } from "@bitwarden/common/tools/send/types/auth-type";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { SendAccessEmailComponent } from "./send-access-email.component";
|
||||
import { SendAccessPasswordComponent } from "./send-access-password.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-send-auth",
|
||||
templateUrl: "send-auth.component.html",
|
||||
imports: [SendAccessPasswordComponent, SharedModule],
|
||||
imports: [SendAccessPasswordComponent, SendAccessEmailComponent, SharedModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class SendAuthComponent {
|
||||
readonly id = input.required<string>();
|
||||
readonly key = input.required<string>();
|
||||
export class SendAuthComponent implements OnInit {
|
||||
protected readonly id = input.required<string>();
|
||||
protected readonly key = input.required<string>();
|
||||
|
||||
accessGranted = output<{
|
||||
response: SendAccessResponse;
|
||||
request: SendAccessRequest;
|
||||
protected accessGranted = output<{
|
||||
response?: SendAccessResponse;
|
||||
request?: SendAccessRequest;
|
||||
accessToken?: SendAccessToken;
|
||||
}>();
|
||||
|
||||
loading = false;
|
||||
error = false;
|
||||
unavailable = false;
|
||||
password?: string;
|
||||
authType = AuthType;
|
||||
|
||||
private accessRequest!: SendAccessRequest;
|
||||
private expiredAuthAttempts = 0;
|
||||
|
||||
readonly loading = signal<boolean>(false);
|
||||
readonly error = signal<boolean>(false);
|
||||
readonly unavailable = signal<boolean>(false);
|
||||
readonly sendAuthType = signal<AuthType>(AuthType.None);
|
||||
readonly enterOtp = signal<boolean>(false);
|
||||
|
||||
sendAccessForm = this.formBuilder.group<{ password?: string; email?: string; otp?: string }>({});
|
||||
|
||||
constructor(
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private sendApiService: SendApiService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private formBuilder: FormBuilder,
|
||||
private configService: ConfigService,
|
||||
private sendTokenService: SendTokenService,
|
||||
) {}
|
||||
|
||||
async onSubmit(password: string) {
|
||||
this.password = password;
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
this.unavailable = false;
|
||||
ngOnInit() {
|
||||
void this.onSubmit();
|
||||
}
|
||||
|
||||
async onSubmit() {
|
||||
this.loading.set(true);
|
||||
this.unavailable.set(false);
|
||||
this.error.set(false);
|
||||
const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP);
|
||||
if (sendEmailOtp) {
|
||||
await this.attemptV2Access();
|
||||
} else {
|
||||
await this.attemptV1Access();
|
||||
}
|
||||
this.loading.set(false);
|
||||
}
|
||||
|
||||
private async attemptV1Access() {
|
||||
try {
|
||||
const keyArray = Utils.fromUrlB64ToArray(this.key());
|
||||
this.accessRequest = new SendAccessRequest();
|
||||
|
||||
const passwordHash = await this.cryptoFunctionService.pbkdf2(
|
||||
this.password,
|
||||
keyArray,
|
||||
"sha256",
|
||||
SEND_KDF_ITERATIONS,
|
||||
);
|
||||
this.accessRequest.password = Utils.fromBufferToB64(passwordHash);
|
||||
|
||||
const sendResponse = await this.sendApiService.postSendAccess(this.id(), this.accessRequest);
|
||||
this.accessGranted.emit({ response: sendResponse, request: this.accessRequest });
|
||||
const accessRequest = new SendAccessRequest();
|
||||
if (this.sendAuthType() === AuthType.Password) {
|
||||
const password = this.sendAccessForm.value.password;
|
||||
if (password == null) {
|
||||
return;
|
||||
}
|
||||
accessRequest.password = await this.getPasswordHashB64(password, this.key());
|
||||
}
|
||||
const sendResponse = await this.sendApiService.postSendAccess(this.id(), accessRequest);
|
||||
this.accessGranted.emit({ request: accessRequest, response: sendResponse });
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse) {
|
||||
if (e.statusCode === 404) {
|
||||
this.unavailable = true;
|
||||
} else if (e.statusCode === 400) {
|
||||
if (e.statusCode === 401) {
|
||||
this.sendAuthType.set(AuthType.Password);
|
||||
} else if (e.statusCode === 404) {
|
||||
this.unavailable.set(true);
|
||||
} else {
|
||||
this.error.set(true);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
} else {
|
||||
this.error = true;
|
||||
this.error.set(true);
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async attemptV2Access(): Promise<void> {
|
||||
let sendAccessCreds: SendAccessDomainCredentials | null = null;
|
||||
if (this.sendAuthType() === AuthType.Email) {
|
||||
const email = this.sendAccessForm.value.email;
|
||||
if (email == null) {
|
||||
return;
|
||||
}
|
||||
if (!this.enterOtp()) {
|
||||
sendAccessCreds = { kind: "email", email };
|
||||
} else {
|
||||
const otp = this.sendAccessForm.value.otp as SendOtp;
|
||||
if (otp == null) {
|
||||
return;
|
||||
}
|
||||
sendAccessCreds = { kind: "email_otp", email, otp };
|
||||
}
|
||||
} else if (this.sendAuthType() === AuthType.Password) {
|
||||
const password = this.sendAccessForm.value.password;
|
||||
if (password == null) {
|
||||
return;
|
||||
}
|
||||
const passwordHashB64 = await this.getPasswordHashB64(password, this.key());
|
||||
sendAccessCreds = { kind: "password", passwordHashB64 };
|
||||
}
|
||||
const response = !sendAccessCreds
|
||||
? await firstValueFrom(this.sendTokenService.tryGetSendAccessToken$(this.id()))
|
||||
: await firstValueFrom(this.sendTokenService.getSendAccessToken$(this.id(), sendAccessCreds));
|
||||
if (response instanceof SendAccessToken) {
|
||||
this.expiredAuthAttempts = 0;
|
||||
this.accessGranted.emit({ accessToken: response });
|
||||
} else if (response.kind === "expired") {
|
||||
if (this.expiredAuthAttempts > 2) {
|
||||
return;
|
||||
}
|
||||
this.expiredAuthAttempts++;
|
||||
await this.attemptV2Access();
|
||||
} else if (response.kind === "expected_server") {
|
||||
this.expiredAuthAttempts = 0;
|
||||
if (emailRequired(response.error)) {
|
||||
this.sendAuthType.set(AuthType.Email);
|
||||
} else if (emailAndOtpRequiredEmailSent(response.error) || emailInvalid(response.error)) {
|
||||
this.enterOtp.set(true);
|
||||
} else if (otpInvalid(response.error)) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidVerificationCode"),
|
||||
});
|
||||
} else if (passwordHashB64Required(response.error)) {
|
||||
this.sendAuthType.set(AuthType.Password);
|
||||
} else if (passwordHashB64Invalid(response.error)) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidSendPassword"),
|
||||
});
|
||||
} else if (sendIdInvalid(response.error)) {
|
||||
this.unavailable.set(true);
|
||||
} else {
|
||||
this.error.set(true);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: response.error.error_description ?? "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.expiredAuthAttempts = 0;
|
||||
this.error.set(true);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: response.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async getPasswordHashB64(password: string, key: string) {
|
||||
const keyArray = Utils.fromUrlB64ToArray(key);
|
||||
const passwordHash = await this.cryptoFunctionService.pbkdf2(
|
||||
password,
|
||||
keyArray,
|
||||
"sha256",
|
||||
SEND_KDF_ITERATIONS,
|
||||
);
|
||||
return Utils.fromBufferToB64(passwordHash) as SendHashedPasswordB64;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,13 @@
|
||||
<bit-callout *ngIf="hideEmail" type="warning" title="{{ 'warning' | i18n }}">
|
||||
{{ "viewSendHiddenEmailWarning" | i18n }}
|
||||
<a bitLink href="https://bitwarden.com/help/receive-send/" target="_blank" rel="noreferrer">{{
|
||||
"learnMore" | i18n
|
||||
}}</a
|
||||
>.
|
||||
</bit-callout>
|
||||
@if (hideEmail()) {
|
||||
<bit-callout type="warning" title="{{ 'warning' | i18n }}">
|
||||
{{ "viewSendHiddenEmailWarning" | i18n }}
|
||||
<a bitLink href="https://bitwarden.com/help/receive-send/" target="_blank" rel="noreferrer">{{
|
||||
"learnMore" | i18n
|
||||
}}</a>
|
||||
</bit-callout>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="!loading; else spinner">
|
||||
<div class="tw-text-main tw-text-center" *ngIf="unavailable">
|
||||
<p bitTypography="body1">{{ "sendAccessUnavailable" | i18n }}</p>
|
||||
</div>
|
||||
<div class="tw-text-main tw-text-center" *ngIf="error">
|
||||
<p bitTypography="body1">{{ "unexpectedErrorSend" | i18n }}</p>
|
||||
</div>
|
||||
<div *ngIf="send && !error && !unavailable">
|
||||
<p class="tw-text-center">
|
||||
<b>{{ send.name }}</b>
|
||||
</p>
|
||||
<hr />
|
||||
<!-- Text -->
|
||||
<ng-container *ngIf="send.type === sendType.Text">
|
||||
<app-send-access-text [send]="send"></app-send-access-text>
|
||||
</ng-container>
|
||||
<!-- File -->
|
||||
<ng-container *ngIf="send.type === sendType.File">
|
||||
<app-send-access-file
|
||||
[send]="send"
|
||||
[decKey]="decKey"
|
||||
[accessRequest]="accessRequest()"
|
||||
></app-send-access-file>
|
||||
</ng-container>
|
||||
<p *ngIf="expirationDate" class="tw-text-center tw-text-muted">
|
||||
Expires: {{ expirationDate | date: "medium" }}
|
||||
</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #spinner>
|
||||
@if (loading()) {
|
||||
<div class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
@@ -44,4 +16,39 @@
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
} @else {
|
||||
@if (unavailable()) {
|
||||
<div class="tw-text-main tw-text-center">
|
||||
<p bitTypography="body1">{{ "sendAccessUnavailable" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (error()) {
|
||||
<div class="tw-text-main tw-text-center">
|
||||
<p bitTypography="body1">{{ "unexpectedErrorSend" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (send()) {
|
||||
<div>
|
||||
<p class="tw-text-center">
|
||||
<b>{{ send().name }}</b>
|
||||
</p>
|
||||
<hr />
|
||||
@switch (send().type) {
|
||||
@case (sendType.Text) {
|
||||
<app-send-access-text [send]="send()"></app-send-access-text>
|
||||
}
|
||||
@case (sendType.File) {
|
||||
<app-send-access-file
|
||||
[send]="send()"
|
||||
[decKey]="decKey"
|
||||
[accessRequest]="accessRequest()"
|
||||
[accessToken]="accessToken()"
|
||||
></app-send-access-file>
|
||||
}
|
||||
}
|
||||
@if (expirationDate()) {
|
||||
<p class="tw-text-center tw-text-muted">Expires: {{ expirationDate() | date: "medium" }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
OnInit,
|
||||
output,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
|
||||
import { SendAccessToken } from "@bitwarden/common/auth/send-access";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
@@ -34,17 +38,25 @@ import { SendAccessTextComponent } from "./send-access-text.component";
|
||||
export class SendViewComponent implements OnInit {
|
||||
readonly id = input.required<string>();
|
||||
readonly key = input.required<string>();
|
||||
readonly accessToken = input<SendAccessToken | null>(null);
|
||||
readonly sendResponse = input<SendAccessResponse | null>(null);
|
||||
readonly accessRequest = input<SendAccessRequest>(new SendAccessRequest());
|
||||
|
||||
authRequired = output<void>();
|
||||
|
||||
send: SendAccessView | null = null;
|
||||
readonly send = signal<SendAccessView | null>(null);
|
||||
readonly expirationDate = computed<Date | null>(() => this.send()?.expirationDate ?? null);
|
||||
readonly creatorIdentifier = computed<string | null>(
|
||||
() => this.send()?.creatorIdentifier ?? null,
|
||||
);
|
||||
readonly hideEmail = computed<boolean>(
|
||||
() => this.send() != null && this.creatorIdentifier() == null,
|
||||
);
|
||||
readonly loading = signal<boolean>(false);
|
||||
readonly unavailable = signal<boolean>(false);
|
||||
readonly error = signal<boolean>(false);
|
||||
|
||||
sendType = SendType;
|
||||
loading = true;
|
||||
unavailable = false;
|
||||
error = false;
|
||||
hideEmail = false;
|
||||
decKey!: SymmetricCryptoKey;
|
||||
|
||||
constructor(
|
||||
@@ -53,50 +65,48 @@ export class SendViewComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private layoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
private cdRef: ChangeDetectorRef,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
get expirationDate() {
|
||||
if (this.send == null || this.send.expirationDate == null) {
|
||||
return null;
|
||||
}
|
||||
return this.send.expirationDate;
|
||||
}
|
||||
|
||||
get creatorIdentifier() {
|
||||
if (this.send == null || this.send.creatorIdentifier == null) {
|
||||
return null;
|
||||
}
|
||||
return this.send.creatorIdentifier;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
ngOnInit() {
|
||||
void this.load();
|
||||
}
|
||||
|
||||
private async load() {
|
||||
this.unavailable = false;
|
||||
this.error = false;
|
||||
this.hideEmail = false;
|
||||
this.loading = true;
|
||||
|
||||
let response = this.sendResponse();
|
||||
this.loading.set(true);
|
||||
this.unavailable.set(false);
|
||||
this.error.set(false);
|
||||
|
||||
try {
|
||||
if (!response) {
|
||||
response = await this.sendApiService.postSendAccess(this.id(), this.accessRequest());
|
||||
const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP);
|
||||
let response: SendAccessResponse;
|
||||
if (sendEmailOtp) {
|
||||
const accessToken = this.accessToken();
|
||||
if (!accessToken) {
|
||||
this.authRequired.emit();
|
||||
return;
|
||||
}
|
||||
response = await this.sendApiService.postSendAccessV2(accessToken);
|
||||
} else {
|
||||
const sendResponse = this.sendResponse();
|
||||
if (!sendResponse) {
|
||||
this.authRequired.emit();
|
||||
return;
|
||||
}
|
||||
response = sendResponse;
|
||||
}
|
||||
|
||||
const keyArray = Utils.fromUrlB64ToArray(this.key());
|
||||
const sendAccess = new SendAccess(response);
|
||||
this.decKey = await this.keyService.makeSendKey(keyArray);
|
||||
this.send = await sendAccess.decrypt(this.decKey);
|
||||
const decSend = await sendAccess.decrypt(this.decKey);
|
||||
this.send.set(decSend);
|
||||
} catch (e) {
|
||||
this.send.set(null);
|
||||
if (e instanceof ErrorResponse) {
|
||||
if (e.statusCode === 401) {
|
||||
this.authRequired.emit();
|
||||
} else if (e.statusCode === 404) {
|
||||
this.unavailable = true;
|
||||
this.unavailable.set(true);
|
||||
} else if (e.statusCode === 400) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
@@ -104,28 +114,23 @@ export class SendViewComponent implements OnInit {
|
||||
message: e.message,
|
||||
});
|
||||
} else {
|
||||
this.error = true;
|
||||
this.error.set(true);
|
||||
}
|
||||
} else {
|
||||
this.error = true;
|
||||
this.error.set(true);
|
||||
}
|
||||
} finally {
|
||||
this.loading.set(false);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.hideEmail =
|
||||
this.creatorIdentifier == null && !this.loading && !this.unavailable && !response;
|
||||
|
||||
this.hideEmail = this.send != null && this.creatorIdentifier == null;
|
||||
|
||||
if (this.creatorIdentifier != null) {
|
||||
const creatorIdentifier = this.creatorIdentifier();
|
||||
if (creatorIdentifier != null) {
|
||||
this.layoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageSubtitle: {
|
||||
key: "sendAccessCreatorIdentifier",
|
||||
placeholders: [this.creatorIdentifier],
|
||||
placeholders: [creatorIdentifier],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<div class="tw-mb-6 tw-mt-8">
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="activeSendIcon"></bit-icon>
|
||||
<bit-svg [content]="activeSendIcon"></bit-svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { map, Observable, of, tap } from "rxjs";
|
||||
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
import { ButtonComponent, IconModule } from "@bitwarden/components";
|
||||
import { ButtonComponent, SvgModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
@@ -24,7 +24,7 @@ import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manua
|
||||
@Component({
|
||||
selector: "vault-browser-extension-prompt",
|
||||
templateUrl: "./browser-extension-prompt.component.html",
|
||||
imports: [CommonModule, I18nPipe, ButtonComponent, IconModule, ManuallyOpenExtensionComponent],
|
||||
imports: [CommonModule, I18nPipe, ButtonComponent, SvgModule, ManuallyOpenExtensionComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<p bitTypography="body1" class="tw-mb-0">
|
||||
{{ "openExtensionFromToolbarPart1" | i18n }}
|
||||
<bit-icon
|
||||
[icon]="BitwardenIcon"
|
||||
<bit-svg
|
||||
[content]="BitwardenIcon"
|
||||
class="!tw-inline-block [&>svg]:tw-align-baseline [&>svg]:-tw-mb-[0.25rem]"
|
||||
></bit-icon>
|
||||
></bit-svg>
|
||||
{{ "openExtensionFromToolbarPart2" | i18n }}
|
||||
</p>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Component, ChangeDetectionStrategy } from "@angular/core";
|
||||
|
||||
import { BitwardenIcon } from "@bitwarden/assets/svg";
|
||||
import { IconModule } from "@bitwarden/components";
|
||||
import { SvgModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: "vault-manually-open-extension",
|
||||
templateUrl: "./manually-open-extension.component.html",
|
||||
imports: [I18nPipe, IconModule],
|
||||
imports: [I18nPipe, SvgModule],
|
||||
})
|
||||
export class ManuallyOpenExtensionComponent {
|
||||
protected BitwardenIcon = BitwardenIcon;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<section *ngIf="showSuccessUI" class="tw-flex tw-flex-col tw-items-center">
|
||||
<div class="tw-size-[90px]">
|
||||
<bit-icon [icon]="PartyIcon"></bit-icon>
|
||||
<bit-svg [content]="PartyIcon"></bit-svg>
|
||||
</div>
|
||||
<h1 bitTypography="h2" class="tw-mb-6 tw-mt-4 tw-text-center">
|
||||
{{
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
CenterPositionStrategy,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@@ -52,7 +52,7 @@ type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
|
||||
JslibModule,
|
||||
ButtonComponent,
|
||||
LinkModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
RouterModule,
|
||||
AddExtensionVideosComponent,
|
||||
ManuallyOpenExtensionComponent,
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
EmptyTrash,
|
||||
FavoritesIcon,
|
||||
ItemTypes,
|
||||
Icon,
|
||||
BitSvg,
|
||||
} from "@bitwarden/assets/svg";
|
||||
import { AutomaticUserConfirmationService } from "@bitwarden/auto-confirm";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -160,7 +160,7 @@ type EmptyStateType = "trash" | "favorites" | "archive";
|
||||
type EmptyStateItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: Icon;
|
||||
icon: BitSvg;
|
||||
};
|
||||
|
||||
type EmptyStateMap = Record<EmptyStateType, EmptyStateItem>;
|
||||
|
||||
@@ -586,6 +586,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@@ -1365,6 +1368,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@@ -6928,17 +6937,17 @@
|
||||
"personalVaultExportPolicyInEffect": {
|
||||
"message": "One or more organization policies prevents you from exporting your individual vault."
|
||||
},
|
||||
"activateAutofill": {
|
||||
"message": "Activate auto-fill"
|
||||
"activateAutofillPolicy": {
|
||||
"message": "Activate autofill"
|
||||
},
|
||||
"activateAutofillPolicyDescription": {
|
||||
"message": "Activate the autofill on page load setting on the browser extension for all existing and new members."
|
||||
},
|
||||
"experimentalFeature": {
|
||||
"message": "Compromised or untrusted websites can exploit auto-fill on page load."
|
||||
"autofillOnPageLoadExploitWarning": {
|
||||
"message": "Compromised or untrusted websites can exploit autofill on page load."
|
||||
},
|
||||
"learnMoreAboutAutofill": {
|
||||
"message": "Learn more about auto-fill"
|
||||
"learnMoreAboutAutofillPolicy": {
|
||||
"message": "Learn more about autofill"
|
||||
},
|
||||
"selectType": {
|
||||
"message": "Select SSO type"
|
||||
@@ -12691,6 +12700,21 @@
|
||||
"storageFullDescription": {
|
||||
"message": "You have used all $GB$ GB of your encrypted storage. To continue storing files, add more storage."
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
},
|
||||
"whenYouRemoveStorage": {
|
||||
"message": "When you remove storage, you will receive a prorated account credit that will automatically go toward your next bill."
|
||||
},
|
||||
@@ -12699,5 +12723,8 @@
|
||||
},
|
||||
"emailProtected": {
|
||||
"message": "Email protected"
|
||||
},
|
||||
"invalidSendPassword": {
|
||||
"message": "Invalid Send password"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<bit-callout type="warning">
|
||||
{{ "experimentalFeature" | i18n }}
|
||||
{{ "autofillOnPageLoadExploitWarning" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/auto-fill-browser/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ "learnMoreAboutAutofill" | i18n }}</a
|
||||
>{{ "learnMoreAboutAutofillPolicy" | i18n }}</a
|
||||
>
|
||||
</bit-callout>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
export class ActivateAutofillPolicy extends BasePolicyEditDefinition {
|
||||
name = "activateAutofill";
|
||||
name = "activateAutofillPolicy";
|
||||
description = "activateAutofillPolicyDescription";
|
||||
type = PolicyType.ActivateAutofill;
|
||||
component = ActivateAutofillPolicyComponent;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="tw-mt-5 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon
|
||||
<bit-svg
|
||||
class="tw-w-72 tw-block tw-mb-4"
|
||||
[icon]="logo"
|
||||
[content]="logo"
|
||||
[ariaLabel]="'appLogoLabel' | i18n"
|
||||
></bit-icon>
|
||||
></bit-svg>
|
||||
<p class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -85,7 +84,6 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
private providerService: ProviderService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {
|
||||
super(
|
||||
@@ -100,7 +98,7 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
toastService,
|
||||
);
|
||||
|
||||
this.dataSource = new MembersTableDataSource(this.configService, this.environmentService);
|
||||
this.dataSource = new MembersTableDataSource(this.environmentService);
|
||||
|
||||
combineLatest([
|
||||
this.activatedRoute.parent.params,
|
||||
|
||||
@@ -21,7 +21,6 @@ import { Provider } from "@bitwarden/common/admin-console/models/domain/provider
|
||||
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
@@ -72,7 +71,6 @@ export class vNextMembersComponent {
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private providerService = inject(ProviderService);
|
||||
private accountService = inject(AccountService);
|
||||
private configService = inject(ConfigService);
|
||||
private environmentService = inject(EnvironmentService);
|
||||
private providerActionsService = inject(ProviderActionsService);
|
||||
private memberActionsService = inject(MemberActionsService);
|
||||
@@ -94,7 +92,7 @@ export class vNextMembersComponent {
|
||||
protected statusToggle = new BehaviorSubject<ProviderUserStatusType | undefined>(undefined);
|
||||
|
||||
protected readonly dataSource: WritableSignal<ProvidersTableDataSource> = signal(
|
||||
new ProvidersTableDataSource(this.configService, this.environmentService),
|
||||
new ProvidersTableDataSource(this.environmentService),
|
||||
);
|
||||
protected readonly firstLoaded: WritableSignal<boolean> = signal(false);
|
||||
|
||||
@@ -177,7 +175,7 @@ export class vNextMembersComponent {
|
||||
// Capture the original count BEFORE enforcing the limit
|
||||
const originalInvitedCount = allInvitedUsers.length;
|
||||
|
||||
// When feature flag is enabled, limit invited users and uncheck the excess
|
||||
// In cloud environments, limit invited users and uncheck the excess
|
||||
let checkedInvitedUsers: ProviderUser[];
|
||||
if (this.dataSource().isIncreasedBulkLimitEnabled()) {
|
||||
checkedInvitedUsers = this.dataSource().limitAndUncheckExcess(
|
||||
@@ -198,7 +196,7 @@ export class vNextMembersComponent {
|
||||
}
|
||||
|
||||
try {
|
||||
// When feature flag is enabled, show toast instead of dialog
|
||||
// In cloud environments, show toast instead of dialog
|
||||
if (this.dataSource().isIncreasedBulkLimitEnabled()) {
|
||||
await this.apiService.postManyProviderUserReinvite(
|
||||
providerId,
|
||||
@@ -226,7 +224,7 @@ export class vNextMembersComponent {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Feature flag disabled - show legacy dialog
|
||||
// In self-hosted environments, show legacy dialog
|
||||
const request = this.apiService.postManyProviderUserReinvite(
|
||||
providerId,
|
||||
new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)),
|
||||
|
||||
@@ -7,13 +7,13 @@ import { combineLatest, map, Observable, Subject, switchMap } from "rxjs";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BusinessUnitPortalLogo, Icon, ProviderPortalLogo } from "@bitwarden/assets/svg";
|
||||
import { BusinessUnitPortalLogo, BitSvg, ProviderPortalLogo } from "@bitwarden/assets/svg";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { IconModule } from "@bitwarden/components";
|
||||
import { SvgModule } from "@bitwarden/components";
|
||||
import { NonIndividualSubscriber } from "@bitwarden/web-vault/app/billing/types";
|
||||
import { TaxIdWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components";
|
||||
import { TaxIdWarningType } from "@bitwarden/web-vault/app/billing/warnings/types";
|
||||
@@ -31,7 +31,7 @@ import { ProviderWarningsService } from "../../billing/providers/warnings/servic
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
WebLayoutModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
TaxIdWarningComponent,
|
||||
],
|
||||
})
|
||||
@@ -41,7 +41,7 @@ export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
protected provider$: Observable<Provider>;
|
||||
|
||||
protected logo$: Observable<Icon>;
|
||||
protected logo$: Observable<BitSvg>;
|
||||
|
||||
protected canAccessBilling$: Observable<boolean>;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="tw-mt-12 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon
|
||||
<bit-svg
|
||||
class="tw-w-72 tw-block tw-mb-4"
|
||||
[icon]="logo"
|
||||
[content]="logo"
|
||||
[ariaLabel]="'appLogoLabel' | i18n"
|
||||
></bit-icon>
|
||||
></bit-svg>
|
||||
<p class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="tw-mt-12 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon
|
||||
<bit-svg
|
||||
class="tw-w-72 tw-block tw-mb-4"
|
||||
[icon]="bitwardenLogo"
|
||||
[content]="bitwardenLogo"
|
||||
[ariaLabel]="'appLogoLabel' | i18n"
|
||||
></bit-icon>
|
||||
></bit-svg>
|
||||
<p class="tw-text-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -68,11 +68,11 @@
|
||||
<div
|
||||
class="tw-size-full tw-flex tw-items-center tw-justify-center tw-bg-secondary-100 tw-rounded-lg"
|
||||
>
|
||||
<bit-icon
|
||||
[icon]="icon()"
|
||||
<bit-svg
|
||||
[content]="icon()"
|
||||
class="tw-size-16 xl:tw-size-24 tw-text-muted"
|
||||
aria-hidden="true"
|
||||
></bit-icon>
|
||||
></bit-svg>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -94,11 +94,11 @@
|
||||
<div
|
||||
class="tw-size-full tw-flex tw-items-center tw-justify-center tw-bg-secondary-100 tw-rounded-lg"
|
||||
>
|
||||
<bit-icon
|
||||
[icon]="icon()"
|
||||
<bit-svg
|
||||
[content]="icon()"
|
||||
class="tw-size-12 sm:tw-size-16 tw-text-muted"
|
||||
aria-hidden="true"
|
||||
></bit-icon>
|
||||
></bit-svg>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, input, isDevMode, OnInit } from "@angular/core";
|
||||
|
||||
import { Icon } from "@bitwarden/assets/svg";
|
||||
import { ButtonModule, IconModule } from "@bitwarden/components";
|
||||
import { BitSvg } from "@bitwarden/assets/svg";
|
||||
import { ButtonModule, SvgModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "empty-state-card",
|
||||
templateUrl: "./empty-state-card.component.html",
|
||||
imports: [CommonModule, IconModule, ButtonModule],
|
||||
imports: [CommonModule, SvgModule, ButtonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class EmptyStateCardComponent implements OnInit {
|
||||
readonly icon = input<Icon | null>(null);
|
||||
readonly icon = input<BitSvg | null>(null);
|
||||
readonly videoSrc = input<string | null>(null);
|
||||
readonly title = input<string>("");
|
||||
readonly description = input<string>("");
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { map, concatMap, firstValueFrom } from "rxjs";
|
||||
|
||||
import { Icon, DeactivatedOrg } from "@bitwarden/assets/svg";
|
||||
import { BitSvg, DeactivatedOrg } from "@bitwarden/assets/svg";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
@@ -23,7 +23,7 @@ export class OrgSuspendedComponent {
|
||||
private route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
protected DeactivatedOrg: Icon = DeactivatedOrg;
|
||||
protected DeactivatedOrg: BitSvg = DeactivatedOrg;
|
||||
protected organizationName$ = this.route.params.pipe(
|
||||
concatMap(async (params) => {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
@@ -207,6 +207,7 @@ export default tseslint.config(
|
||||
"error",
|
||||
{ ignoreIfHas: ["bitPasswordInputToggle"] },
|
||||
],
|
||||
"@bitwarden/components/no-bwi-class-usage": "warn",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ng-container>
|
||||
<div class="tw-size-[70px] tw-content-center" *ngIf="!!IconProviderMap[provider]">
|
||||
<bit-icon [icon]="IconProviderMap[provider]"></bit-icon>
|
||||
<bit-svg [content]="IconProviderMap[provider]"></bit-svg>
|
||||
</div>
|
||||
<!-- Other 2FA Types (Duo, Yubico, U2F as PNG) -->
|
||||
<img
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import {
|
||||
Icon,
|
||||
BitSvg,
|
||||
TwoFactorAuthAuthenticatorIcon,
|
||||
TwoFactorAuthEmailIcon,
|
||||
TwoFactorAuthWebAuthnIcon,
|
||||
@@ -24,7 +24,7 @@ export class TwoFactorIconComponent {
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() name: string;
|
||||
|
||||
protected readonly IconProviderMap: { [key: number | string]: Icon } = {
|
||||
protected readonly IconProviderMap: { [key: number | string]: BitSvg } = {
|
||||
0: TwoFactorAuthAuthenticatorIcon,
|
||||
1: TwoFactorAuthEmailIcon,
|
||||
7: TwoFactorAuthWebAuthnIcon,
|
||||
|
||||
@@ -25,7 +25,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
@@ -43,7 +43,7 @@ export type State = "assert" | "assertFailed";
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
],
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
RadioButtonModule,
|
||||
@@ -73,9 +73,9 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
MenuModule,
|
||||
NoItemsModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
LinkModule,
|
||||
IconModule,
|
||||
SvgModule,
|
||||
TextDragDirective,
|
||||
CopyClickDirective,
|
||||
A11yTitleDirective,
|
||||
|
||||
@@ -8,9 +8,9 @@ This lib contains assets used by the Bitwarden clients. Unused assets are tree-s
|
||||
|
||||
### SVGs
|
||||
|
||||
SVGs intended to be used with the `bit-icon` component live in `src/svgs`. These SVGs are built with the `icon-service` for security reasons. These SVGs can be viewed in our Component Library [Icon Story](https://components.bitwarden.com/?path=/story/component-library-icon--default).
|
||||
SVGs intended to be used with the `bit-svg` component live in `src/svgs`. These SVGs are built with the `svg` function for security reasons. These SVGs can be viewed in our Component Library [SVG Story](https://components.bitwarden.com/?path=/story/component-library-svg--default).
|
||||
|
||||
When adding a new SVG, follow the instructions in our Component Library: [SVG Icon Docs](https://components.bitwarden.com/?path=/docs/component-library-icon--docs)
|
||||
When adding a new SVG, follow the instructions in our Component Library: [SVG Docs](https://components.bitwarden.com/?path=/docs/component-library-svg--docs)
|
||||
|
||||
When importing an SVG in one of the clients:
|
||||
`import { ExampleSvg } from "@bitwarden/assets/svg";`
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
class Icon {
|
||||
constructor(readonly svg: string) {}
|
||||
}
|
||||
|
||||
// We only export the type to prohibit the creation of Icons without using
|
||||
// the `svgIcon` template literal tag.
|
||||
export type { Icon };
|
||||
|
||||
export function isIcon(icon: unknown): icon is Icon {
|
||||
return icon instanceof Icon;
|
||||
}
|
||||
|
||||
export class DynamicContentNotAllowedError extends Error {
|
||||
constructor() {
|
||||
super("Dynamic content in icons is not allowed due to risk of user-injected XSS.");
|
||||
}
|
||||
}
|
||||
|
||||
export function svgIcon(strings: TemplateStringsArray, ...values: unknown[]): Icon {
|
||||
if (values.length > 0) {
|
||||
throw new DynamicContentNotAllowedError();
|
||||
}
|
||||
|
||||
return new Icon(strings[0]);
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./svgs";
|
||||
export * from "./icon-service";
|
||||
export * from "./svg";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user