mirror of
https://github.com/bitwarden/browser
synced 2026-02-12 22:44:11 +00:00
Merge branch 'main' into mer/browser-refresh/beta-3
This commit is contained in:
@@ -4888,5 +4888,14 @@
|
||||
},
|
||||
"beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"extensionWidth": {
|
||||
"message": "Extension width"
|
||||
},
|
||||
"wide": {
|
||||
"message": "Wide"
|
||||
},
|
||||
"extraWide": {
|
||||
"message": "Extra wide"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
import { ScrollOptions } from "./abstractions/browser-popup-utils.abstractions";
|
||||
import { PopupWidthOptions } from "./layout/popup-width.service";
|
||||
|
||||
class BrowserPopupUtils {
|
||||
/**
|
||||
@@ -108,7 +109,7 @@ class BrowserPopupUtils {
|
||||
const defaultPopoutWindowOptions: chrome.windows.CreateData = {
|
||||
type: "popup",
|
||||
focused: true,
|
||||
width: 380,
|
||||
width: Math.max(PopupWidthOptions.default, document.body.clientWidth),
|
||||
height: 630,
|
||||
};
|
||||
const offsetRight = 15;
|
||||
|
||||
@@ -514,3 +514,25 @@ export const TransparentHeader: Story = {
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WidthOptions: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /* HTML */ `
|
||||
<div class="tw-flex tw-flex-col tw-gap-4 tw-text-main">
|
||||
<div>Default:</div>
|
||||
<div class="tw-h-[640px] tw-w-[380px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<mock-vault-page></mock-vault-page>
|
||||
</div>
|
||||
<div>Wide:</div>
|
||||
<div class="tw-h-[640px] tw-w-[480px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<mock-vault-page></mock-vault-page>
|
||||
</div>
|
||||
<div>Extra wide:</div>
|
||||
<div class="tw-h-[640px] tw-w-[600px] tw-border tw-border-solid tw-border-secondary-300">
|
||||
<mock-vault-page></mock-vault-page>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
GlobalStateProvider,
|
||||
KeyDefinition,
|
||||
POPUP_STYLE_DISK,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
/**
|
||||
*
|
||||
* Value represents width in pixels
|
||||
*/
|
||||
export const PopupWidthOptions = Object.freeze({
|
||||
default: 380,
|
||||
wide: 480,
|
||||
"extra-wide": 600,
|
||||
});
|
||||
|
||||
type PopupWidthOptions = typeof PopupWidthOptions;
|
||||
export type PopupWidthOption = keyof PopupWidthOptions;
|
||||
|
||||
const POPUP_WIDTH_KEY_DEF = new KeyDefinition<PopupWidthOption>(POPUP_STYLE_DISK, "popup-width", {
|
||||
deserializer: (s) => s,
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the extension popup width based on a user setting
|
||||
**/
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class PopupWidthService {
|
||||
private static readonly LocalStorageKey = "bw-popup-width";
|
||||
private readonly state = inject(GlobalStateProvider).get(POPUP_WIDTH_KEY_DEF);
|
||||
|
||||
readonly width$: Observable<PopupWidthOption> = this.state.state$.pipe(
|
||||
map((state) => state ?? "default"),
|
||||
);
|
||||
|
||||
async setWidth(width: PopupWidthOption) {
|
||||
await this.state.update(() => width);
|
||||
}
|
||||
|
||||
/** Begin listening for state changes */
|
||||
init() {
|
||||
this.width$.subscribe((width: PopupWidthOption) => {
|
||||
PopupWidthService.setStyle(width);
|
||||
localStorage.setItem(PopupWidthService.LocalStorageKey, width);
|
||||
});
|
||||
}
|
||||
|
||||
private static setStyle(width: PopupWidthOption) {
|
||||
const pxWidth = PopupWidthOptions[width] ?? PopupWidthOptions.default;
|
||||
document.body.style.width = `${pxWidth}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* To keep the popup size from flickering on bootstrap, we store the width in `localStorage` so we can quickly & synchronously reference it.
|
||||
**/
|
||||
static initBodyWidthFromLocalStorage() {
|
||||
const storedValue = localStorage.getItem(PopupWidthService.LocalStorageKey);
|
||||
this.setStyle(storedValue as any);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
|
||||
import { PopupWidthService } from "../platform/popup/layout/popup-width.service";
|
||||
import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service";
|
||||
import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service";
|
||||
import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service";
|
||||
@@ -44,6 +45,7 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
private viewCacheService = inject(PopupViewCacheService);
|
||||
private compactModeService = inject(PopupCompactModeService);
|
||||
private widthService = inject(PopupWidthService);
|
||||
|
||||
private lastActivity: Date;
|
||||
private activeUserId: UserId;
|
||||
@@ -99,6 +101,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.viewCacheService.init();
|
||||
|
||||
this.compactModeService.init();
|
||||
this.widthService.init();
|
||||
|
||||
// Component states must not persist between closing and reopening the popup, otherwise they become dead objects
|
||||
// Clear them aggressively to make sure this doesn't occur
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { enableProdMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
import { PopupWidthService } from "../platform/popup/layout/popup-width.service";
|
||||
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||
|
||||
require("./scss/popup.scss");
|
||||
@@ -8,7 +9,8 @@ require("./scss/tailwind.css");
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
// We put this first to minimize the delay in window changing.
|
||||
// We put these first to minimize the delay in window changing.
|
||||
PopupWidthService.initBodyWidthFromLocalStorage();
|
||||
// Should be removed once we deprecate support for Safari 16.0 and older. See Jira ticket [PM-1861]
|
||||
if (BrowserPlatformUtilsService.shouldApplySafariHeightFix(window)) {
|
||||
document.documentElement.classList.add("safari_height_fix");
|
||||
|
||||
@@ -19,7 +19,7 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
width: 380px !important;
|
||||
min-width: 380px !important;
|
||||
height: 600px !important;
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
||||
83
apps/browser/src/services/families-policy.service.spec.ts
Normal file
83
apps/browser/src/services/families-policy.service.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
|
||||
import { FamiliesPolicyService } from "./families-policy.service"; // Adjust the import as necessary
|
||||
|
||||
describe("FamiliesPolicyService", () => {
|
||||
let service: FamiliesPolicyService;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
|
||||
beforeEach(() => {
|
||||
organizationService = mock<OrganizationService>();
|
||||
policyService = mock<PolicyService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
FamiliesPolicyService,
|
||||
{ provide: OrganizationService, useValue: organizationService },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(FamiliesPolicyService);
|
||||
});
|
||||
|
||||
it("should return false when there are no enterprise organizations", async () => {
|
||||
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(false));
|
||||
|
||||
const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$());
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when the policy is enabled for the one enterprise organization", async () => {
|
||||
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true));
|
||||
|
||||
const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[];
|
||||
organizationService.getAll$.mockReturnValue(of(organizations));
|
||||
|
||||
const policies = [{ organizationId: "org1", enabled: true }] as Policy[];
|
||||
policyService.getAll$.mockReturnValue(of(policies));
|
||||
|
||||
const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when the policy is not enabled for the one enterprise organization", async () => {
|
||||
jest.spyOn(service, "hasSingleEnterpriseOrg$").mockReturnValue(of(true));
|
||||
|
||||
const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[];
|
||||
organizationService.getAll$.mockReturnValue(of(organizations));
|
||||
|
||||
const policies = [{ organizationId: "org1", enabled: false }] as Policy[];
|
||||
policyService.getAll$.mockReturnValue(of(policies));
|
||||
|
||||
const result = await firstValueFrom(service.isFreeFamilyPolicyEnabled$());
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when there is exactly one enterprise organization that can manage sponsorships", async () => {
|
||||
const organizations = [{ id: "org1", canManageSponsorships: true }] as Organization[];
|
||||
organizationService.getAll$.mockReturnValue(of(organizations));
|
||||
|
||||
const result = await firstValueFrom(service.hasSingleEnterpriseOrg$());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when there are multiple organizations that can manage sponsorships", async () => {
|
||||
const organizations = [
|
||||
{ id: "org1", canManageSponsorships: true },
|
||||
{ id: "org2", canManageSponsorships: true },
|
||||
] as Organization[];
|
||||
organizationService.getAll$.mockReturnValue(of(organizations));
|
||||
|
||||
const result = await firstValueFrom(service.hasSingleEnterpriseOrg$());
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
54
apps/browser/src/services/families-policy.service.ts
Normal file
54
apps/browser/src/services/families-policy.service.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { map, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class FamiliesPolicyService {
|
||||
constructor(
|
||||
private policyService: PolicyService,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
hasSingleEnterpriseOrg$(): Observable<boolean> {
|
||||
// Retrieve all organizations the user is part of
|
||||
return this.organizationService.getAll$().pipe(
|
||||
map((organizations) => {
|
||||
// Filter to only those organizations that can manage sponsorships
|
||||
const sponsorshipOrgs = organizations.filter((org) => org.canManageSponsorships);
|
||||
|
||||
// Check if there is exactly one organization that can manage sponsorships.
|
||||
// This is important because users that are part of multiple organizations
|
||||
// may always access free bitwarden family menu. We want to restrict access
|
||||
// to the policy only when there is a single enterprise organization and the free family policy is turn.
|
||||
return sponsorshipOrgs.length === 1;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
isFreeFamilyPolicyEnabled$(): Observable<boolean> {
|
||||
return this.hasSingleEnterpriseOrg$().pipe(
|
||||
switchMap((hasSingleEnterpriseOrg) => {
|
||||
if (!hasSingleEnterpriseOrg) {
|
||||
return of(false);
|
||||
}
|
||||
return this.organizationService.getAll$().pipe(
|
||||
map((organizations) => organizations.find((org) => org.canManageSponsorships)?.id),
|
||||
switchMap((enterpriseOrgId) =>
|
||||
this.policyService
|
||||
.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy)
|
||||
.pipe(
|
||||
map(
|
||||
(policies) =>
|
||||
policies.find((policy) => policy.organizationId === enterpriseOrgId)?.enabled ??
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,12 @@
|
||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-item>
|
||||
<bit-item *ngIf="familySponsorshipAvailable$ | async">
|
||||
<bit-item
|
||||
*ngIf="
|
||||
(familySponsorshipAvailable$ | async) &&
|
||||
!((isFreeFamilyPolicyEnabled$ | async) && (hasSingleEnterpriseOrg$ | async))
|
||||
"
|
||||
>
|
||||
<button type="button" bit-item-content (click)="openFreeBitwardenFamiliesPage()">
|
||||
{{ "freeBitwardenFamilies" | i18n }}
|
||||
<i slot="end" class="bwi bwi-external-link" aria-hidden="true"></i>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
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 { FamiliesPolicyService } from "../../../../services/families-policy.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "more-from-bitwarden-page-v2.component.html",
|
||||
@@ -30,15 +31,20 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
||||
export class MoreFromBitwardenPageV2Component {
|
||||
canAccessPremium$: Observable<boolean>;
|
||||
protected familySponsorshipAvailable$: Observable<boolean>;
|
||||
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
|
||||
protected hasSingleEnterpriseOrg$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private environmentService: EnvironmentService,
|
||||
private organizationService: OrganizationService,
|
||||
private familiesPolicyService: FamiliesPolicyService,
|
||||
) {
|
||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||
this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$;
|
||||
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
|
||||
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
|
||||
}
|
||||
|
||||
async openFreeBitwardenFamiliesPage() {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="openFreeBitwardenFamiliesPage()"
|
||||
*ngIf="!((isFreeFamilyPolicyEnabled$ | async) && (hasSingleEnterpriseOrg$ | async))"
|
||||
>
|
||||
<div class="row-main">{{ "freeBitwardenFamilies" | i18n }}</div>
|
||||
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||
import { FamiliesPolicyService } from "../../../../services/families-policy.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "more-from-bitwarden-page.component.html",
|
||||
@@ -18,13 +19,18 @@ import { PopOutComponent } from "../../../../platform/popup/components/pop-out.c
|
||||
})
|
||||
export class MoreFromBitwardenPageComponent {
|
||||
canAccessPremium$: Observable<boolean>;
|
||||
protected isFreeFamilyPolicyEnabled$: Observable<boolean>;
|
||||
protected hasSingleEnterpriseOrg$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private environmentService: EnvironmentService,
|
||||
private familiesPolicyService: FamiliesPolicyService,
|
||||
) {
|
||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||
this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$();
|
||||
this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$();
|
||||
}
|
||||
|
||||
async openFreeBitwardenFamiliesPage() {
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "extensionWidth" | i18n }}</bit-label>
|
||||
<bit-select formControlName="width" [items]="widthOptions"></bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="enableCompactMode" type="checkbox" />
|
||||
<bit-label
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat
|
||||
import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
import { PopupWidthService } from "../../../platform/popup/layout/popup-width.service";
|
||||
|
||||
import { AppearanceV2Component } from "./appearance-v2.component";
|
||||
|
||||
@@ -51,6 +52,11 @@ describe("AppearanceV2Component", () => {
|
||||
const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined);
|
||||
const setEnableCompactMode = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const mockWidthService: Partial<PopupWidthService> = {
|
||||
width$: new BehaviorSubject("default"),
|
||||
setWidth: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
setSelectedTheme.mockClear();
|
||||
setShowFavicons.mockClear();
|
||||
@@ -78,6 +84,10 @@ describe("AppearanceV2Component", () => {
|
||||
provide: PopupCompactModeService,
|
||||
useValue: { enabled$: enableCompactMode$, setEnabled: setEnableCompactMode },
|
||||
},
|
||||
{
|
||||
provide: PopupWidthService,
|
||||
useValue: mockWidthService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(AppearanceV2Component, {
|
||||
@@ -102,6 +112,7 @@ describe("AppearanceV2Component", () => {
|
||||
enableBadgeCounter: true,
|
||||
theme: ThemeType.Nord,
|
||||
enableCompactMode: false,
|
||||
width: "default",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { BadgeModule, CheckboxModule } from "@bitwarden/components";
|
||||
import { BadgeModule, CheckboxModule, Option } from "@bitwarden/components";
|
||||
|
||||
import { CardComponent } from "../../../../../../libs/components/src/card/card.component";
|
||||
import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module";
|
||||
@@ -21,6 +21,10 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
|
||||
import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
import {
|
||||
PopupWidthOption,
|
||||
PopupWidthService,
|
||||
} from "../../../platform/popup/layout/popup-width.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -41,6 +45,8 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
||||
})
|
||||
export class AppearanceV2Component implements OnInit {
|
||||
private compactModeService = inject(PopupCompactModeService);
|
||||
private popupWidthService = inject(PopupWidthService);
|
||||
private i18nService = inject(I18nService);
|
||||
|
||||
appearanceForm = this.formBuilder.group({
|
||||
enableFavicon: false,
|
||||
@@ -48,6 +54,7 @@ export class AppearanceV2Component implements OnInit {
|
||||
theme: ThemeType.System,
|
||||
enableAnimations: true,
|
||||
enableCompactMode: false,
|
||||
width: "default" as PopupWidthOption,
|
||||
});
|
||||
|
||||
/** To avoid flashes of inaccurate values, only show the form after the entire form is populated. */
|
||||
@@ -56,6 +63,13 @@ export class AppearanceV2Component implements OnInit {
|
||||
/** Available theme options */
|
||||
themeOptions: { name: string; value: ThemeType }[];
|
||||
|
||||
/** Available width options */
|
||||
protected readonly widthOptions: Option<PopupWidthOption>[] = [
|
||||
{ label: this.i18nService.t("default"), value: "default" },
|
||||
{ label: this.i18nService.t("wide"), value: "wide" },
|
||||
{ label: this.i18nService.t("extraWide"), value: "extra-wide" },
|
||||
];
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
@@ -81,6 +95,7 @@ export class AppearanceV2Component implements OnInit {
|
||||
this.animationControlService.enableRoutingAnimation$,
|
||||
);
|
||||
const enableCompactMode = await firstValueFrom(this.compactModeService.enabled$);
|
||||
const width = await firstValueFrom(this.popupWidthService.width$);
|
||||
|
||||
// Set initial values for the form
|
||||
this.appearanceForm.setValue({
|
||||
@@ -89,6 +104,7 @@ export class AppearanceV2Component implements OnInit {
|
||||
theme,
|
||||
enableAnimations,
|
||||
enableCompactMode,
|
||||
width,
|
||||
});
|
||||
|
||||
this.formLoading = false;
|
||||
@@ -122,6 +138,12 @@ export class AppearanceV2Component implements OnInit {
|
||||
.subscribe((enableCompactMode) => {
|
||||
void this.updateCompactMode(enableCompactMode);
|
||||
});
|
||||
|
||||
this.appearanceForm.controls.width.valueChanges
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((width) => {
|
||||
void this.updateWidth(width);
|
||||
});
|
||||
}
|
||||
|
||||
async updateFavicon(enableFavicon: boolean) {
|
||||
@@ -144,4 +166,8 @@ export class AppearanceV2Component implements OnInit {
|
||||
async updateCompactMode(enableCompactMode: boolean) {
|
||||
await this.compactModeService.setEnabled(enableCompactMode);
|
||||
}
|
||||
|
||||
async updateWidth(width: PopupWidthOption) {
|
||||
await this.popupWidthService.setWidth(width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"papaparse": "5.4.1",
|
||||
"proper-lockfile": "4.1.2",
|
||||
"rxjs": "7.8.1",
|
||||
"tldts": "6.1.61",
|
||||
"tldts": "6.1.64",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ ssh-key = { version = "=0.6.6", default-features = false, features = [
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", branch = "km/pm-10098/clean-russh-implementation" }
|
||||
tokio = { version = "=1.40.0", features = ["io-util", "sync", "macros", "net"] }
|
||||
tokio-stream = { version = "=0.1.15", features = ["net"] }
|
||||
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||
tokio-util = { version = "=0.7.12", features = ["codec"] }
|
||||
thiserror = "=1.0.69"
|
||||
typenum = "=1.17.0"
|
||||
rand_chacha = "=0.3.1"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.9.0",
|
||||
"@types/node": "22.9.3",
|
||||
"@types/node-ipc": "9.2.3",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
@@ -106,9 +106,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
||||
"version": "22.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.3.tgz",
|
||||
"integrity": "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.8"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.9.0",
|
||||
"@types/node": "22.9.3",
|
||||
"@types/node-ipc": "9.2.3",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2024.11.1",
|
||||
"version": "2024.11.2",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -62,13 +62,6 @@ const routes: Routes = [
|
||||
(m) => m.OrganizationReportingModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "access-intelligence",
|
||||
loadChildren: () =>
|
||||
import("../../tools/access-intelligence/access-intelligence.module").then(
|
||||
(m) => m.AccessIntelligenceModule,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "billing",
|
||||
loadChildren: () =>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "risk-insights",
|
||||
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
|
||||
component: RiskInsightsComponent,
|
||||
data: {
|
||||
titleId: "RiskInsights",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AccessIntelligenceRoutingModule {}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
|
||||
import { RiskInsightsComponent } from "./risk-insights.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule],
|
||||
})
|
||||
export class AccessIntelligenceModule {}
|
||||
@@ -1,114 +0,0 @@
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
|
||||
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
|
||||
<ng-container slot="title">
|
||||
<h2 class="tw-font-semibold mt-4">
|
||||
{{ "noAppsInOrgTitle" | i18n: organization.name }}
|
||||
</h2>
|
||||
</ng-container>
|
||||
<ng-container slot="description">
|
||||
<div class="tw-flex tw-flex-col tw-mb-2">
|
||||
<span class="tw-text-muted">
|
||||
{{ "noAppsInOrgDescription" | i18n }}
|
||||
</span>
|
||||
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container slot="button">
|
||||
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
|
||||
{{ "createNewLoginItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="mockAtRiskMembersCount"
|
||||
[maxValue]="mockTotalMembersCount"
|
||||
>
|
||||
</tools-card>
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="mockAtRiskAppsCount"
|
||||
[maxValue]="mockTotalAppsCount"
|
||||
>
|
||||
</tools-card>
|
||||
</div>
|
||||
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
|
||||
<bit-search
|
||||
[placeholder]="'searchApps' | i18n"
|
||||
class="tw-grow"
|
||||
[formControl]="searchControl"
|
||||
></bit-search>
|
||||
<button
|
||||
class="tw-rounded-lg"
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
*ngIf="isCritialAppsFeatureEnabled"
|
||||
[disabled]="!selectedIds.size"
|
||||
[loading]="markingAsCritical"
|
||||
(click)="markAppsAsCritical()"
|
||||
>
|
||||
<i class="bwi bwi-star-f tw-mr-2"></i>
|
||||
{{ "markAppAsCritical" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th *ngIf="isCritialAppsFeatureEnabled"></th>
|
||||
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
|
||||
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
|
||||
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
|
||||
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
|
||||
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
|
||||
<td *ngIf="isCritialAppsFeatureEnabled">
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
[checked]="selectedIds.has(r.id)"
|
||||
(change)="onCheckboxChange(r.id, $event)"
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>{{ r.name }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.atRiskPasswords }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.totalPasswords }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.atRiskMembers }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell data-testid="total-membership">
|
||||
{{ r.totalMembers }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
@@ -1,126 +0,0 @@
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { debounceTime, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
Icons,
|
||||
NoItemsModule,
|
||||
SearchModule,
|
||||
TableDataSource,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { CardComponent } from "@bitwarden/tools-card";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { applicationTableMockData } from "./application-table.mock";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "tools-all-applications",
|
||||
templateUrl: "./all-applications.component.html",
|
||||
imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule],
|
||||
})
|
||||
export class AllApplicationsComponent implements OnInit {
|
||||
protected dataSource = new TableDataSource<any>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
private destroyRef = inject(DestroyRef);
|
||||
protected loading = false;
|
||||
protected organization: Organization;
|
||||
noItemsIcon = Icons.Security;
|
||||
protected markingAsCritical = false;
|
||||
isCritialAppsFeatureEnabled = false;
|
||||
|
||||
// MOCK DATA
|
||||
protected mockData = applicationTableMockData;
|
||||
protected mockAtRiskMembersCount = 0;
|
||||
protected mockAtRiskAppsCount = 0;
|
||||
protected mockTotalMembersCount = 0;
|
||||
protected mockTotalAppsCount = 0;
|
||||
|
||||
async ngOnInit() {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(async (params) => {
|
||||
const organizationId = params.get("organizationId");
|
||||
this.organization = await firstValueFrom(this.organizationService.get$(organizationId));
|
||||
// TODO: use organizationId to fetch data
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.CriticalApps,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected auditService: AuditService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected toastService: ToastService,
|
||||
protected organizationService: OrganizationService,
|
||||
protected configService: ConfigService,
|
||||
) {
|
||||
this.dataSource.data = applicationTableMockData;
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((v) => (this.dataSource.filter = v));
|
||||
}
|
||||
|
||||
goToCreateNewLoginItem = async () => {
|
||||
// TODO: implement
|
||||
this.toastService.showToast({
|
||||
variant: "warning",
|
||||
title: null,
|
||||
message: "Not yet implemented",
|
||||
});
|
||||
};
|
||||
|
||||
markAppsAsCritical = async () => {
|
||||
// TODO: Send to API once implemented
|
||||
this.markingAsCritical = true;
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.selectedIds.clear();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("appsMarkedAsCritical"),
|
||||
});
|
||||
resolve(true);
|
||||
this.markingAsCritical = false;
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
trackByFunction(_: number, item: CipherView) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
onCheckboxChange(id: number, event: Event) {
|
||||
const isChecked = (event.target as HTMLInputElement).checked;
|
||||
if (isChecked) {
|
||||
this.selectedIds.add(id);
|
||||
} else {
|
||||
this.selectedIds.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
export const applicationTableMockData = [
|
||||
{
|
||||
id: 1,
|
||||
name: "google.com",
|
||||
atRiskPasswords: 4,
|
||||
totalPasswords: 10,
|
||||
atRiskMembers: 2,
|
||||
totalMembers: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "facebook.com",
|
||||
atRiskPasswords: 3,
|
||||
totalPasswords: 8,
|
||||
atRiskMembers: 1,
|
||||
totalMembers: 3,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "twitter.com",
|
||||
atRiskPasswords: 2,
|
||||
totalPasswords: 6,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 2,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "linkedin.com",
|
||||
atRiskPasswords: 1,
|
||||
totalPasswords: 4,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 1,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "instagram.com",
|
||||
atRiskPasswords: 0,
|
||||
totalPasswords: 2,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 0,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "tiktok.com",
|
||||
atRiskPasswords: 0,
|
||||
totalPasswords: 1,
|
||||
atRiskMembers: 0,
|
||||
totalMembers: 0,
|
||||
},
|
||||
];
|
||||
@@ -1,99 +0,0 @@
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
|
||||
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
|
||||
<ng-container slot="title">
|
||||
<h2 class="tw-font-semibold mt-4">
|
||||
{{ "noCriticalAppsTitle" | i18n }}
|
||||
</h2>
|
||||
</ng-container>
|
||||
<ng-container slot="description">
|
||||
<p class="tw-text-muted">
|
||||
{{ "noCriticalAppsDescription" | i18n }}
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container slot="button">
|
||||
<button (click)="goToAllAppsTab()" bitButton buttonType="primary" type="button">
|
||||
{{ "markCriticalApps" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<div class="tw-flex tw-justify-between tw-mb-4">
|
||||
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
|
||||
<button bitButton buttonType="primary" type="button">
|
||||
<i class="bwi bwi-envelope tw-mr-2"></i>
|
||||
{{ "requestPasswordChange" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-flex tw-gap-6">
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskMembers' | i18n"
|
||||
[value]="mockAtRiskMembersCount"
|
||||
[maxValue]="mockTotalMembersCount"
|
||||
>
|
||||
</tools-card>
|
||||
<tools-card
|
||||
class="tw-flex-1"
|
||||
[title]="'atRiskApplications' | i18n"
|
||||
[value]="mockAtRiskAppsCount"
|
||||
[maxValue]="mockTotalAppsCount"
|
||||
>
|
||||
</tools-card>
|
||||
</div>
|
||||
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
|
||||
<bit-search
|
||||
[placeholder]="'searchApps' | i18n"
|
||||
class="tw-grow"
|
||||
[formControl]="searchControl"
|
||||
></bit-search>
|
||||
</div>
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
|
||||
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
|
||||
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
|
||||
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
|
||||
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td>
|
||||
<i class="bwi bwi-star-f"></i>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>{{ r.name }}</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.atRiskPasswords }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.totalPasswords }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<span>
|
||||
{{ r.atRiskMembers }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell data-testid="total-membership">
|
||||
{{ r.totalMembers }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { debounceTime, map } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components";
|
||||
import { CardComponent } from "@bitwarden/tools-card";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { applicationTableMockData } from "./application-table.mock";
|
||||
import { RiskInsightsTabType } from "./risk-insights.component";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "tools-critical-applications",
|
||||
templateUrl: "./critical-applications.component.html",
|
||||
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
|
||||
})
|
||||
export class CriticalApplicationsComponent implements OnInit {
|
||||
protected dataSource = new TableDataSource<any>();
|
||||
protected selectedIds: Set<number> = new Set<number>();
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
private destroyRef = inject(DestroyRef);
|
||||
protected loading = false;
|
||||
protected organizationId: string;
|
||||
noItemsIcon = Icons.Security;
|
||||
// MOCK DATA
|
||||
protected mockData = applicationTableMockData;
|
||||
protected mockAtRiskMembersCount = 0;
|
||||
protected mockAtRiskAppsCount = 0;
|
||||
protected mockTotalMembersCount = 0;
|
||||
protected mockTotalAppsCount = 0;
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(async (params) => {
|
||||
this.organizationId = params.get("organizationId");
|
||||
// TODO: use organizationId to fetch data
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
goToAllAppsTab = async () => {
|
||||
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
|
||||
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected router: Router,
|
||||
) {
|
||||
this.dataSource.data = []; //applicationTableMockData;
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((v) => (this.dataSource.filter = v));
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- <bit-table [dataSource]="dataSource"> -->
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "member" | i18n }}</th>
|
||||
<th bitCell>{{ "atRiskPasswords" | i18n }}</th>
|
||||
<th bitCell>{{ "totalPasswords" | i18n }}</th>
|
||||
<th bitCell>{{ "atRiskApplications" | i18n }}</th>
|
||||
<th bitCell>{{ "totalApplications" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<!-- </bit-table> -->
|
||||
@@ -1,19 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { TableDataSource, TableModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "tools-notified-members-table",
|
||||
templateUrl: "./notified-members-table.component.html",
|
||||
imports: [CommonModule, JslibModule, TableModule],
|
||||
})
|
||||
export class NotifiedMembersTableComponent {
|
||||
dataSource = new TableDataSource<any>();
|
||||
|
||||
constructor() {
|
||||
this.dataSource.data = [];
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<bit-container>
|
||||
<p>{{ "passwordsReportDesc" | i18n }}</p>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!loading">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell bitSortable="hostURI">{{ "application" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>
|
||||
<ng-container>
|
||||
<span>{{ r.hostURI }}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span
|
||||
bitBadge
|
||||
*ngIf="passwordStrengthMap.has(r.id)"
|
||||
[variant]="passwordStrengthMap.get(r.id)[1]"
|
||||
>
|
||||
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
|
||||
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right" data-testid="total-membership">
|
||||
{{ totalMembersMap.get(r.id) || 0 }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
</bit-container>
|
||||
@@ -1,73 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, convertToParamMap } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
MemberCipherDetailsApiService,
|
||||
PasswordHealthService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TableModule } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component";
|
||||
|
||||
describe("PasswordHealthMembersUriComponent", () => {
|
||||
let component: PasswordHealthMembersURIComponent;
|
||||
let fixture: ComponentFixture<PasswordHealthMembersURIComponent>;
|
||||
let cipherServiceMock: MockProxy<CipherService>;
|
||||
const passwordHealthServiceMock = mock<PasswordHealthService>();
|
||||
|
||||
const activeRouteParams = convertToParamMap({ organizationId: "orgId" });
|
||||
|
||||
beforeEach(async () => {
|
||||
cipherServiceMock = mock<CipherService>();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PasswordHealthMembersURIComponent, PipesModule, TableModule, LooseComponentsModule],
|
||||
providers: [
|
||||
{ provide: CipherService, useValue: cipherServiceMock },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: AuditService, useValue: mock<AuditService>() },
|
||||
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||
{
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useValue: mock<PasswordStrengthServiceAbstraction>(),
|
||||
},
|
||||
{ provide: PasswordHealthService, useValue: passwordHealthServiceMock },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
paramMap: of(activeRouteParams),
|
||||
url: of([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MemberCipherDetailsApiService,
|
||||
useValue: mock<MemberCipherDetailsApiService>(),
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordHealthMembersURIComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it("should initialize component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
MemberCipherDetailsApiService,
|
||||
PasswordHealthService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
BadgeVariant,
|
||||
ContainerComponent,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "tools-password-health-members-uri",
|
||||
templateUrl: "password-health-members-uri.component.html",
|
||||
imports: [
|
||||
BadgeModule,
|
||||
CommonModule,
|
||||
ContainerComponent,
|
||||
PipesModule,
|
||||
JslibModule,
|
||||
HeaderModule,
|
||||
TableModule,
|
||||
],
|
||||
providers: [PasswordHealthService, MemberCipherDetailsApiService],
|
||||
})
|
||||
export class PasswordHealthMembersURIComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
|
||||
|
||||
weakPasswordCiphers: CipherView[] = [];
|
||||
|
||||
passwordUseMap = new Map<string, number>();
|
||||
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
|
||||
totalMembersMap = new Map<string, number>();
|
||||
|
||||
dataSource = new TableDataSource<CipherView>();
|
||||
|
||||
reportCiphers: (CipherView & { hostURI: string })[] = [];
|
||||
reportCipherURIs: string[] = [];
|
||||
|
||||
organization: Organization;
|
||||
|
||||
loading = true;
|
||||
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected organizationService: OrganizationService,
|
||||
protected auditService: AuditService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected memberCipherDetailsApiService: MemberCipherDetailsApiService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(async (params) => {
|
||||
const organizationId = params.get("organizationId");
|
||||
await this.setCiphers(organizationId);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async setCiphers(organizationId: string) {
|
||||
const passwordHealthService = new PasswordHealthService(
|
||||
this.passwordStrengthService,
|
||||
this.auditService,
|
||||
this.cipherService,
|
||||
this.memberCipherDetailsApiService,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
await passwordHealthService.generateReport();
|
||||
|
||||
this.dataSource.data = passwordHealthService.groupCiphersByLoginUri();
|
||||
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
|
||||
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
|
||||
this.passwordUseMap = passwordHealthService.passwordUseMap;
|
||||
this.totalMembersMap = passwordHealthService.totalMembersMap;
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<p>{{ "passwordsReportDesc" | i18n }}</p>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
|
||||
<td bitCell>
|
||||
<input
|
||||
bitCheckbox
|
||||
type="checkbox"
|
||||
[checked]="selectedIds.has(r.id)"
|
||||
(change)="onCheckboxChange(r.id, $event)"
|
||||
/>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container>
|
||||
<span>{{ r.name }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span
|
||||
bitBadge
|
||||
*ngIf="passwordStrengthMap.has(r.id)"
|
||||
[variant]="passwordStrengthMap.get(r.id)[1]"
|
||||
>
|
||||
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
|
||||
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right" data-testid="total-membership">
|
||||
{{ totalMembersMap.get(r.id) || 0 }}
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
@@ -1,130 +0,0 @@
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl, FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { debounceTime, map } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
MemberCipherDetailsApiService,
|
||||
PasswordHealthService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeVariant,
|
||||
SearchModule,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SharedModule } from "../../shared";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "tools-password-health-members",
|
||||
templateUrl: "password-health-members.component.html",
|
||||
imports: [PipesModule, HeaderModule, SearchModule, FormsModule, SharedModule, TableModule],
|
||||
providers: [PasswordHealthService, MemberCipherDetailsApiService],
|
||||
})
|
||||
export class PasswordHealthMembersComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
|
||||
|
||||
passwordUseMap = new Map<string, number>();
|
||||
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
|
||||
totalMembersMap = new Map<string, number>();
|
||||
|
||||
dataSource = new TableDataSource<CipherView>();
|
||||
|
||||
loading = true;
|
||||
|
||||
selectedIds: Set<number> = new Set<number>();
|
||||
|
||||
protected searchControl = new FormControl("", { nonNullable: true });
|
||||
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected auditService: AuditService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected toastService: ToastService,
|
||||
protected memberCipherDetailsApiService: MemberCipherDetailsApiService,
|
||||
) {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||
.subscribe((v) => (this.dataSource.filter = v));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(async (params) => {
|
||||
const organizationId = params.get("organizationId");
|
||||
await this.setCiphers(organizationId);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async setCiphers(organizationId: string) {
|
||||
const passwordHealthService = new PasswordHealthService(
|
||||
this.passwordStrengthService,
|
||||
this.auditService,
|
||||
this.cipherService,
|
||||
this.memberCipherDetailsApiService,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
await passwordHealthService.generateReport();
|
||||
|
||||
this.dataSource.data = passwordHealthService.reportCiphers;
|
||||
|
||||
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
|
||||
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
|
||||
this.passwordUseMap = passwordHealthService.passwordUseMap;
|
||||
this.totalMembersMap = passwordHealthService.totalMembersMap;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
markAppsAsCritical = async () => {
|
||||
// TODO: Send to API once implemented
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.selectedIds.clear();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("appsMarkedAsCritical"),
|
||||
});
|
||||
resolve(true);
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
trackByFunction(_: number, item: CipherView) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
onCheckboxChange(id: number, event: Event) {
|
||||
const isChecked = (event.target as HTMLInputElement).checked;
|
||||
if (isChecked) {
|
||||
this.selectedIds.add(id);
|
||||
} else {
|
||||
this.selectedIds.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<bit-container>
|
||||
<p>{{ "passwordsReportDesc" | i18n }}</p>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div class="tw-mt-4" *ngIf="!loading && dataSource.data.length">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr bitRow>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
|
||||
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body let-rows$>
|
||||
<tr bitRow *ngFor="let r of rows$ | async">
|
||||
<td bitCell>
|
||||
<app-vault-icon [cipher]="r"></app-vault-icon>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<ng-container>
|
||||
<span>{{ r.name }}</span>
|
||||
</ng-container>
|
||||
<br />
|
||||
<small>{{ r.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span
|
||||
bitBadge
|
||||
*ngIf="passwordStrengthMap.has(r.id)"
|
||||
[variant]="passwordStrengthMap.get(r.id)[1]"
|
||||
>
|
||||
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
|
||||
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell class="tw-text-right">
|
||||
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
|
||||
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
</bit-container>
|
||||
@@ -1,70 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute, convertToParamMap } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
MemberCipherDetailsApiService,
|
||||
PasswordHealthService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TableModule } from "@bitwarden/components";
|
||||
import { TableBodyDirective } from "@bitwarden/components/src/table/table.component";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared";
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { PasswordHealthComponent } from "./password-health.component";
|
||||
|
||||
describe("PasswordHealthComponent", () => {
|
||||
let component: PasswordHealthComponent;
|
||||
let fixture: ComponentFixture<PasswordHealthComponent>;
|
||||
const activeRouteParams = convertToParamMap({ organizationId: "orgId" });
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule],
|
||||
declarations: [TableBodyDirective],
|
||||
providers: [
|
||||
{ provide: CipherService, useValue: mock<CipherService>() },
|
||||
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||
{ provide: AuditService, useValue: mock<AuditService>() },
|
||||
{ provide: ApiService, useValue: mock<ApiService>() },
|
||||
{ provide: MemberCipherDetailsApiService, useValue: mock<MemberCipherDetailsApiService>() },
|
||||
{
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useValue: mock<PasswordStrengthServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordHealthService,
|
||||
useValue: mock<PasswordHealthService>(),
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
paramMap: of(activeRouteParams),
|
||||
url: of([]),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordHealthComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should initialize component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should call generateReport on init", () => {});
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
MemberCipherDetailsApiService,
|
||||
PasswordHealthService,
|
||||
} from "@bitwarden/bit-common/tools/reports/risk-insights";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
BadgeVariant,
|
||||
ContainerComponent,
|
||||
TableDataSource,
|
||||
TableModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "tools-password-health",
|
||||
templateUrl: "password-health.component.html",
|
||||
imports: [
|
||||
BadgeModule,
|
||||
OrganizationBadgeModule,
|
||||
CommonModule,
|
||||
ContainerComponent,
|
||||
PipesModule,
|
||||
JslibModule,
|
||||
HeaderModule,
|
||||
TableModule,
|
||||
],
|
||||
providers: [PasswordHealthService, MemberCipherDetailsApiService],
|
||||
})
|
||||
export class PasswordHealthComponent implements OnInit {
|
||||
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
|
||||
|
||||
passwordUseMap = new Map<string, number>();
|
||||
|
||||
exposedPasswordMap = new Map<string, number>();
|
||||
|
||||
dataSource = new TableDataSource<CipherView>();
|
||||
|
||||
loading = true;
|
||||
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
protected auditService: AuditService,
|
||||
protected i18nService: I18nService,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected memberCipherDetailsApiService: MemberCipherDetailsApiService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
map(async (params) => {
|
||||
const organizationId = params.get("organizationId");
|
||||
await this.setCiphers(organizationId);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async setCiphers(organizationId: string) {
|
||||
const passwordHealthService = new PasswordHealthService(
|
||||
this.passwordStrengthService,
|
||||
this.auditService,
|
||||
this.cipherService,
|
||||
this.memberCipherDetailsApiService,
|
||||
organizationId,
|
||||
);
|
||||
|
||||
await passwordHealthService.generateReport();
|
||||
|
||||
this.dataSource.data = passwordHealthService.reportCiphers;
|
||||
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
|
||||
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
|
||||
this.passwordUseMap = passwordHealthService.passwordUseMap;
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div>
|
||||
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
|
||||
<div class="tw-text-muted tw-max-w-4xl tw-mb-2">
|
||||
{{ "reviewAtRiskPasswords" | i18n }}
|
||||
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
|
||||
</div>
|
||||
<div class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
|
||||
<i class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] text-muted" aria-hidden="true"></i>
|
||||
<span class="tw-mx-4">{{
|
||||
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
|
||||
}}</span>
|
||||
<a
|
||||
bitButton
|
||||
buttonType="unstyled"
|
||||
class="tw-border-none !tw-font-normal tw-cursor-pointer"
|
||||
[bitAction]="refreshData.bind(this)"
|
||||
>
|
||||
{{ "refresh" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
|
||||
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
|
||||
<tools-all-applications></tools-all-applications>
|
||||
</bit-tab>
|
||||
<bit-tab *ngIf="isCritialAppsFeatureEnabled">
|
||||
<ng-template bitTabLabel>
|
||||
<i class="bwi bwi-star"></i>
|
||||
{{ "criticalApplicationsWithCount" | i18n: criticalApps.length }}
|
||||
</ng-template>
|
||||
<tools-critical-applications></tools-critical-applications>
|
||||
</bit-tab>
|
||||
<bit-tab label="Raw Data">
|
||||
<tools-password-health></tools-password-health>
|
||||
</bit-tab>
|
||||
<bit-tab label="Raw Data + members">
|
||||
<tools-password-health-members></tools-password-health-members>
|
||||
</bit-tab>
|
||||
<bit-tab label="Raw Data + uri">
|
||||
<tools-password-health-members-uri></tools-password-health-members-uri>
|
||||
</bit-tab>
|
||||
<!-- <bit-tab>
|
||||
<ng-template bitTabLabel>
|
||||
<i class="bwi bwi-envelope"></i>
|
||||
{{ "notifiedMembersWithCount" | i18n: priorityApps.length }}
|
||||
</ng-template>
|
||||
<h2 bitTypography="h2">{{ "notifiedMembers" | i18n }}</h2>
|
||||
<tools-notified-members-table></tools-notified-members-table>
|
||||
</bit-tab> -->
|
||||
</bit-tab-group>
|
||||
@@ -1,86 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
|
||||
import { AllApplicationsComponent } from "./all-applications.component";
|
||||
import { CriticalApplicationsComponent } from "./critical-applications.component";
|
||||
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
|
||||
import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component";
|
||||
import { PasswordHealthMembersComponent } from "./password-health-members.component";
|
||||
import { PasswordHealthComponent } from "./password-health.component";
|
||||
|
||||
export enum RiskInsightsTabType {
|
||||
AllApps = 0,
|
||||
CriticalApps = 1,
|
||||
NotifiedMembers = 2,
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "./risk-insights.component.html",
|
||||
imports: [
|
||||
AllApplicationsComponent,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CommonModule,
|
||||
CriticalApplicationsComponent,
|
||||
JslibModule,
|
||||
HeaderModule,
|
||||
PasswordHealthComponent,
|
||||
PasswordHealthMembersComponent,
|
||||
PasswordHealthMembersURIComponent,
|
||||
NotifiedMembersTableComponent,
|
||||
TabsModule,
|
||||
],
|
||||
})
|
||||
export class RiskInsightsComponent implements OnInit {
|
||||
tabIndex: RiskInsightsTabType;
|
||||
dataLastUpdated = new Date();
|
||||
isCritialAppsFeatureEnabled = false;
|
||||
|
||||
apps: any[] = [];
|
||||
criticalApps: any[] = [];
|
||||
notifiedMembers: any[] = [];
|
||||
|
||||
async refreshData() {
|
||||
// TODO: Implement
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
this.dataLastUpdated = new Date();
|
||||
resolve(true);
|
||||
}, 1000),
|
||||
);
|
||||
}
|
||||
|
||||
onTabChange = async (newIndex: number) => {
|
||||
await this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { tabIndex: newIndex },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
};
|
||||
|
||||
async ngOnInit() {
|
||||
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.CriticalApps,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
|
||||
this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -51,14 +51,12 @@ window.addEventListener("load", async () => {
|
||||
*/
|
||||
function redirectToDuoFrameless(redirectUrl: string) {
|
||||
const validateUrl = new URL(redirectUrl);
|
||||
const validDuoUrl =
|
||||
validateUrl.protocol === "https:" &&
|
||||
(validateUrl.hostname.endsWith(".duosecurity.com") ||
|
||||
validateUrl.hostname.endsWith(".duofederal.com"));
|
||||
|
||||
if (
|
||||
validateUrl.protocol !== "https:" ||
|
||||
!(
|
||||
validateUrl.hostname.endsWith("duosecurity.com") ||
|
||||
validateUrl.hostname.endsWith("duofederal.com")
|
||||
)
|
||||
) {
|
||||
if (!validDuoUrl) {
|
||||
throw new Error("Invalid redirect URL");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user