1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-16 00:24:52 +00:00

Merge branch 'main' into dev/kreynolds/tunnel_proto_v2

This commit is contained in:
Katherine Reynolds
2025-11-17 09:20:25 -08:00
25 changed files with 240 additions and 96 deletions

View File

@@ -251,7 +251,7 @@ jobs:
TARGET: musl
run: |
rustup target add x86_64-unknown-linux-musl
node build.js --target=x86_64-unknown-linux-musl --release
node build.js --target=x86_64-unknown-linux-musl
- name: Build application
run: npm run dist:lin
@@ -414,7 +414,7 @@ jobs:
TARGET: musl
run: |
rustup target add aarch64-unknown-linux-musl
node build.js --target=aarch64-unknown-linux-musl --release
node build.js --target=aarch64-unknown-linux-musl
- name: Check index.d.ts generated
if: github.event_name == 'pull_request' && steps.cache.outputs.cache-hit != 'true'
@@ -995,12 +995,12 @@ jobs:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.14'
- name: Set up Node-gyp
run: python3 -m pip install setuptools
@@ -1232,12 +1232,12 @@ jobs:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.14'
- name: Set up Node-gyp
run: python3 -m pip install setuptools
@@ -1504,12 +1504,12 @@ jobs:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.14'
- name: Set up Node-gyp
run: python3 -m pip install setuptools

View File

@@ -1530,5 +1530,63 @@ describe("NotificationBackground", () => {
expect(environmentServiceSpy).toHaveBeenCalled();
});
});
describe("handleUnlockPopoutClosed", () => {
let onRemovedListeners: Array<(tabId: number, removeInfo: chrome.tabs.OnRemovedInfo) => void>;
let tabsQuerySpy: jest.SpyInstance;
beforeEach(() => {
onRemovedListeners = [];
chrome.tabs.onRemoved.addListener = jest.fn((listener) => {
onRemovedListeners.push(listener);
});
chrome.runtime.getURL = jest.fn().mockReturnValue("chrome-extension://id/popup/index.html");
notificationBackground.init();
});
const triggerTabRemoved = async (tabId: number) => {
onRemovedListeners[0](tabId, mock<chrome.tabs.OnRemovedInfo>());
await flushPromises();
};
it("sends abandon message when unlock popout is closed and vault is locked", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]);
await triggerTabRemoved(1);
expect(tabsQuerySpy).toHaveBeenCalled();
expect(messagingService.send).toHaveBeenCalledWith("abandonAutofillPendingNotifications");
});
it("uses tracked tabId for fast lookup when available", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Locked);
tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([
{
id: 123,
url: "chrome-extension://id/popup/index.html?singleActionPopout=auth_unlockExtension",
} as chrome.tabs.Tab,
]);
await triggerTabRemoved(999);
tabsQuerySpy.mockClear();
messagingService.send.mockClear();
await triggerTabRemoved(123);
expect(tabsQuerySpy).not.toHaveBeenCalled();
expect(messagingService.send).toHaveBeenCalledWith("abandonAutofillPendingNotifications");
});
it("returns early when vault is unlocked", async () => {
activeAccountStatusMock$.next(AuthenticationStatus.Unlocked);
tabsQuerySpy = jest.spyOn(BrowserApi, "tabsQuery").mockResolvedValue([]);
await triggerTabRemoved(1);
expect(tabsQuerySpy).not.toHaveBeenCalled();
expect(messagingService.send).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -45,7 +45,7 @@ import { SecurityTask } from "@bitwarden/common/vault/tasks/models/security-task
// FIXME (PM-22628): Popup imports are forbidden in background
// eslint-disable-next-line no-restricted-imports
import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { AuthPopoutType, openUnlockPopout } from "../../auth/popup/utils/auth-popout-window";
import { BrowserApi } from "../../platform/browser/browser-api";
// FIXME (PM-22628): Popup imports are forbidden in background
// eslint-disable-next-line no-restricted-imports
@@ -89,6 +89,7 @@ export default class NotificationBackground {
ExtensionCommand.AutofillCard,
ExtensionCommand.AutofillIdentity,
]);
private unlockPopoutTabId?: number;
private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = {
bgAdjustNotificationBar: ({ message, sender }) =>
this.handleAdjustNotificationBarMessage(message, sender),
@@ -146,6 +147,7 @@ export default class NotificationBackground {
}
this.setupExtensionMessageListener();
this.setupUnlockPopoutCloseListener();
this.cleanupNotificationQueue();
}
@@ -1163,6 +1165,7 @@ export default class NotificationBackground {
message: NotificationBackgroundExtensionMessage,
sender: chrome.runtime.MessageSender,
): Promise<void> {
this.unlockPopoutTabId = undefined;
const messageData = message.data as LockedVaultPendingNotificationsData;
const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType;
if (this.allowedRetryCommands.has(retryCommand)) {
@@ -1313,4 +1316,43 @@ export default class NotificationBackground {
const tabDomain = Utils.getDomain(tab.url);
return tabDomain === queueMessage.domain || tabDomain === Utils.getDomain(queueMessage.tab.url);
}
private setupUnlockPopoutCloseListener() {
chrome.tabs.onRemoved.addListener(async (tabId: number) => {
await this.handleUnlockPopoutClosed(tabId);
});
}
/**
* If the unlock popout is closed while the vault
* is still locked and there are pending autofill notifications, abandon them.
*/
private async handleUnlockPopoutClosed(removedTabId: number) {
const authStatus = await this.getAuthStatus();
if (authStatus >= AuthenticationStatus.Unlocked) {
this.unlockPopoutTabId = undefined;
return;
}
if (this.unlockPopoutTabId === removedTabId) {
this.unlockPopoutTabId = undefined;
this.messagingService.send("abandonAutofillPendingNotifications");
return;
}
if (this.unlockPopoutTabId) {
return;
}
const extensionUrl = chrome.runtime.getURL("popup/index.html");
const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter(
(tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`),
);
if (unlockPopoutTabs.length === 0) {
this.messagingService.send("abandonAutofillPendingNotifications");
} else if (unlockPopoutTabs[0].id) {
this.unlockPopoutTabId = unlockPopoutTabs[0].id;
}
}
}

View File

@@ -548,7 +548,7 @@ export default class MainBackground {
this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session
this.memoryStorageService = this.memoryStorageForStateProviders;
} else {
this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(this.logService); // mv2 stores to memory
this.memoryStorageService = this.memoryStorageForStateProviders;
}

View File

@@ -256,6 +256,9 @@ export default class RuntimeBackground {
case "addToLockedVaultPendingNotifications":
this.lockedVaultPendingNotifications.push(msg.data);
break;
case "abandonAutofillPendingNotifications":
this.lockedVaultPendingNotifications = [];
break;
case "lockVault":
await this.lockService.lock(msg.userId);
break;

View File

@@ -60,8 +60,8 @@ export class BrowserApi {
}
// Normalize both URLs by removing trailing slashes
const normalizedOrigin = sender.origin.replace(/\/$/, "");
const normalizedExtensionUrl = extensionUrl.replace(/\/$/, "");
const normalizedOrigin = sender.origin.replace(/\/$/, "").toLowerCase();
const normalizedExtensionUrl = extensionUrl.replace(/\/$/, "").toLowerCase();
if (!normalizedOrigin.startsWith(normalizedExtensionUrl)) {
logger?.warning(

View File

@@ -1,6 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LogService } from "@bitwarden/logging";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { BrowserApi } from "../browser/browser-api";
@@ -11,14 +12,14 @@ import { portName } from "./port-name";
export class BackgroundMemoryStorageService extends SerializedMemoryStorageService {
private _ports: chrome.runtime.Port[] = [];
constructor() {
constructor(private readonly logService: LogService) {
super();
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
if (port.name !== portName(chrome.storage.session)) {
return;
}
if (!BrowserApi.senderIsInternal(port.sender)) {
if (!BrowserApi.senderIsInternal(port.sender, this.logService)) {
return;
}

View File

@@ -4,6 +4,9 @@
*/
import { trackEmissions } from "@bitwarden/common/../spec/utils";
import { mock, MockProxy } from "jest-mock-extended";
import { LogService } from "@bitwarden/logging";
import { mockPorts } from "../../../spec/mock-port.spec-util";
@@ -14,11 +17,13 @@ import { ForegroundMemoryStorageService } from "./foreground-memory-storage.serv
describe.skip("foreground background memory storage interaction", () => {
let foreground: ForegroundMemoryStorageService;
let background: BackgroundMemoryStorageService;
let logService: MockProxy<LogService>;
beforeEach(() => {
mockPorts();
logService = mock();
background = new BackgroundMemoryStorageService();
background = new BackgroundMemoryStorageService(logService);
foreground = new ForegroundMemoryStorageService();
});

View File

@@ -16,7 +16,7 @@
<button bitButton type="submit" form="sendForm" buttonType="primary" #submitBtn>
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" popupBackAction>
<button bitButton type="button" buttonType="secondary" [popupBackAction]>
{{ "cancel" | i18n }}
</button>
<button

View File

@@ -1,4 +1,4 @@
<popup-page [loading]="sendsLoading$ | async">
<popup-page [loading]="showSpinnerLoaders$ | async" [hideOverflow]="showSkeletonsLoaders$ | async">
<popup-header slot="header" [pageTitle]="'send' | i18n">
<ng-container slot="end">
<tools-new-send-dropdown *ngIf="!sendsDisabled"></tools-new-send-dropdown>
@@ -6,7 +6,7 @@
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<ng-container slot="above-scroll-area" *ngIf="!(sendsLoading$ | async)">
<ng-container slot="above-scroll-area">
<bit-callout *ngIf="sendsDisabled" [title]="'sendDisabled' | i18n">
{{ "sendDisabledWarning" | i18n }}
</bit-callout>
@@ -34,7 +34,7 @@
</bit-no-items>
</div>
<ng-container *ngIf="listState !== sendState.Empty">
<ng-container *ngIf="listState !== sendState.Empty && !(showSkeletonsLoaders$ | async)">
<div
*ngIf="listState === sendState.NoResults"
class="tw-flex tw-flex-col tw-justify-center tw-h-auto tw-pt-12"
@@ -46,4 +46,9 @@
</div>
<app-send-list-items-container [headerText]="title | i18n" [sends]="sends$ | async" />
</ng-container>
@if (showSkeletonsLoaders$ | async) {
<vault-fade-in-skeleton>
<vault-loading-skeleton></vault-loading-skeleton>
</vault-fade-in-skeleton>
}
</popup-page>

View File

@@ -1,15 +1,18 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { combineLatest, switchMap } from "rxjs";
import { combineLatest, distinctUntilChanged, map, shareReplay, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { NoResults, NoSendsIcon } from "@bitwarden/assets/svg";
import { VaultLoadingSkeletonComponent } from "@bitwarden/browser/vault/popup/components/vault-loading-skeleton/vault-loading-skeleton.component";
import { BrowserPremiumUpgradePromptService } from "@bitwarden/browser/vault/popup/services/browser-premium-upgrade-prompt.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import {
@@ -31,6 +34,7 @@ import { CurrentAccountComponent } from "../../../auth/popup/account-switching/c
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { VaultFadeInOutSkeletonComponent } from "../../../vault/popup/components/vault-fade-in-out-skeleton/vault-fade-in-out-skeleton.component";
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
@@ -64,6 +68,8 @@ export enum SendState {
SendListFiltersComponent,
SendSearchComponent,
TypographyModule,
VaultFadeInOutSkeletonComponent,
VaultLoadingSkeletonComponent,
],
})
export class SendV2Component implements OnDestroy {
@@ -72,7 +78,26 @@ export class SendV2Component implements OnDestroy {
protected listState: SendState | null = null;
protected sends$ = this.sendItemsService.filteredAndSortedSends$;
protected sendsLoading$ = this.sendItemsService.loading$;
private skeletonFeatureFlag$ = this.configService.getFeatureFlag$(
FeatureFlag.VaultLoadingSkeletons,
);
protected sendsLoading$ = this.sendItemsService.loading$.pipe(
distinctUntilChanged(),
shareReplay({ bufferSize: 1, refCount: true }),
);
/** Spinner Loading State */
protected showSpinnerLoaders$ = combineLatest([
this.sendsLoading$,
this.skeletonFeatureFlag$,
]).pipe(map(([loading, skeletonsEnabled]) => loading && !skeletonsEnabled));
/** Skeleton Loading State */
protected showSkeletonsLoaders$ = combineLatest([
this.sendsLoading$,
this.skeletonFeatureFlag$,
]).pipe(map(([loading, skeletonsEnabled]) => loading && skeletonsEnabled));
protected title: string = "allSends";
protected noItemIcon = NoSendsIcon;
protected noResultsIcon = NoResults;
@@ -84,6 +109,7 @@ export class SendV2Component implements OnDestroy {
protected sendListFiltersService: SendListFiltersService,
private policyService: PolicyService,
private accountService: AccountService,
private configService: ConfigService,
) {
combineLatest([
this.sendItemsService.emptyList$,

View File

@@ -23,7 +23,7 @@
>
{{ "exportVault" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" popupBackAction>
<button bitButton type="button" buttonType="secondary" [popupBackAction]>
{{ "cancel" | i18n }}
</button>
</popup-footer>

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "",
"scripts": {
"build": "napi build --platform --js false",
"build": "node scripts/build.js",
"test": "cargo test"
},
"author": "",

View File

@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { execSync } = require('child_process');
const args = process.argv.slice(2);
const isRelease = args.includes('--release');
if (isRelease) {
console.log('Building release mode.');
} else {
console.log('Building debug mode.');
process.env.RUST_LOG = 'debug';
}
execSync(`napi build --platform --js false`, { stdio: 'inherit', env: process.env });

View File

@@ -957,10 +957,7 @@ pub mod logging {
use tracing::Level;
use tracing_subscriber::fmt::format::{DefaultVisitor, Writer};
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
layer::SubscriberExt,
util::SubscriberInitExt,
Layer,
filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt, Layer,
};
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
@@ -1044,9 +1041,17 @@ pub mod logging {
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
let _ = JS_LOGGER.0.set(js_log_fn);
// the log level hierarchy is determined by:
// - if RUST_LOG is detected at runtime
// - if RUST_LOG is provided at compile time
// - default to INFO
let filter = EnvFilter::builder()
// set the default log level to INFO.
.with_default_directive(LevelFilter::INFO.into())
.with_default_directive(
option_env!("RUST_LOG")
.unwrap_or("info")
.parse()
.expect("should provide valid log level at compile time."),
)
// parse directives from the RUST_LOG environment variable,
// overriding the default directive for matching targets.
.from_env_lossy();

View File

@@ -93,8 +93,8 @@
"assignMembersTasksToMonitorProgress": {
"message": "Assign members tasks to monitor progress"
},
"onceYouReviewApps": {
"message": "Once you review applications and mark them as critical, you can assign tasks to members to resolve at-risk items and monitor progress here"
"onceYouReviewApplications": {
"message": "Once you review applications and mark them as critical, assign tasks to your members to change their passwords."
},
"sendReminders": {
"message": "Send reminders"
@@ -178,41 +178,35 @@
}
}
},
"noApplicationsInOrgTitle": {
"message": "No applications found for $ORG NAME$",
"placeholders": {
"org name": {
"content": "$1",
"example": "Company Name"
}
}
"noDataInOrgTitle": {
"message": "No data found"
},
"noApplicationsInOrgDescription": {
"message": "Import your organization's login data to start monitoring credential security risks. Once imported you get to:"
"noDataInOrgDescription": {
"message": "Import your organization's login data to get started with Access Intelligence. Once you do that, you'll be able to:"
},
"benefit1Title": {
"message": "Prioritize risks"
"feature1Title": {
"message": "Mark applications as critical"
},
"benefit1Description": {
"message": "Focus on applications that matter the most"
"feature1Description": {
"message": "This will help you remove risks to your most important applications first."
},
"benefit2Title": {
"message": "Guide remediation"
"feature2Title": {
"message": "Help members improve their security"
},
"benefit2Description": {
"message": "Assign at-risk members guided tasks to rotate at-risk credentials"
"feature2Description": {
"message": "Assign at-risk members guided security tasks to update credentials."
},
"benefit3Title": {
"feature3Title": {
"message": "Monitor progress"
},
"benefit3Description": {
"message": "Track changes over time to show security improvements"
"feature3Description": {
"message": "Track changes over time to show security improvements."
},
"noReportRunTitle": {
"message": "Run your first report to see applications"
"noReportsRunTitle": {
"message": "Generate report"
},
"noReportRunDescription": {
"message": "Generate a risk insights report to analyze your organization's applications and identify at-risk passwords that need attention. Running your first report will:"
"noReportsRunDescription": {
"message": "Youre ready to start generating reports. Once you generate, youll be able to:"
},
"noCriticalApplicationsTitle": {
"message": "You havent marked any applications as critical"
@@ -271,14 +265,14 @@
"atRiskMembers": {
"message": "At-risk members"
},
"membersWithAccessToAtRiskItemsForCriticalApps": {
"message": "Members with access to at-risk items for critical applications"
"membersWithAccessToAtRiskItemsForCriticalApplications": {
"message": "These members have access to vulnerable items for critical applications."
},
"membersWithAtRiskPasswords": {
"message": "Members with at-risk passwords"
},
"membersWillReceiveNotification": {
"message": "Members will receive a notification to resolve at-risk logins through the browser extension."
"membersWillReceiveSecurityTask": {
"message": "Members of your organization will be assigned a task to change vulnerable passwords. Theyll receive a notification within their Bitwarden browser extension."
},
"membersAtRiskCount": {
"message": "$COUNT$ members at-risk",
@@ -307,8 +301,8 @@
}
}
},
"atRiskMembersDescription": {
"message": "These members are logging into applications with weak, exposed, or reused passwords."
"atRiskMemberDescription": {
"message": "These members are logging into critical applications with weak, exposed, or reused passwords."
},
"atRiskMembersDescriptionNone": {
"message": "These are no members logging into applications with weak, exposed, or reused passwords."
@@ -391,14 +385,14 @@
"prioritizeCriticalApplications": {
"message": "Prioritize critical applications"
},
"selectCriticalApplicationsDescription": {
"message": "Select which applications are most critical to your organization, then assign security tasks to members to resolve risks."
"selectCriticalAppsDescription": {
"message": "Select which applications are most critical to your organization. Then, youll be able to assign security tasks to members to remove risks."
},
"reviewNewApplications": {
"message": "Review new applications"
},
"reviewNewApplicationsDescription": {
"message": "We've highlighted at-risk items for new applications stored in Admin console that have weak, exposed, or reused passwords."
"reviewNewAppsDescription": {
"message": "Review new applications with vulnerable items and mark those youd like to monitor closely as critical. Then, youll be able to assign security tasks to members to remove risks."
},
"clickIconToMarkAppAsCritical": {
"message": "Click the star icon to mark an app as critical"
@@ -9860,8 +9854,8 @@
"assignTasks": {
"message": "Assign tasks"
},
"assignTasksToMembers": {
"message": "Assign tasks to members for guided resolution"
"assignSecurityTasksToMembers": {
"message": "Send notifications to change passwords"
},
"assignToCollections": {
"message": "Assign to collections"

View File

@@ -12,7 +12,7 @@
</div>
<div class="tw-items-baseline tw-gap-2">
<span bitTypography="body2">{{ "onceYouReviewApps" | i18n }}</span>
<span bitTypography="body2">{{ "onceYouReviewApplications" | i18n }}</span>
</div>
}

View File

@@ -14,7 +14,7 @@
<dirt-activity-card
[title]="'atRiskMembers' | i18n"
[cardMetrics]="'membersAtRiskCount' | i18n: totalCriticalAppsAtRiskMemberCount"
[metricDescription]="'membersWithAccessToAtRiskItemsForCriticalApps' | i18n"
[metricDescription]="'membersWithAccessToAtRiskItemsForCriticalApplications' | i18n"
actionText="{{ 'viewAtRiskMembers' | i18n }}"
[showActionLink]="totalCriticalAppsAtRiskMemberCount > 0"
(actionClick)="onViewAtRiskMembers()"

View File

@@ -71,7 +71,7 @@
<!-- Description Text -->
<div bitTypography="helper" class="tw-text-muted">
{{ "membersWillReceiveNotification" | i18n }}
{{ "membersWillReceiveSecurityTask" | i18n }}
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@
? hasNoCriticalApplications()
? ("prioritizeCriticalApplications" | i18n)
: ("reviewNewApplications" | i18n)
: ("assignTasksToMembers" | i18n)
: ("assignSecurityTasksToMembers" | i18n)
}}
</span>
@@ -15,8 +15,8 @@
<p bitTypography="body1" class="tw-mb-5">
{{
hasNoCriticalApplications()
? ("selectCriticalApplicationsDescription" | i18n)
: ("reviewNewApplicationsDescription" | i18n)
? ("selectCriticalAppsDescription" | i18n)
: ("reviewNewAppsDescription" | i18n)
}}
</p>

View File

@@ -16,8 +16,8 @@
<!-- Show Empty state when there are no applications (no ciphers to make reports on) -->
<empty-state-card
[videoSrc]="emptyStateVideoSrc"
[title]="this.i18nService.t('noApplicationsInOrgTitle', organizationName)"
[description]="this.i18nService.t('noApplicationsInOrgDescription')"
[title]="this.i18nService.t('noDataInOrgTitle')"
[description]="this.i18nService.t('noDataInOrgDescription')"
[benefits]="emptyStateBenefits"
[buttonText]="this.i18nService.t('importData')"
[buttonIcon]="IMPORT_ICON"
@@ -27,8 +27,8 @@
<!-- Show empty state for no reports run -->
<empty-state-card
[videoSrc]="emptyStateVideoSrc"
[title]="this.i18nService.t('noReportRunTitle')"
[description]="this.i18nService.t('noReportRunDescription')"
[title]="this.i18nService.t('noReportsRunTitle')"
[description]="this.i18nService.t('noReportsRunDescription')"
[benefits]="emptyStateBenefits"
[buttonText]="this.i18nService.t('riskInsightsRunReport')"
[buttonIcon]=""

View File

@@ -10,7 +10,7 @@ import {
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, EMPTY, firstValueFrom } from "rxjs";
import { EMPTY, firstValueFrom } from "rxjs";
import { distinctUntilChanged, map, tap } from "rxjs/operators";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -84,14 +84,11 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
dataLastUpdated: Date | null = null;
// Empty state properties
protected organizationName = "";
// Empty state computed properties
protected emptyStateBenefits: [string, string][] = [
[this.i18nService.t("benefit1Title"), this.i18nService.t("benefit1Description")],
[this.i18nService.t("benefit2Title"), this.i18nService.t("benefit2Description")],
[this.i18nService.t("benefit3Title"), this.i18nService.t("benefit3Description")],
[this.i18nService.t("feature1Title"), this.i18nService.t("feature1Description")],
[this.i18nService.t("feature2Title"), this.i18nService.t("feature2Description")],
[this.i18nService.t("feature3Title"), this.i18nService.t("feature3Description")],
];
protected emptyStateVideoSrc: string | null = "/videos/risk-insights-mark-as-critical.mp4";
@@ -140,17 +137,14 @@ export class RiskInsightsComponent implements OnInit, OnDestroy {
)
.subscribe();
// Combine report data, vault items check, organization details, and generation state
// Subscribe to report data updates
// This declarative pattern ensures proper cleanup and prevents memory leaks
combineLatest([this.dataService.enrichedReportData$, this.dataService.organizationDetails$])
this.dataService.enrichedReportData$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([report, orgDetails]) => {
.subscribe((report) => {
// Update report state
this.appsCount = report?.reportData.length ?? 0;
this.dataLastUpdated = report?.creationDate ?? null;
// Update organization name
this.organizationName = orgDetails?.organizationName ?? "";
});
// Subscribe to drawer state changes

View File

@@ -8,7 +8,7 @@
<ng-container bitDialogContent>
<span bitTypography="body1" class="tw-text-muted tw-text-sm">{{
(drawerDetails.atRiskMemberDetails?.length > 0
? "atRiskMembersDescription"
? "atRiskMemberDescription"
: "atRiskMembersDescriptionNone"
) | i18n
}}</span>

View File

@@ -13,7 +13,7 @@ export abstract class SendTokenService {
/**
* Attempts to retrieve a {@link SendAccessToken} for the given sendId.
* If the access token is found in session storage and is not expired, then it returns the token.
* If the access token is expired, then it returns a {@link TryGetSendAccessTokenError} expired error.
* If the access token found in session storage is expired, then it returns a {@link TryGetSendAccessTokenError} expired error and clears the token from storage so that a subsequent call can attempt to retrieve a new token.
* If an access token is not found in storage, then it attempts to retrieve it from the server (will succeed for sends that don't require any credentials to view).
* If the access token is successfully retrieved from the server, then it stores the token in session storage and returns it.
* If an access token cannot be granted b/c the send requires credentials, then it returns a {@link TryGetSendAccessTokenError} indicating which credentials are required.

View File

@@ -1,6 +1,3 @@
<bit-callout type="info" *ngIf="importBlockedByPolicy">
{{ "personalOwnershipPolicyInEffectImports" | i18n }}
</bit-callout>
<bit-callout
[title]="'restrictCardTypeImport' | i18n"
type="info"