mirror of
https://github.com/bitwarden/browser
synced 2026-03-02 11:31:44 +00:00
Merge branch 'main' into km/cose-upgrade
This commit is contained in:
@@ -50,11 +50,15 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
||||
}
|
||||
|
||||
get showBulkConfirmUsers(): boolean {
|
||||
return this.dataSource.acceptedUserCount > 0;
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Accepted);
|
||||
}
|
||||
|
||||
get showBulkReinviteUsers(): boolean {
|
||||
return this.dataSource.invitedUserCount > 0;
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Invited);
|
||||
}
|
||||
|
||||
abstract userType: typeof OrganizationUserType | typeof ProviderUserType;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { getNestedCollectionTree } from "./collection-utils";
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./collection-utils";
|
||||
|
||||
describe("CollectionUtils Service", () => {
|
||||
describe("getNestedCollectionTree", () => {
|
||||
@@ -36,4 +37,63 @@ describe("CollectionUtils Service", () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFlatCollectionTree", () => {
|
||||
it("should flatten a tree node with no children", () => {
|
||||
// Arrange
|
||||
const collection = new CollectionView();
|
||||
collection.name = "Test Collection";
|
||||
collection.id = "test-id";
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [
|
||||
new TreeNode<CollectionView>(collection, null),
|
||||
];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toBe(collection);
|
||||
});
|
||||
|
||||
it("should flatten a tree node with children", () => {
|
||||
// Arrange
|
||||
const parentCollection = new CollectionView();
|
||||
parentCollection.name = "Parent";
|
||||
parentCollection.id = "parent-id";
|
||||
|
||||
const child1Collection = new CollectionView();
|
||||
child1Collection.name = "Child 1";
|
||||
child1Collection.id = "child1-id";
|
||||
|
||||
const child2Collection = new CollectionView();
|
||||
child2Collection.name = "Child 2";
|
||||
child2Collection.id = "child2-id";
|
||||
|
||||
const grandchildCollection = new CollectionView();
|
||||
grandchildCollection.name = "Grandchild";
|
||||
grandchildCollection.id = "grandchild-id";
|
||||
|
||||
const parentNode = new TreeNode<CollectionView>(parentCollection, null);
|
||||
const child1Node = new TreeNode<CollectionView>(child1Collection, parentNode);
|
||||
const child2Node = new TreeNode<CollectionView>(child2Collection, parentNode);
|
||||
const grandchildNode = new TreeNode<CollectionView>(grandchildCollection, child1Node);
|
||||
|
||||
parentNode.children = [child1Node, child2Node];
|
||||
child1Node.children = [grandchildNode];
|
||||
|
||||
const treeNodes: TreeNode<CollectionView>[] = [parentNode];
|
||||
|
||||
// Act
|
||||
const result = getFlatCollectionTree(treeNodes);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(4);
|
||||
expect(result[0]).toBe(parentCollection);
|
||||
expect(result).toContain(child1Collection);
|
||||
expect(result).toContain(child2Collection);
|
||||
expect(result).toContain(grandchildCollection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,27 @@ export function getNestedCollectionTree(
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
export function getFlatCollectionTree(nodes: TreeNode<CollectionView>[]): CollectionView[];
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionView | CollectionAdminView>[],
|
||||
): (CollectionView | CollectionAdminView)[] {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return nodes.flatMap((node) => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return [node.node];
|
||||
}
|
||||
|
||||
const children = getFlatCollectionTree(node.children);
|
||||
return [node.node, ...children];
|
||||
});
|
||||
}
|
||||
|
||||
function cloneCollection(collection: CollectionView): CollectionView;
|
||||
function cloneCollection(collection: CollectionAdminView): CollectionAdminView;
|
||||
function cloneCollection(
|
||||
|
||||
@@ -105,14 +105,14 @@ export class VaultFilterComponent
|
||||
id: "AllCollections",
|
||||
name: "collections",
|
||||
type: "all",
|
||||
icon: "bwi-collection",
|
||||
icon: "bwi-collection-shared",
|
||||
},
|
||||
[
|
||||
{
|
||||
id: "AllCollections",
|
||||
name: "Collections",
|
||||
type: "all",
|
||||
icon: "bwi-collection",
|
||||
icon: "bwi-collection-shared",
|
||||
},
|
||||
],
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<ng-container>
|
||||
<bit-breadcrumb
|
||||
*ngFor="let collection of collections"
|
||||
icon="bwi-collection"
|
||||
icon="bwi-collection-shared"
|
||||
[route]="[]"
|
||||
[queryParams]="{ collectionId: collection.id }"
|
||||
queryParamsHandling="merge"
|
||||
@@ -115,7 +115,7 @@
|
||||
id="newItemDropdown"
|
||||
appA11yTitle="{{ 'new' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||
@@ -140,7 +140,7 @@
|
||||
<ng-container *ngIf="canCreateCollection">
|
||||
<bit-menu-divider *ngIf="canCreateCipher"></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="addCollection()">
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
||||
{{ "collection" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@@ -117,7 +117,7 @@ export class VaultHeaderComponent {
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this.filter.collectionId !== undefined ? "bwi-collection" : "";
|
||||
return this.filter.collectionId !== undefined ? "bwi-collection-shared" : "";
|
||||
}
|
||||
|
||||
protected get showBreadcrumbs() {
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<ng-container *ngIf="freeTrial$ | async as freeTrial">
|
||||
<app-free-trial-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-free-trial-warning>
|
||||
<app-reseller-renewal-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-reseller-renewal-warning>
|
||||
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
@@ -19,7 +30,7 @@
|
||||
</a>
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="resellerWarning$ | async as resellerWarning">
|
||||
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
|
||||
<bit-banner
|
||||
id="reseller-warning-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Subject,
|
||||
} from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
catchError,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import {
|
||||
@@ -44,6 +44,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -61,8 +62,8 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import {
|
||||
DialogRef,
|
||||
BannerModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
Icons,
|
||||
NoItemsModule,
|
||||
@@ -77,6 +78,8 @@ import {
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/services/organization-warnings.service";
|
||||
import { ResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/reseller-renewal-warning.component";
|
||||
|
||||
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||
import {
|
||||
@@ -85,6 +88,7 @@ import {
|
||||
} from "../../../billing/services/reseller-warning.service";
|
||||
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
|
||||
import { FreeTrial } from "../../../billing/types/free-trial";
|
||||
import { FreeTrialWarningComponent } from "../../../billing/warnings/free-trial-warning.component";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
|
||||
import {
|
||||
@@ -121,7 +125,7 @@ import {
|
||||
BulkCollectionsDialogResult,
|
||||
} from "./bulk-collections-dialog";
|
||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||
import { getNestedCollectionTree } from "./utils";
|
||||
import { getNestedCollectionTree, getFlatCollectionTree } from "./utils";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
|
||||
@@ -145,6 +149,8 @@ enum AddAccessStatusType {
|
||||
SharedModule,
|
||||
BannerModule,
|
||||
NoItemsModule,
|
||||
FreeTrialWarningComponent,
|
||||
ResellerRenewalWarningComponent,
|
||||
],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
@@ -174,8 +180,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected showCollectionAccessRestricted: boolean;
|
||||
private hasSubscription$ = new BehaviorSubject<boolean>(false);
|
||||
protected currentSearchText$: Observable<string>;
|
||||
protected freeTrial$: Observable<FreeTrial>;
|
||||
protected resellerWarning$: Observable<ResellerWarning | null>;
|
||||
protected useOrganizationWarningsService$: Observable<boolean>;
|
||||
protected freeTrialWhenWarningsServiceDisabled$: Observable<FreeTrial>;
|
||||
protected resellerWarningWhenWarningsServiceDisabled$: Observable<ResellerWarning | null>;
|
||||
protected prevCipherId: string | null = null;
|
||||
protected userId: UserId;
|
||||
/**
|
||||
@@ -255,6 +262,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private resellerWarningService: ResellerWarningService,
|
||||
private accountService: AccountService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -432,23 +440,33 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.showAddAccessToggle = false;
|
||||
let collectionsToReturn = [];
|
||||
let searchableCollectionNodes: TreeNode<CollectionAdminView>[] = [];
|
||||
if (filter.collectionId === undefined || filter.collectionId === All) {
|
||||
collectionsToReturn = collections.map((c) => c.node);
|
||||
searchableCollectionNodes = collections;
|
||||
} else {
|
||||
const selectedCollection = ServiceUtils.getTreeNodeObjectFromList(
|
||||
collections,
|
||||
filter.collectionId,
|
||||
);
|
||||
collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? [];
|
||||
searchableCollectionNodes = selectedCollection?.children ?? [];
|
||||
}
|
||||
|
||||
let collectionsToReturn: CollectionAdminView[] = [];
|
||||
|
||||
if (await this.searchService.isSearchable(this.userId, searchText)) {
|
||||
// Flatten the tree for searching through all levels
|
||||
const flatCollectionTree: CollectionAdminView[] =
|
||||
getFlatCollectionTree(searchableCollectionNodes);
|
||||
|
||||
collectionsToReturn = this.searchPipe.transform(
|
||||
collectionsToReturn,
|
||||
flatCollectionTree,
|
||||
searchText,
|
||||
(collection: CollectionAdminView) => collection.name,
|
||||
(collection: CollectionAdminView) => collection.id,
|
||||
(collection) => collection.name,
|
||||
(collection) => collection.id,
|
||||
);
|
||||
} else {
|
||||
collectionsToReturn = searchableCollectionNodes.map(
|
||||
(treeNode: TreeNode<CollectionAdminView>): CollectionAdminView => treeNode.node,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -618,9 +636,23 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.unpaidSubscriptionDialog$.pipe(takeUntil(this.destroy$)).subscribe();
|
||||
// Billing Warnings
|
||||
this.useOrganizationWarningsService$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.UseOrganizationWarningsService,
|
||||
);
|
||||
|
||||
this.freeTrial$ = combineLatest([
|
||||
this.useOrganizationWarningsService$
|
||||
.pipe(
|
||||
switchMap((enabled) =>
|
||||
enabled
|
||||
? this.organizationWarningsService.showInactiveSubscriptionDialog$(this.organization)
|
||||
: this.unpaidSubscriptionDialog$,
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
const freeTrial$ = combineLatest([
|
||||
organization$,
|
||||
this.hasSubscription$.pipe(filter((hasSubscription) => hasSubscription !== null)),
|
||||
]).pipe(
|
||||
@@ -645,7 +677,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
filter((result) => result !== null),
|
||||
);
|
||||
|
||||
this.resellerWarning$ = organization$.pipe(
|
||||
this.freeTrialWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe(
|
||||
filter((enabled) => !enabled),
|
||||
switchMap(() => freeTrial$),
|
||||
);
|
||||
|
||||
const resellerWarning$ = organization$.pipe(
|
||||
filter((org) => org.isOwner),
|
||||
switchMap((org) =>
|
||||
from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe(
|
||||
@@ -655,6 +692,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
map(({ org, metadata }) => this.resellerWarningService.getWarning(org, metadata)),
|
||||
);
|
||||
|
||||
this.resellerWarningWhenWarningsServiceDisabled$ = this.useOrganizationWarningsService$.pipe(
|
||||
filter((enabled) => !enabled),
|
||||
switchMap(() => resellerWarning$),
|
||||
);
|
||||
// End Billing Warnings
|
||||
|
||||
firstSetup$
|
||||
.pipe(
|
||||
switchMap(() => this.refresh$),
|
||||
@@ -809,6 +852,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
|
||||
cipherId: cipher.id as CipherId,
|
||||
organizationId: cipher.organizationId as OrganizationId,
|
||||
admin: true,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
icon="bwi-collection"
|
||||
icon="bwi-collection-shared"
|
||||
[text]="'collections' | i18n"
|
||||
route="vault"
|
||||
*ngIf="canShowVaultTab(organization)"
|
||||
@@ -115,7 +115,7 @@
|
||||
*ngIf="canAccessExport$ | async"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="domainVerificationNavigationTextKey | i18n"
|
||||
[text]="'claimedDomains' | i18n"
|
||||
route="settings/domain-verification"
|
||||
*ngIf="organization?.canManageDomainVerification"
|
||||
></bit-nav-item>
|
||||
|
||||
@@ -55,7 +55,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
protected readonly logo = AdminConsoleLogo;
|
||||
|
||||
protected orgFilter = (org: Organization) => canAccessOrgAdmin(org);
|
||||
protected domainVerificationNavigationTextKey: string;
|
||||
|
||||
protected integrationPageEnabled$: Observable<boolean>;
|
||||
|
||||
@@ -146,12 +145,6 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
|
||||
this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations));
|
||||
|
||||
this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
))
|
||||
? "claimedDomains"
|
||||
: "domainVerification";
|
||||
|
||||
this.canShowPoliciesTab$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</div>
|
||||
<bit-callout
|
||||
type="info"
|
||||
[title]="'upgradeEventLogTitle' | i18n"
|
||||
[title]="'upgradeEventLogTitleMessage' | i18n"
|
||||
*ngIf="loaded && usePlaceHolderEvents"
|
||||
>
|
||||
{{ "upgradeEventLogMessage" | i18n }}
|
||||
@@ -125,10 +125,10 @@
|
||||
<i class="bwi bwi-2x bwi-business tw-text-primary-600"></i>
|
||||
|
||||
<p class="tw-font-bold tw-mt-2">
|
||||
{{ "limitedEventLogs" | i18n: ProductTierType[organization?.productTierType] }}
|
||||
{{ "upgradeEventLogTitleMessage" | i18n }}
|
||||
</p>
|
||||
<p>
|
||||
{{ "upgradeForFullEvents" | i18n }}
|
||||
{{ "upgradeForFullEventsMessage" | i18n }}
|
||||
</p>
|
||||
|
||||
<button type="button" class="tw-mt-1" bitButton buttonType="primary" (click)="changePlan()">
|
||||
|
||||
@@ -93,7 +93,8 @@
|
||||
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "members" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Collections)">
|
||||
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
|
||||
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
||||
{{ "collections" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="delete(g)">
|
||||
<span class="tw-text-danger"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<form [formGroup]="confirmForm" [bitSubmit]="submit">
|
||||
<bit-dialog
|
||||
dialogSize="large"
|
||||
[loading]="loading"
|
||||
[title]="'trustOrganization' | i18n"
|
||||
[subtitle]="params.name"
|
||||
>
|
||||
<ng-container bitDialogContent>
|
||||
<bit-callout type="warning">{{ "orgTrustWarning" | i18n }}</bit-callout>
|
||||
<p bitTypography="body1">
|
||||
{{ "fingerprintPhrase" | i18n }} <code>{{ fingerprint }}</code>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
<span>{{ "trust" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
|
||||
{{ "doNotTrust" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -1,69 +0,0 @@
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit, Inject } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
type OrganizationTrustDialogData = {
|
||||
/** display name of the organization */
|
||||
name: string;
|
||||
/** identifies the organization */
|
||||
orgId: string;
|
||||
/** org public key */
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
@Component({
|
||||
selector: "organization-trust",
|
||||
templateUrl: "organization-trust.component.html",
|
||||
})
|
||||
export class OrganizationTrustComponent implements OnInit {
|
||||
loading = true;
|
||||
fingerprint: string = "";
|
||||
confirmForm = this.formBuilder.group({});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: OrganizationTrustDialogData,
|
||||
private formBuilder: FormBuilder,
|
||||
private keyService: KeyService,
|
||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
private logService: LogService,
|
||||
private dialogRef: DialogRef<boolean>,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const fingerprint = await this.keyService.getFingerprint(
|
||||
this.params.orgId,
|
||||
this.params.publicKey,
|
||||
);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join("-");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a OrganizationTrustComponent
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param data The data to pass to the dialog
|
||||
*/
|
||||
static open(dialogService: DialogService, data: OrganizationTrustDialogData) {
|
||||
return dialogService.open<boolean, OrganizationTrustDialogData>(OrganizationTrustComponent, {
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
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 { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
|
||||
@@ -35,7 +32,6 @@ export class BulkDeleteDialogComponent {
|
||||
@Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams,
|
||||
protected i18nService: I18nService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private configService: ConfigService,
|
||||
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
) {
|
||||
this.organizationId = dialogParams.organizationId;
|
||||
@@ -43,11 +39,7 @@ export class BulkDeleteDialogComponent {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (
|
||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning))
|
||||
) {
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
|
||||
}
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<bit-dialog
|
||||
dialogSize="large"
|
||||
*ngIf="{ enabled: accountDeprovisioningEnabled$ | async } as accountDeprovisioning"
|
||||
>
|
||||
<bit-dialog dialogSize="large">
|
||||
<ng-container bitDialogTitle>
|
||||
<span *ngIf="accountDeprovisioning.enabled; else nonMemberTitle">{{ bulkMemberTitle }}</span>
|
||||
<ng-template #nonMemberTitle>
|
||||
{{ bulkTitle }}
|
||||
</ng-template>
|
||||
<span>{{ bulkTitle }}</span>
|
||||
</ng-container>
|
||||
|
||||
<div bitDialogContent>
|
||||
@@ -20,7 +14,7 @@
|
||||
|
||||
<bit-callout
|
||||
type="danger"
|
||||
*ngIf="nonCompliantMembers && accountDeprovisioning.enabled"
|
||||
*ngIf="nonCompliantMembers"
|
||||
title="{{ 'nonCompliantMembersTitle' | i18n }}"
|
||||
>
|
||||
{{ "nonCompliantMembersError" | i18n }}
|
||||
@@ -50,7 +44,7 @@
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }}</th>
|
||||
<th bitCell>{{ "member" | i18n }}</th>
|
||||
<th bitCell class="tw-w-1/2" *ngIf="this.showNoMasterPasswordWarning">
|
||||
{{ "details" | i18n }}
|
||||
</th>
|
||||
@@ -82,7 +76,7 @@
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-w-1/2">
|
||||
{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }}
|
||||
{{ "member" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-1/2">{{ "status" | i18n }}</th>
|
||||
</tr>
|
||||
@@ -113,7 +107,7 @@
|
||||
[bitAction]="submit"
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ accountDeprovisioning.enabled ? bulkMemberTitle : bulkTitle }}
|
||||
{{ bulkTitle }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
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 { DIALOG_DATA, DialogService } from "@bitwarden/components";
|
||||
|
||||
@@ -34,12 +31,10 @@ export class BulkRestoreRevokeComponent {
|
||||
error: string;
|
||||
showNoMasterPasswordWarning = false;
|
||||
nonCompliantMembers: boolean = false;
|
||||
accountDeprovisioningEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private configService: ConfigService,
|
||||
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
|
||||
) {
|
||||
this.isRevoking = data.isRevoking;
|
||||
@@ -48,17 +43,9 @@ export class BulkRestoreRevokeComponent {
|
||||
this.showNoMasterPasswordWarning = this.users.some(
|
||||
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
|
||||
);
|
||||
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
}
|
||||
|
||||
get bulkTitle() {
|
||||
const titleKey = this.isRevoking ? "revokeUsers" : "restoreUsers";
|
||||
return this.i18nService.t(titleKey);
|
||||
}
|
||||
|
||||
get bulkMemberTitle() {
|
||||
const titleKey = this.isRevoking ? "revokeMembers" : "restoreMembers";
|
||||
return this.i18nService.t(titleKey);
|
||||
}
|
||||
|
||||
@@ -275,11 +275,7 @@
|
||||
{{ "revoke" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="
|
||||
this.editMode &&
|
||||
(!(accountDeprovisioningEnabled$ | async) ||
|
||||
!(editParams$ | async)?.managedByOrganization)
|
||||
"
|
||||
*ngIf="this.editMode && !(editParams$ | async)?.managedByOrganization"
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
bitButton
|
||||
@@ -290,11 +286,7 @@
|
||||
{{ "remove" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="
|
||||
this.editMode &&
|
||||
(accountDeprovisioningEnabled$ | async) &&
|
||||
(editParams$ | async)?.managedByOrganization
|
||||
"
|
||||
*ngIf="this.editMode && (editParams$ | async)?.managedByOrganization"
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
bitButton
|
||||
|
||||
@@ -152,10 +152,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
manageResetPassword: false,
|
||||
});
|
||||
|
||||
protected accountDeprovisioningEnabled$: Observable<boolean> = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
@@ -667,11 +663,9 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
const showWarningDialog = combineLatest([
|
||||
this.organization$,
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.params.organizationId),
|
||||
this.accountDeprovisioningEnabled$,
|
||||
]).pipe(
|
||||
map(
|
||||
([organization, acknowledged, featureFlagEnabled]) =>
|
||||
featureFlagEnabled &&
|
||||
([organization, acknowledged]) =>
|
||||
organization.canManageUsers &&
|
||||
organization.productTierType === ProductTierType.Enterprise &&
|
||||
!acknowledged,
|
||||
@@ -714,9 +708,8 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
message: this.i18nService.t("organizationUserDeleted", this.params.name),
|
||||
});
|
||||
|
||||
if (await firstValueFrom(this.accountDeprovisioningEnabled$)) {
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
|
||||
}
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
|
||||
|
||||
this.close(MemberDialogResult.Deleted);
|
||||
};
|
||||
|
||||
|
||||
@@ -307,7 +307,8 @@
|
||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
|
||||
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
|
||||
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
|
||||
{{ "collections" | i18n }}
|
||||
</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button
|
||||
@@ -345,7 +346,7 @@
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!accountDeprovisioningEnabled || !u.managedByOrganization"
|
||||
*ngIf="!u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="remove(u)"
|
||||
@@ -355,7 +356,7 @@
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="accountDeprovisioningEnabled && u.managedByOrganization"
|
||||
*ngIf="u.managedByOrganization"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="deleteUser(u)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import {
|
||||
@@ -46,9 +46,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
@@ -91,7 +89,7 @@ class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView>
|
||||
@Component({
|
||||
templateUrl: "members.component.html",
|
||||
})
|
||||
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> implements OnInit {
|
||||
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> {
|
||||
@ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true })
|
||||
resetPasswordModalRef: ViewContainerRef;
|
||||
|
||||
@@ -104,7 +102,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
status: OrganizationUserStatusType = null;
|
||||
orgResetPasswordPolicyEnabled = false;
|
||||
orgIsOnSecretsManagerStandalone = false;
|
||||
accountDeprovisioningEnabled = false;
|
||||
|
||||
protected canUseSecretsManager$: Observable<boolean>;
|
||||
|
||||
@@ -139,7 +136,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
private groupService: GroupApiService,
|
||||
private collectionService: CollectionService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
) {
|
||||
super(
|
||||
@@ -237,12 +233,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountDeprovisioningEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
}
|
||||
|
||||
async getUsers(): Promise<OrganizationUserView[]> {
|
||||
let groupsPromise: Promise<Map<string, string>>;
|
||||
let collectionsPromise: Promise<Map<string, string>>;
|
||||
@@ -591,20 +581,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
async bulkDelete() {
|
||||
if (this.accountDeprovisioningEnabled) {
|
||||
const warningAcknowledged = await firstValueFrom(
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||
);
|
||||
const warningAcknowledged = await firstValueFrom(
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||
);
|
||||
|
||||
if (
|
||||
!warningAcknowledged &&
|
||||
this.organization.canManageUsers &&
|
||||
this.organization.productTierType === ProductTierType.Enterprise
|
||||
) {
|
||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||
if (!acknowledged) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!warningAcknowledged &&
|
||||
this.organization.canManageUsers &&
|
||||
this.organization.productTierType === ProductTierType.Enterprise
|
||||
) {
|
||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||
if (!acknowledged) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,20 +782,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
async deleteUser(user: OrganizationUserView) {
|
||||
if (this.accountDeprovisioningEnabled) {
|
||||
const warningAcknowledged = await firstValueFrom(
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||
);
|
||||
const warningAcknowledged = await firstValueFrom(
|
||||
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||
);
|
||||
|
||||
if (
|
||||
!warningAcknowledged &&
|
||||
this.organization.canManageUsers &&
|
||||
this.organization.productTierType === ProductTierType.Enterprise
|
||||
) {
|
||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||
if (!acknowledged) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!warningAcknowledged &&
|
||||
this.organization.canManageUsers &&
|
||||
this.organization.productTierType === ProductTierType.Enterprise
|
||||
) {
|
||||
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||
if (!acknowledged) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -829,9 +815,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.accountDeprovisioningEnabled) {
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
|
||||
}
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
|
||||
|
||||
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
|
||||
this.organization.id,
|
||||
@@ -864,56 +848,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
});
|
||||
}
|
||||
|
||||
get showBulkConfirmUsers(): boolean {
|
||||
if (!this.accountDeprovisioningEnabled) {
|
||||
return super.showBulkConfirmUsers;
|
||||
}
|
||||
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Accepted);
|
||||
}
|
||||
|
||||
get showBulkReinviteUsers(): boolean {
|
||||
if (!this.accountDeprovisioningEnabled) {
|
||||
return super.showBulkReinviteUsers;
|
||||
}
|
||||
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Invited);
|
||||
}
|
||||
|
||||
get showBulkRestoreUsers(): boolean {
|
||||
return (
|
||||
!this.accountDeprovisioningEnabled ||
|
||||
this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Revoked)
|
||||
);
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status == this.userStatusType.Revoked);
|
||||
}
|
||||
|
||||
get showBulkRevokeUsers(): boolean {
|
||||
return (
|
||||
!this.accountDeprovisioningEnabled ||
|
||||
this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status != this.userStatusType.Revoked)
|
||||
);
|
||||
return this.dataSource
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.status != this.userStatusType.Revoked);
|
||||
}
|
||||
|
||||
get showBulkRemoveUsers(): boolean {
|
||||
return (
|
||||
!this.accountDeprovisioningEnabled ||
|
||||
this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization)
|
||||
);
|
||||
return this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization);
|
||||
}
|
||||
|
||||
get showBulkDeleteUsers(): boolean {
|
||||
if (!this.accountDeprovisioningEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const validStatuses = [
|
||||
this.userStatusType.Accepted,
|
||||
this.userStatusType.Confirmed,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<bit-callout *ngIf="accountDeprovisioningEnabled$ | async; else disabledBlock" type="warning">
|
||||
<bit-callout type="warning">
|
||||
{{ "singleOrgPolicyMemberWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<ng-template #disabledBlock>
|
||||
<bit-callout type="warning">
|
||||
{{ "singleOrgPolicyWarning" | i18n }}
|
||||
</bit-callout>
|
||||
</ng-template>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
export class SingleOrgPolicy extends BasePolicy {
|
||||
name = "singleOrg";
|
||||
description = "singleOrgDesc";
|
||||
description = "singleOrgPolicyDesc";
|
||||
type = PolicyType.SingleOrg;
|
||||
component = SingleOrgPolicyComponent;
|
||||
}
|
||||
@@ -19,22 +16,9 @@ export class SingleOrgPolicy extends BasePolicy {
|
||||
templateUrl: "single-org.component.html",
|
||||
})
|
||||
export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit {
|
||||
constructor(private configService: ConfigService) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected accountDeprovisioningEnabled$: Observable<boolean> = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
|
||||
async ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
const isAccountDeprovisioningEnabled = await firstValueFrom(this.accountDeprovisioningEnabled$);
|
||||
this.policy.description = isAccountDeprovisioningEnabled
|
||||
? "singleOrgPolicyDesc"
|
||||
: "singleOrgDesc";
|
||||
|
||||
if (!this.policyResponse.canToggleState) {
|
||||
this.enabled.disable();
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { ExposedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/exposed-passwords-report.component";
|
||||
import { InactiveTwoFactorReportComponent } from "../../../tools/reports/pages/organizations/inactive-two-factor-report.component";
|
||||
import { ReusedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/reused-passwords-report.component";
|
||||
import { UnsecuredWebsitesReportComponent } from "../../../tools/reports/pages/organizations/unsecured-websites-report.component";
|
||||
import { WeakPasswordsReportComponent } from "../../../tools/reports/pages/organizations/weak-passwords-report.component";
|
||||
import { ExposedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/exposed-passwords-report.component";
|
||||
import { InactiveTwoFactorReportComponent } from "../../../dirt/reports/pages/organizations/inactive-two-factor-report.component";
|
||||
import { ReusedPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/reused-passwords-report.component";
|
||||
import { UnsecuredWebsitesReportComponent } from "../../../dirt/reports/pages/organizations/unsecured-websites-report.component";
|
||||
import { WeakPasswordsReportComponent } from "../../../dirt/reports/pages/organizations/weak-passwords-report.component";
|
||||
/* eslint no-restricted-imports: "error" */
|
||||
import { isPaidOrgGuard } from "../guards/is-paid-org.guard";
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ReportsSharedModule } from "../../../dirt/reports";
|
||||
import { LooseComponentsModule } from "../../../shared";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { ReportsSharedModule } from "../../../tools/reports";
|
||||
|
||||
import { OrganizationReportingRoutingModule } from "./organization-reporting-routing.module";
|
||||
import { ReportsHomeComponent } from "./reports-home.component";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
import { ReportVariant, reports, ReportType, ReportEntry } from "../../../tools/reports";
|
||||
import { ReportVariant, reports, ReportType, ReportEntry } from "../../../dirt/reports";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-reports-home",
|
||||
|
||||
@@ -310,7 +310,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
||||
protected itemIcon(item: AccessItemView) {
|
||||
switch (item.type) {
|
||||
case AccessItemType.Collection:
|
||||
return "bwi-collection";
|
||||
return "bwi-collection-shared";
|
||||
case AccessItemType.Group:
|
||||
return "bwi-users";
|
||||
case AccessItemType.Member:
|
||||
|
||||
@@ -48,14 +48,14 @@
|
||||
<bit-option
|
||||
*ngIf="deletedParentName"
|
||||
disabled
|
||||
icon="bwi-collection"
|
||||
icon="bwi-collection-shared"
|
||||
[value]="deletedParentName"
|
||||
label="{{ deletedParentName }} ({{ 'deleted' | i18n }})"
|
||||
>
|
||||
</bit-option>
|
||||
<bit-option
|
||||
*ngFor="let collection of nestOptions"
|
||||
icon="bwi-collection"
|
||||
icon="bwi-collection-shared"
|
||||
[value]="collection.name"
|
||||
[label]="collection.name"
|
||||
>
|
||||
|
||||
@@ -18,8 +18,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { AccountRecoveryTrustComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { OrganizationTrustComponent } from "../manage/organization-trust.component";
|
||||
import { OrganizationUserResetPasswordService } from "../members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
|
||||
interface EnrollMasterPasswordResetData {
|
||||
@@ -62,7 +62,7 @@ export class EnrollMasterPasswordReset {
|
||||
await userVerificationService.buildRequest<OrganizationUserResetPasswordEnrollmentRequest>(
|
||||
secret,
|
||||
);
|
||||
const dialogRef = OrganizationTrustComponent.open(dialogService, {
|
||||
const dialogRef = AccountRecoveryTrustComponent.open(dialogService, {
|
||||
name: data.organization.name,
|
||||
orgId: data.organization.id,
|
||||
publicKey,
|
||||
|
||||
@@ -58,7 +58,7 @@ export class RotateableKeySetService {
|
||||
throw new Error("failed to rotate key set: newUserKey is required");
|
||||
}
|
||||
|
||||
const publicKey = await this.encryptService.decryptToBytes(
|
||||
const publicKey = await this.encryptService.unwrapEncapsulationKey(
|
||||
keySet.encryptedPublicKey,
|
||||
oldUserKey,
|
||||
);
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
encryptService.wrapDecapsulationKey.mockResolvedValue({
|
||||
encryptedString: "string",
|
||||
} as EncString);
|
||||
encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString);
|
||||
encryptService.encryptString.mockResolvedValue({ encryptedString: "string" } as EncString);
|
||||
const invite = createOrgInvite({ initOrganization: true });
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
|
||||
@@ -145,7 +145,7 @@ export class AcceptOrganizationInviteService {
|
||||
|
||||
const [encryptedOrgKey, orgKey] = await this.keyService.makeOrgKey<OrgKey>();
|
||||
const [orgPublicKey, encryptedOrgPrivateKey] = await this.keyService.makeKeyPair(orgKey);
|
||||
const collection = await this.encryptService.encrypt(
|
||||
const collection = await this.encryptService.encryptString(
|
||||
this.i18nService.t("defaultCollection"),
|
||||
orgKey,
|
||||
);
|
||||
|
||||
@@ -16,7 +16,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
import { RecoverTwoFactorComponent } from "./recover-two-factor.component";
|
||||
|
||||
@@ -35,7 +34,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockLoginSuccessHandlerService: MockProxy<LoginSuccessHandlerService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockNewDeviceVerificationNoticeService: MockProxy<NewDeviceVerificationNoticeService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRouter = mock<Router>();
|
||||
@@ -48,7 +46,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockLoginSuccessHandlerService = mock<LoginSuccessHandlerService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockNewDeviceVerificationNoticeService = mock<NewDeviceVerificationNoticeService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [RecoverTwoFactorComponent],
|
||||
@@ -63,10 +60,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{
|
||||
provide: NewDeviceVerificationNoticeService,
|
||||
useValue: mockNewDeviceVerificationNoticeService,
|
||||
},
|
||||
],
|
||||
imports: [I18nPipe],
|
||||
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
|
||||
@@ -102,9 +95,6 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
title: "",
|
||||
message: mockI18nService.t("youHaveBeenLoggedIn"),
|
||||
});
|
||||
expect(
|
||||
mockNewDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState,
|
||||
).toHaveBeenCalledWith(authResult.userId, true);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/settings/security/two-factor"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { NewDeviceVerificationNoticeService } from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "app-recover-two-factor",
|
||||
@@ -37,7 +36,6 @@ export class RecoverTwoFactorComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private logService: LogService,
|
||||
private newDeviceVerificationNoticeService: NewDeviceVerificationNoticeService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -104,13 +102,6 @@ export class RecoverTwoFactorComponent implements OnInit {
|
||||
|
||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||
|
||||
// Before routing, set the state to skip the new device notification. This is a temporary
|
||||
// fix and will be cleaned up in PM-18485.
|
||||
await this.newDeviceVerificationNoticeService.updateNewDeviceVerificationSkipNoticeState(
|
||||
authResult.userId,
|
||||
true,
|
||||
);
|
||||
|
||||
await this.router.navigate(["/settings/security/two-factor"]);
|
||||
} catch (error) {
|
||||
// If login errors, redirect to login page per product. Don't show error
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import {
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
from,
|
||||
lastValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
Subject,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
import { firstValueFrom, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
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 { DialogService } from "@bitwarden/components";
|
||||
|
||||
@@ -47,10 +37,6 @@ export class AccountComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
|
||||
const userIsManagedByOrganization$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(
|
||||
@@ -61,25 +47,14 @@ export class AccountComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.showChangeEmail$ = hasMasterPassword$;
|
||||
|
||||
this.showPurgeVault$ = combineLatest([
|
||||
isAccountDeprovisioningEnabled$,
|
||||
userIsManagedByOrganization$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
|
||||
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
|
||||
),
|
||||
this.showPurgeVault$ = userIsManagedByOrganization$.pipe(
|
||||
map((userIsManagedByOrganization) => !userIsManagedByOrganization),
|
||||
);
|
||||
|
||||
this.showDeleteAccount$ = combineLatest([
|
||||
isAccountDeprovisioningEnabled$,
|
||||
userIsManagedByOrganization$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
|
||||
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
|
||||
),
|
||||
this.showDeleteAccount$ = userIsManagedByOrganization$.pipe(
|
||||
map((userIsManagedByOrganization) => !userIsManagedByOrganization),
|
||||
);
|
||||
|
||||
this.accountService.accountVerifyNewDeviceLogin$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((verifyDevices) => {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
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 { TypographyModule } from "@bitwarden/components";
|
||||
|
||||
/**
|
||||
@@ -18,13 +15,4 @@ import { TypographyModule } from "@bitwarden/components";
|
||||
standalone: true,
|
||||
imports: [TypographyModule, JslibModule, CommonModule],
|
||||
})
|
||||
export class DangerZoneComponent implements OnInit {
|
||||
constructor(private configService: ConfigService) {}
|
||||
accountDeprovisioningEnabled$: Observable<boolean>;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
}
|
||||
}
|
||||
export class DangerZoneComponent {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
import { firstValueFrom, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -10,9 +10,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
@@ -40,7 +38,6 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
private accountService: AccountService,
|
||||
private dialogService: DialogService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
@@ -53,21 +50,12 @@ export class ProfileComponent implements OnInit, OnDestroy {
|
||||
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.managingOrganization$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.AccountDeprovisioning)
|
||||
this.managingOrganization$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(
|
||||
switchMap((isAccountDeprovisioningEnabled) =>
|
||||
isAccountDeprovisioningEnabled
|
||||
? this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(
|
||||
map((organizations) =>
|
||||
organizations.find((o) => o.userIsManagedByOrganization === true),
|
||||
),
|
||||
)
|
||||
: of(null),
|
||||
),
|
||||
map((organizations) => organizations.find((o) => o.userIsManagedByOrganization === true)),
|
||||
);
|
||||
|
||||
this.formGroup.get("name").setValue(this.profile.name);
|
||||
this.formGroup.get("email").setValue(this.profile.email);
|
||||
|
||||
|
||||
@@ -310,13 +310,16 @@ export class ChangePasswordComponent
|
||||
newMasterKey: MasterKey,
|
||||
newUserKey: [UserKey, EncString],
|
||||
) {
|
||||
const masterKey = await this.keyService.makeMasterKey(
|
||||
this.currentMasterPassword,
|
||||
await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email))),
|
||||
await this.kdfConfigService.getKdfConfig(),
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
|
||||
const masterKey = await this.keyService.makeMasterKey(
|
||||
this.currentMasterPassword,
|
||||
email,
|
||||
await this.kdfConfigService.getKdfConfig(userId),
|
||||
);
|
||||
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const newLocalKeyHash = await this.keyService.hashMasterKey(
|
||||
this.masterPassword,
|
||||
newMasterKey,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
<ng-container *ngIf="currentCipher.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
class="bwi bwi-collection-shared"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -8,14 +8,16 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserId, EmergencyAccessId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService, DialogRef, DIALOG_DATA } from "@bitwarden/components";
|
||||
import { ChangeLoginPasswordService } from "@bitwarden/vault";
|
||||
@@ -28,14 +30,15 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
|
||||
const open = jest.fn();
|
||||
const close = jest.fn();
|
||||
const emergencyAccessId = "emergency-access-id" as EmergencyAccessId;
|
||||
|
||||
const mockCipher = {
|
||||
id: "cipher1",
|
||||
name: "Cipher",
|
||||
type: CipherType.Login,
|
||||
login: { uris: [] },
|
||||
login: { uris: [] } as Partial<LoginView>,
|
||||
card: {},
|
||||
} as CipherView;
|
||||
} as Partial<CipherView> as CipherView;
|
||||
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(Utils.newGuid() as UserId);
|
||||
|
||||
@@ -56,6 +59,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: TaskService, useValue: mock<TaskService>() },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(EmergencyViewDialogComponent, {
|
||||
@@ -94,18 +98,24 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
});
|
||||
|
||||
it("opens dialog", () => {
|
||||
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher });
|
||||
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, {
|
||||
cipher: mockCipher,
|
||||
emergencyAccessId,
|
||||
});
|
||||
|
||||
expect(open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes the dialog", () => {
|
||||
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, { cipher: mockCipher });
|
||||
EmergencyViewDialogComponent.open({ open } as unknown as DialogService, {
|
||||
cipher: mockCipher,
|
||||
emergencyAccessId,
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
const cancelButton = fixture.debugElement.queryAll(By.css("button")).pop();
|
||||
|
||||
cancelButton.nativeElement.click();
|
||||
cancelButton!.nativeElement.click();
|
||||
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
KdfConfigService,
|
||||
@@ -43,6 +45,7 @@ export class ChangeKdfComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private accountService: AccountService,
|
||||
private formBuilder: FormBuilder,
|
||||
) {
|
||||
this.kdfOptions = [
|
||||
@@ -52,7 +55,8 @@ export class ChangeKdfComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.kdfConfig = await this.kdfConfigService.getKdfConfig(userId);
|
||||
this.formGroup.get("kdf").setValue(this.kdfConfig.kdfType);
|
||||
this.setFormControlValues(this.kdfConfig);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "email" | i18n }}:</bit-label>
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="email"
|
||||
@@ -19,13 +19,12 @@
|
||||
</div>
|
||||
<div class="tw-col-span-12">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "notes" | i18n }}:</bit-label>
|
||||
<bit-label>{{ "notes" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
inputmode="text"
|
||||
formControlName="sponsorshipNote"
|
||||
[attr.aria-invalid]="sponsorshipNoteControl.invalid"
|
||||
appInputStripSpaces
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
@@ -34,7 +33,15 @@
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="button" buttonType="primary" (click)="save()">
|
||||
<button
|
||||
bitButton
|
||||
bitFormButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
[loading]="loading"
|
||||
[disabled]="loading"
|
||||
(click)="save()"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" [bitDialogClose]="false">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
@@ -10,32 +10,30 @@ import {
|
||||
ValidationErrors,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PlanSponsorshipType } from "@bitwarden/common/billing/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ButtonModule, DialogModule, DialogService, FormFieldModule } from "@bitwarden/components";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
interface RequestSponsorshipForm {
|
||||
sponsorshipEmail: FormControl<string | null>;
|
||||
sponsorshipNote: FormControl<string | null>;
|
||||
}
|
||||
|
||||
export interface AddSponsorshipDialogResult {
|
||||
action: AddSponsorshipDialogAction;
|
||||
value: Partial<AddSponsorshipFormValue> | null;
|
||||
}
|
||||
|
||||
interface AddSponsorshipFormValue {
|
||||
sponsorshipEmail: string;
|
||||
sponsorshipNote: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
enum AddSponsorshipDialogAction {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
interface AddSponsorshipDialogParams {
|
||||
organizationId: string;
|
||||
organizationKey: OrgKey;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -53,54 +51,86 @@ enum AddSponsorshipDialogAction {
|
||||
export class AddSponsorshipDialogComponent {
|
||||
sponsorshipForm: FormGroup<RequestSponsorshipForm>;
|
||||
loading = false;
|
||||
organizationId: string;
|
||||
organizationKey: OrgKey;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef<AddSponsorshipDialogResult>,
|
||||
private dialogRef: DialogRef,
|
||||
private formBuilder: FormBuilder,
|
||||
private accountService: AccountService,
|
||||
private i18nService: I18nService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private toastService: ToastService,
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
|
||||
@Inject(DIALOG_DATA) protected dialogParams: AddSponsorshipDialogParams,
|
||||
) {
|
||||
this.organizationId = this.dialogParams?.organizationId;
|
||||
this.organizationKey = this.dialogParams.organizationKey;
|
||||
|
||||
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
|
||||
sponsorshipEmail: new FormControl<string | null>("", {
|
||||
validators: [Validators.email, Validators.required],
|
||||
asyncValidators: [this.validateNotCurrentUserEmail.bind(this)],
|
||||
asyncValidators: [this.isOrganizationMember.bind(this)],
|
||||
updateOn: "change",
|
||||
}),
|
||||
sponsorshipNote: new FormControl<string | null>("", {}),
|
||||
sponsorshipNote: new FormControl<string | null>("", {
|
||||
validators: [Validators.maxLength(1000)],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService): DialogRef<AddSponsorshipDialogResult> {
|
||||
return dialogService.open<AddSponsorshipDialogResult>(AddSponsorshipDialogComponent);
|
||||
static open(dialogService: DialogService, config: DialogConfig<AddSponsorshipDialogParams>) {
|
||||
return dialogService.open(AddSponsorshipDialogComponent, {
|
||||
...config,
|
||||
data: config.data,
|
||||
} as unknown as DialogConfig<unknown, DialogRef>);
|
||||
}
|
||||
|
||||
protected async save() {
|
||||
this.sponsorshipEmailControl.markAllAsTouched();
|
||||
|
||||
if (this.sponsorshipForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
// TODO: This is a mockup implementation - needs to be updated with actual API integration
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Simulate API call
|
||||
|
||||
const formValue = this.sponsorshipForm.getRawValue();
|
||||
const dialogValue: Partial<AddSponsorshipFormValue> = {
|
||||
status: "Sent",
|
||||
sponsorshipEmail: formValue.sponsorshipEmail ?? "",
|
||||
sponsorshipNote: formValue.sponsorshipNote ?? "",
|
||||
};
|
||||
try {
|
||||
const notes = this.sponsorshipForm.value.sponsorshipNote || "";
|
||||
const email = this.sponsorshipForm.value.sponsorshipEmail || "";
|
||||
|
||||
this.dialogRef.close({
|
||||
action: AddSponsorshipDialogAction.Saved,
|
||||
value: dialogValue,
|
||||
});
|
||||
const encryptedNotes = await this.encryptService.encryptString(notes, this.organizationKey);
|
||||
const isAdminInitiated = true;
|
||||
await this.apiService.postCreateSponsorship(this.organizationId, {
|
||||
sponsoredEmail: email,
|
||||
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
|
||||
friendlyName: email,
|
||||
isAdminInitiated,
|
||||
notes: encryptedNotes.encryptedString,
|
||||
});
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: undefined,
|
||||
message: this.i18nService.t("sponsorshipCreated"),
|
||||
});
|
||||
await this.resetForm();
|
||||
} catch (e: any) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e?.message || this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
protected close = () => {
|
||||
this.dialogRef.close({ action: AddSponsorshipDialogAction.Canceled, value: null });
|
||||
};
|
||||
private async resetForm() {
|
||||
this.sponsorshipForm.reset();
|
||||
}
|
||||
|
||||
get sponsorshipEmailControl() {
|
||||
return this.sponsorshipForm.controls.sponsorshipEmail;
|
||||
@@ -110,24 +140,21 @@ export class AddSponsorshipDialogComponent {
|
||||
return this.sponsorshipForm.controls.sponsorshipNote;
|
||||
}
|
||||
|
||||
private async validateNotCurrentUserEmail(
|
||||
control: AbstractControl,
|
||||
): Promise<ValidationErrors | null> {
|
||||
private async isOrganizationMember(control: AbstractControl): Promise<ValidationErrors | null> {
|
||||
const value = control.value;
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentUserEmail = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email ?? "")),
|
||||
const users = await this.organizationUserApiService.getAllMiniUserDetails(this.organizationId);
|
||||
|
||||
const userExists = users.data.some(
|
||||
(member) => member.email.toLowerCase() === value.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!currentUserEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.toLowerCase() === currentUserEmail.toLowerCase()) {
|
||||
return { currentUserEmail: true };
|
||||
if (userExists) {
|
||||
return {
|
||||
isOrganizationMember: {
|
||||
message: this.i18nService.t("organizationHasMemberMessage", value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -5,19 +5,98 @@
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex">
|
||||
<bit-tab [label]="'sponsoredBitwardenFamilies' | i18n">
|
||||
<app-organization-sponsored-families
|
||||
[sponsoredFamilies]="sponsoredFamilies"
|
||||
(removeSponsorshipEvent)="removeSponsorhip($event)"
|
||||
></app-organization-sponsored-families>
|
||||
</bit-tab>
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "sponsorshipFreeBitwardenFamilies" | i18n }}
|
||||
</p>
|
||||
<div bitTypography="body1">
|
||||
{{ "sponsoredFamiliesIncludeMessage" | i18n }}:
|
||||
<ul class="tw-list-outside">
|
||||
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
|
||||
<li>{{ "sponsoredFamiliesSharedCollectionsForFamilyMembers" | i18n }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<bit-tab [label]="'memberFamilies' | i18n">
|
||||
<app-organization-member-families
|
||||
[memberFamilies]="sponsoredFamilies"
|
||||
></app-organization-member-families>
|
||||
</bit-tab>
|
||||
</bit-tab-group>
|
||||
<h2 bitTypography="h2" class="">{{ "sponsoredBitwardenFamilies" | i18n }}</h2>
|
||||
|
||||
<p class="tw-px-4" bitTypography="body2">{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}</p>
|
||||
@if (loading()) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading() && sponsoredFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "recipient" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "notes" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (sponsoredFamily of sponsoredFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ sponsoredFamily.friendlyName }}</td>
|
||||
<td bitCell [class]="sponsoredFamily.statusClass">
|
||||
{{ sponsoredFamily.statusMessage }}
|
||||
</td>
|
||||
<td bitCell>{{ sponsoredFamily.notes }}</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n"
|
||||
*ngIf="!isSelfHosted && !sponsoredFamily.validUntil"
|
||||
(click)="resendEmail(sponsoredFamily)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
|
||||
<hr class="m-0" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'revokeAccountMessage' | i18n"
|
||||
(click)="removeSponsorship(sponsoredFamily)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-close tw-text-danger"></i>
|
||||
<span class="tw-text-danger pl-1">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else if (!loading()) {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noSponsoredFamiliesMessage" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "nosponsoredFamiliesDetails" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading() && sponsoredFamilies.length > 0) {
|
||||
<p bitTypography="body2">{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}</p>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
|
||||
@@ -1,62 +1,262 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { formatDate } from "@angular/common";
|
||||
import { Component, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction";
|
||||
import { OrganizationSponsorshipInvitesResponse } from "@bitwarden/common/billing/models/response/organization-sponsorship-invites.response";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
|
||||
|
||||
import {
|
||||
AddSponsorshipDialogComponent,
|
||||
AddSponsorshipDialogResult,
|
||||
} from "./add-sponsorship-dialog.component";
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-free-bitwarden-families",
|
||||
templateUrl: "free-bitwarden-families.component.html",
|
||||
})
|
||||
export class FreeBitwardenFamiliesComponent implements OnInit {
|
||||
loading = signal<boolean>(true);
|
||||
tabIndex = 0;
|
||||
sponsoredFamilies: SponsoredFamily[] = [];
|
||||
sponsoredFamilies: OrganizationSponsorshipInvitesResponse[] = [];
|
||||
|
||||
organizationId = "";
|
||||
organizationKey$: Observable<OrgKey>;
|
||||
|
||||
private locale: string = "";
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private dialogService: DialogService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
) {}
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
private keyService: KeyService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
private organizationSponsorshipApiService: OrganizationSponsorshipApiServiceAbstraction,
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.organizationId = this.route.snapshot.params.organizationId || "";
|
||||
this.organizationKey$ = this.stateProvider.activeUserId$.pipe(
|
||||
switchMap(
|
||||
(userId) =>
|
||||
this.keyService.orgKeys$(userId as UserId) as Observable<Record<OrganizationId, OrgKey>>,
|
||||
),
|
||||
map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]),
|
||||
takeUntilDestroyed(),
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.preventAccessToFreeFamiliesPage();
|
||||
this.locale = await firstValueFrom(this.i18nService.locale$);
|
||||
await this.loadSponsorships();
|
||||
|
||||
this.loading.set(false);
|
||||
}
|
||||
|
||||
async loadSponsorships() {
|
||||
if (!this.organizationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [response, orgKey] = await Promise.all([
|
||||
this.organizationSponsorshipApiService.getOrganizationSponsorship(this.organizationId),
|
||||
firstValueFrom(this.organizationKey$),
|
||||
]);
|
||||
|
||||
if (!orgKey) {
|
||||
this.logService.error("Organization key not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const organizationFamilies = response.data;
|
||||
|
||||
this.sponsoredFamilies = await Promise.all(
|
||||
organizationFamilies.map(async (family) => {
|
||||
let decryptedNote = "";
|
||||
try {
|
||||
decryptedNote = await this.encryptService.decryptString(
|
||||
new EncString(family.notes),
|
||||
orgKey,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
const { statusMessage, statusClass } = this.getStatus(
|
||||
this.isSelfHosted,
|
||||
family.toDelete,
|
||||
family.validUntil,
|
||||
family.lastSyncDate,
|
||||
this.locale,
|
||||
);
|
||||
|
||||
const newFamily = {
|
||||
...family,
|
||||
notes: decryptedNote,
|
||||
statusMessage: statusMessage || "",
|
||||
statusClass: statusClass || "tw-text-success",
|
||||
status: statusMessage || "",
|
||||
};
|
||||
|
||||
return new OrganizationSponsorshipInvitesResponse(newFamily);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async addSponsorship() {
|
||||
const addSponsorshipDialogRef: DialogRef<AddSponsorshipDialogResult> =
|
||||
AddSponsorshipDialogComponent.open(this.dialogService);
|
||||
const addSponsorshipDialogRef: DialogRef = AddSponsorshipDialogComponent.open(
|
||||
this.dialogService,
|
||||
{
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
organizationKey: await firstValueFrom(this.organizationKey$),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed);
|
||||
await firstValueFrom(addSponsorshipDialogRef.closed);
|
||||
|
||||
if (dialogRef?.value) {
|
||||
this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies];
|
||||
await this.loadSponsorships();
|
||||
}
|
||||
|
||||
async removeSponsorship(sponsorship: OrganizationSponsorshipInvitesResponse) {
|
||||
try {
|
||||
await this.doRevokeSponsorship(sponsorship);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
removeSponsorhip(sponsorship: any) {
|
||||
const index = this.sponsoredFamilies.findIndex(
|
||||
(e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail,
|
||||
);
|
||||
this.sponsoredFamilies.splice(index, 1);
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
private async preventAccessToFreeFamiliesPage() {
|
||||
const showFreeFamiliesPage = await firstValueFrom(
|
||||
this.freeFamiliesPolicyService.showFreeFamilies$,
|
||||
async resendEmail(sponsorship: OrganizationSponsorshipInvitesResponse) {
|
||||
await this.organizationSponsorshipApiService.postResendSponsorshipOffer(
|
||||
this.organizationId,
|
||||
sponsorship.friendlyName,
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: undefined,
|
||||
message: this.i18nService.t("emailSent"),
|
||||
});
|
||||
}
|
||||
|
||||
if (!showFreeFamiliesPage) {
|
||||
await this.router.navigate(["/"]);
|
||||
private async doRevokeSponsorship(sponsorship: OrganizationSponsorshipInvitesResponse) {
|
||||
const content = sponsorship.validUntil
|
||||
? this.i18nService.t(
|
||||
"updatedRevokeSponsorshipConfirmationForAcceptedSponsorship",
|
||||
sponsorship.friendlyName,
|
||||
formatDate(sponsorship.validUntil, "MM/dd/yyyy", this.locale),
|
||||
)
|
||||
: this.i18nService.t(
|
||||
"updatedRevokeSponsorshipConfirmationForSentSponsorship",
|
||||
sponsorship.friendlyName,
|
||||
);
|
||||
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: `${this.i18nService.t("removeSponsorship")}?`,
|
||||
content,
|
||||
acceptButtonText: { key: "remove" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.apiService.deleteRevokeSponsorship(this.organizationId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: undefined,
|
||||
message: this.i18nService.t("reclaimedFreePlan"),
|
||||
});
|
||||
|
||||
await this.loadSponsorships();
|
||||
}
|
||||
|
||||
private getStatus(
|
||||
selfHosted: boolean,
|
||||
toDelete?: boolean,
|
||||
validUntil?: Date,
|
||||
lastSyncDate?: Date,
|
||||
locale: string = "",
|
||||
): { statusMessage: string; statusClass: "tw-text-success" | "tw-text-danger" } {
|
||||
/*
|
||||
* Possible Statuses:
|
||||
* Requested (self-hosted only)
|
||||
* Sent
|
||||
* Active
|
||||
* RequestRevoke
|
||||
* RevokeWhenExpired
|
||||
*/
|
||||
|
||||
if (toDelete && validUntil) {
|
||||
// They want to delete but there is a valid until date which means there is an active sponsorship
|
||||
return {
|
||||
statusMessage: this.i18nService.t(
|
||||
"revokeWhenExpired",
|
||||
formatDate(validUntil, "MM/dd/yyyy", locale),
|
||||
),
|
||||
statusClass: "tw-text-danger",
|
||||
};
|
||||
}
|
||||
|
||||
if (toDelete) {
|
||||
// They want to delete and we don't have a valid until date so we can
|
||||
// this should only happen on a self-hosted install
|
||||
return {
|
||||
statusMessage: this.i18nService.t("requestRemoved"),
|
||||
statusClass: "tw-text-danger",
|
||||
};
|
||||
}
|
||||
|
||||
if (validUntil) {
|
||||
// They don't want to delete and they have a valid until date
|
||||
// that means they are actively sponsoring someone
|
||||
return {
|
||||
statusMessage: this.i18nService.t("active"),
|
||||
statusClass: "tw-text-success",
|
||||
};
|
||||
}
|
||||
|
||||
if (selfHosted && lastSyncDate) {
|
||||
// We are on a self-hosted install and it has been synced but we have not gotten
|
||||
// a valid until date so we can't know if they are actively sponsoring someone
|
||||
return {
|
||||
statusMessage: this.i18nService.t("sent"),
|
||||
statusClass: "tw-text-success",
|
||||
};
|
||||
}
|
||||
|
||||
if (!selfHosted) {
|
||||
// We are in cloud and all other status checks have been false therefore we have
|
||||
// sent the request but it hasn't been accepted yet
|
||||
return {
|
||||
statusMessage: this.i18nService.t("sent"),
|
||||
statusClass: "tw-text-success",
|
||||
};
|
||||
}
|
||||
|
||||
// We are on a self-hosted install and we have not synced yet
|
||||
return {
|
||||
statusMessage: this.i18nService.t("requested"),
|
||||
statusClass: "tw-text-success",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "membersWithSponsoredFamilies" | i18n }}
|
||||
</p>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "memberFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && memberFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "member" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of memberFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noMemberFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noMemberFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-member-families",
|
||||
templateUrl: "organization-member-families.component.html",
|
||||
})
|
||||
export class OrganizationMemberFamiliesComponent implements OnInit, OnDestroy {
|
||||
tabIndex = 0;
|
||||
loading = false;
|
||||
|
||||
@Input() memberFamilies: SponsoredFamily[] = [];
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
<bit-container>
|
||||
<ng-container>
|
||||
<p bitTypography="body1">
|
||||
{{ "sponsorFreeBitwardenFamilies" | i18n }}
|
||||
</p>
|
||||
<div bitTypography="body1">
|
||||
{{ "sponsoredFamiliesInclude" | i18n }}:
|
||||
<ul class="tw-list-outside">
|
||||
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
|
||||
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 bitTypography="h2" class="">{{ "sponsoredBitwardenFamilies" | i18n }}</h2>
|
||||
|
||||
@if (loading) {
|
||||
<ng-container>
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@if (!loading && sponsoredFamilies?.length > 0) {
|
||||
<ng-container>
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "recipient" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "notes" | i18n }}</th>
|
||||
<th bitCell></th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body alignContent="middle">
|
||||
@for (o of sponsoredFamilies; let i = $index; track i) {
|
||||
<ng-container>
|
||||
<tr bitRow>
|
||||
<td bitCell>{{ o.sponsorshipEmail }}</td>
|
||||
<td bitCell class="tw-text-success">{{ o.status }}</td>
|
||||
<td bitCell>{{ o.sponsorshipNote }}</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'resendEmailLabel' | i18n"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
|
||||
<hr class="m-0" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
[attr.aria-label]="'revokeAccount' | i18n"
|
||||
(click)="remove(o)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-close tw-text-danger"></i>
|
||||
<span class="tw-text-danger pl-1">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<hr class="mt-0" />
|
||||
</ng-container>
|
||||
} @else {
|
||||
<div class="tw-my-5 tw-py-5 tw-flex tw-flex-col tw-items-center">
|
||||
<img class="tw-w-32" src="./../../../images/search.svg" alt="Search" />
|
||||
<h4 class="mt-3" bitTypography="h4">{{ "noSponsoredFamilies" | i18n }}</h4>
|
||||
<p bitTypography="body2">{{ "noSponsoredFamiliesDescription" | i18n }}</p>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
import { SponsoredFamily } from "./types/sponsored-family";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-sponsored-families",
|
||||
templateUrl: "organization-sponsored-families.component.html",
|
||||
})
|
||||
export class OrganizationSponsoredFamiliesComponent implements OnInit, OnDestroy {
|
||||
loading = false;
|
||||
tabIndex = 0;
|
||||
|
||||
@Input() sponsoredFamilies: SponsoredFamily[] = [];
|
||||
@Output() removeSponsorshipEvent = new EventEmitter<SponsoredFamily>();
|
||||
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
remove(sponsorship: SponsoredFamily) {
|
||||
this.removeSponsorshipEvent.emit(sponsorship);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
}
|
||||
}
|
||||
@@ -150,10 +150,11 @@
|
||||
>{{
|
||||
"costPerMember"
|
||||
| i18n
|
||||
: ((selectableProduct.isAnnual
|
||||
? selectableProduct.PasswordManager.seatPrice / 12
|
||||
: selectableProduct.PasswordManager.seatPrice
|
||||
)
|
||||
: (((this.sub.useSecretsManager
|
||||
? selectableProduct.SecretsManager.seatPrice
|
||||
: 0) +
|
||||
selectableProduct.PasswordManager.seatPrice) /
|
||||
(selectableProduct.isAnnual ? 12 : 1)
|
||||
| currency: "$")
|
||||
}}
|
||||
</b>
|
||||
@@ -983,7 +984,7 @@
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "upgrade" | i18n }}
|
||||
{{ submitButtonLabel }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs";
|
||||
import { firstValueFrom, map, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -31,10 +31,10 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import {
|
||||
BillingApiServiceAbstraction,
|
||||
BillingInformation,
|
||||
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
||||
OrganizationInformation,
|
||||
PaymentInformation,
|
||||
PlanInformation,
|
||||
OrganizationBillingServiceAbstraction as OrganizationBillingService,
|
||||
} from "@bitwarden/common/billing/abstractions";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import {
|
||||
@@ -744,7 +744,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
const doSubmit = async (): Promise<string> => {
|
||||
let orgId: string = null;
|
||||
if (this.isSubscriptionCanceled) {
|
||||
if (this.sub?.subscription?.status === "canceled") {
|
||||
await this.restartSubscription();
|
||||
orgId = this.organizationId;
|
||||
} else {
|
||||
@@ -1089,4 +1089,15 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
this.isSubscriptionCanceled
|
||||
);
|
||||
}
|
||||
|
||||
get submitButtonLabel(): string {
|
||||
if (
|
||||
this.organization.productTierType !== ProductTierType.Free &&
|
||||
this.sub.subscription.status === "canceled"
|
||||
) {
|
||||
return this.i18nService.t("restart");
|
||||
} else {
|
||||
return this.i18nService.t("upgrade");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +494,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
};
|
||||
|
||||
get showChangePlanButton() {
|
||||
return this.sub.plan.productTier !== ProductTierType.Enterprise && !this.showChangePlan;
|
||||
return (
|
||||
!this.showChangePlan &&
|
||||
this.sub.plan.productTier !== ProductTierType.Enterprise &&
|
||||
!this.sub.subscription?.cancelled
|
||||
);
|
||||
}
|
||||
|
||||
get canUseBillingSync() {
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { firstValueFrom, lastValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationWarningsService } from "./organization-warnings.service";
|
||||
|
||||
describe("OrganizationWarningsService", () => {
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationBillingApiService: MockProxy<OrganizationBillingApiServiceAbstraction>;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
let organizationWarningsService: OrganizationWarningsService;
|
||||
|
||||
const respond = (responseBody: any) =>
|
||||
Promise.resolve(new OrganizationWarningsResponse(responseBody));
|
||||
|
||||
const empty = () => Promise.resolve(new OrganizationWarningsResponse({}));
|
||||
|
||||
beforeEach(() => {
|
||||
dialogService = mock<DialogService>();
|
||||
i18nService = mock<I18nService>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
organizationBillingApiService = mock<OrganizationBillingApiServiceAbstraction>();
|
||||
router = mock<Router>();
|
||||
|
||||
organizationWarningsService = new OrganizationWarningsService(
|
||||
dialogService,
|
||||
i18nService,
|
||||
organizationApiService,
|
||||
organizationBillingApiService,
|
||||
router,
|
||||
);
|
||||
});
|
||||
|
||||
describe("cache$", () => {
|
||||
it("should only request warnings once for a specific organization and replay the cached result for multiple subscriptions", async () => {
|
||||
const response1 = respond({
|
||||
freeTrial: {
|
||||
remainingTrialDays: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const organization1 = {
|
||||
id: "1",
|
||||
name: "Test",
|
||||
} as Organization;
|
||||
|
||||
const response2 = respond({
|
||||
freeTrial: {
|
||||
remainingTrialDays: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const organization2 = {
|
||||
id: "2",
|
||||
name: "Test",
|
||||
} as Organization;
|
||||
|
||||
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||
if (id === organization1.id) {
|
||||
return response1;
|
||||
}
|
||||
|
||||
if (id === organization2.id) {
|
||||
return response2;
|
||||
}
|
||||
|
||||
return empty();
|
||||
});
|
||||
|
||||
const oneDayRemainingTranslation = "oneDayRemaining";
|
||||
const twoDaysRemainingTranslation = "twoDaysRemaining";
|
||||
|
||||
i18nService.t.mockImplementation((id, p1) => {
|
||||
if (id === "freeTrialEndPromptTomorrowNoOrgName") {
|
||||
return oneDayRemainingTranslation;
|
||||
}
|
||||
|
||||
if (id === "freeTrialEndPromptCount" && p1 === 2) {
|
||||
return twoDaysRemainingTranslation;
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
const organization1Subscription1 = await firstValueFrom(
|
||||
organizationWarningsService.getFreeTrialWarning$(organization1),
|
||||
);
|
||||
|
||||
const organization1Subscription2 = await firstValueFrom(
|
||||
organizationWarningsService.getFreeTrialWarning$(organization1),
|
||||
);
|
||||
|
||||
expect(organization1Subscription1).toEqual({
|
||||
organization: organization1,
|
||||
message: oneDayRemainingTranslation,
|
||||
});
|
||||
|
||||
expect(organization1Subscription2).toEqual(organization1Subscription1);
|
||||
|
||||
const organization2Subscription1 = await firstValueFrom(
|
||||
organizationWarningsService.getFreeTrialWarning$(organization2),
|
||||
);
|
||||
|
||||
const organization2Subscription2 = await firstValueFrom(
|
||||
organizationWarningsService.getFreeTrialWarning$(organization2),
|
||||
);
|
||||
|
||||
expect(organization2Subscription1).toEqual({
|
||||
organization: organization2,
|
||||
message: twoDaysRemainingTranslation,
|
||||
});
|
||||
|
||||
expect(organization2Subscription2).toEqual(organization2Subscription1);
|
||||
|
||||
expect(organizationBillingApiService.getWarnings).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFreeTrialWarning$", () => {
|
||||
it("should not emit a free trial warning when none is included in the warnings response", (done) => {
|
||||
const organization = {
|
||||
id: "1",
|
||||
name: "Test",
|
||||
} as Organization;
|
||||
|
||||
organizationBillingApiService.getWarnings.mockReturnValue(empty());
|
||||
|
||||
const warning$ = organizationWarningsService.getFreeTrialWarning$(organization);
|
||||
|
||||
warning$.subscribe({
|
||||
next: () => {
|
||||
fail("Observable should not emit a value.");
|
||||
},
|
||||
complete: () => {
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit a free trial warning when one is included in the warnings response", async () => {
|
||||
const response = respond({
|
||||
freeTrial: {
|
||||
remainingTrialDays: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const organization = {
|
||||
id: "1",
|
||||
name: "Test",
|
||||
} as Organization;
|
||||
|
||||
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||
if (id === organization.id) {
|
||||
return response;
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
});
|
||||
|
||||
const translation = "translation";
|
||||
i18nService.t.mockImplementation((id) => {
|
||||
if (id === "freeTrialEndPromptTomorrowNoOrgName") {
|
||||
return translation;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const warning = await firstValueFrom(
|
||||
organizationWarningsService.getFreeTrialWarning$(organization),
|
||||
);
|
||||
|
||||
expect(warning).toEqual({
|
||||
organization,
|
||||
message: translation,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getResellerRenewalWarning$", () => {
|
||||
it("should not emit a reseller renewal warning when none is included in the warnings response", (done) => {
|
||||
const organization = {
|
||||
id: "1",
|
||||
name: "Test",
|
||||
} as Organization;
|
||||
|
||||
organizationBillingApiService.getWarnings.mockReturnValue(empty());
|
||||
|
||||
const warning$ = organizationWarningsService.getResellerRenewalWarning$(organization);
|
||||
|
||||
warning$.subscribe({
|
||||
next: () => {
|
||||
fail("Observable should not emit a value.");
|
||||
},
|
||||
complete: () => {
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit a reseller renewal warning when one is included in the warnings response", async () => {
|
||||
const response = respond({
|
||||
resellerRenewal: {
|
||||
type: "upcoming",
|
||||
upcoming: {
|
||||
renewalDate: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const organization = {
|
||||
id: "1",
|
||||
name: "Test",
|
||||
providerName: "Provider",
|
||||
} as Organization;
|
||||
|
||||
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||
if (id === organization.id) {
|
||||
return response;
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
});
|
||||
|
||||
const formattedDate = new Date("2026-01-01T00:00:00.000Z").toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const translation = "translation";
|
||||
i18nService.t.mockImplementation((id, p1, p2) => {
|
||||
if (
|
||||
id === "resellerRenewalWarningMsg" &&
|
||||
p1 === organization.providerName &&
|
||||
p2 === formattedDate
|
||||
) {
|
||||
return translation;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const warning = await firstValueFrom(
|
||||
organizationWarningsService.getResellerRenewalWarning$(organization),
|
||||
);
|
||||
|
||||
expect(warning).toEqual({
|
||||
type: "info",
|
||||
message: translation,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("showInactiveSubscriptionDialog$", () => {
|
||||
it("should not emit the opening of a dialog for an inactive subscription warning when the warning is not included in the warnings response", (done) => {
|
||||
const organization = {
|
||||
id: "1",
|
||||
name: "Test",
|
||||
} as Organization;
|
||||
|
||||
organizationBillingApiService.getWarnings.mockReturnValue(empty());
|
||||
|
||||
const warning$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization);
|
||||
|
||||
warning$.subscribe({
|
||||
next: () => {
|
||||
fail("Observable should not emit a value.");
|
||||
},
|
||||
complete: () => {
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit the opening of a dialog for an inactive subscription warning when the warning is included in the warnings response", async () => {
|
||||
const response = respond({
|
||||
inactiveSubscription: {
|
||||
resolution: "add_payment_method",
|
||||
},
|
||||
});
|
||||
|
||||
const organization = {
|
||||
id: "1",
|
||||
name: "Test",
|
||||
providerName: "Provider",
|
||||
} as Organization;
|
||||
|
||||
organizationBillingApiService.getWarnings.mockImplementation((id) => {
|
||||
if (id === organization.id) {
|
||||
return response;
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
});
|
||||
|
||||
const titleTranslation = "title";
|
||||
const continueTranslation = "continue";
|
||||
const closeTranslation = "close";
|
||||
|
||||
i18nService.t.mockImplementation((id, param) => {
|
||||
if (id === "suspendedOrganizationTitle" && param === organization.name) {
|
||||
return titleTranslation;
|
||||
}
|
||||
if (id === "continue") {
|
||||
return continueTranslation;
|
||||
}
|
||||
if (id === "close") {
|
||||
return closeTranslation;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const expectedOptions = {
|
||||
title: titleTranslation,
|
||||
content: {
|
||||
key: "suspendedOwnerOrgMessage",
|
||||
},
|
||||
type: "danger",
|
||||
acceptButtonText: continueTranslation,
|
||||
cancelButtonText: closeTranslation,
|
||||
} as SimpleDialogOptions;
|
||||
|
||||
dialogService.openSimpleDialog.mockImplementation((options) => {
|
||||
if (JSON.stringify(options) == JSON.stringify(expectedOptions)) {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
const observable$ = organizationWarningsService.showInactiveSubscriptionDialog$(organization);
|
||||
|
||||
const routerNavigateSpy = jest.spyOn(router, "navigate").mockResolvedValue(true);
|
||||
|
||||
await lastValueFrom(observable$);
|
||||
|
||||
expect(routerNavigateSpy).toHaveBeenCalledWith(
|
||||
["organizations", `${organization.id}`, "billing", "payment-method"],
|
||||
{
|
||||
state: { launchPaymentModalAutomatically: true },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import {
|
||||
filter,
|
||||
from,
|
||||
lastValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
takeWhile,
|
||||
} from "rxjs";
|
||||
import { take } from "rxjs/operators";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||
import { OrganizationWarningsResponse } from "@bitwarden/common/billing/models/response/organization-warnings.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { openChangePlanDialog } from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
|
||||
|
||||
const format = (date: Date) =>
|
||||
date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
export type FreeTrialWarning = {
|
||||
organization: Pick<Organization, "id" & "name">;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ResellerRenewalWarning = {
|
||||
type: "info" | "warning";
|
||||
message: string;
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class OrganizationWarningsService {
|
||||
private cache$ = new Map<OrganizationId, Observable<OrganizationWarningsResponse>>();
|
||||
|
||||
constructor(
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private organizationBillingApiService: OrganizationBillingApiServiceAbstraction,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
getFreeTrialWarning$ = (organization: Organization): Observable<FreeTrialWarning> =>
|
||||
this.getWarning$(organization, (response) => response.freeTrial).pipe(
|
||||
map((warning) => {
|
||||
const { remainingTrialDays } = warning;
|
||||
|
||||
if (remainingTrialDays >= 2) {
|
||||
return {
|
||||
organization,
|
||||
message: this.i18nService.t("freeTrialEndPromptCount", remainingTrialDays),
|
||||
};
|
||||
}
|
||||
|
||||
if (remainingTrialDays == 1) {
|
||||
return {
|
||||
organization,
|
||||
message: this.i18nService.t("freeTrialEndPromptTomorrowNoOrgName"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
organization,
|
||||
message: this.i18nService.t("freeTrialEndingTodayWithoutOrgName"),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
getResellerRenewalWarning$ = (organization: Organization): Observable<ResellerRenewalWarning> =>
|
||||
this.getWarning$(organization, (response) => response.resellerRenewal).pipe(
|
||||
map((warning): ResellerRenewalWarning | null => {
|
||||
switch (warning.type) {
|
||||
case "upcoming": {
|
||||
return {
|
||||
type: "info",
|
||||
message: this.i18nService.t(
|
||||
"resellerRenewalWarningMsg",
|
||||
organization.providerName,
|
||||
format(warning.upcoming!.renewalDate),
|
||||
),
|
||||
};
|
||||
}
|
||||
case "issued": {
|
||||
return {
|
||||
type: "info",
|
||||
message: this.i18nService.t(
|
||||
"resellerOpenInvoiceWarningMgs",
|
||||
organization.providerName,
|
||||
format(warning.issued!.issuedDate),
|
||||
format(warning.issued!.dueDate),
|
||||
),
|
||||
};
|
||||
}
|
||||
case "past_due": {
|
||||
return {
|
||||
type: "warning",
|
||||
message: this.i18nService.t(
|
||||
"resellerPastDueWarningMsg",
|
||||
organization.providerName,
|
||||
format(warning.pastDue!.suspensionDate),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}),
|
||||
filter((result): result is NonNullable<typeof result> => result !== null),
|
||||
);
|
||||
|
||||
showInactiveSubscriptionDialog$ = (organization: Organization): Observable<void> =>
|
||||
this.getWarning$(organization, (response) => response.inactiveSubscription).pipe(
|
||||
switchMap(async (warning) => {
|
||||
switch (warning.resolution) {
|
||||
case "contact_provider": {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: this.i18nService.t("suspendedOrganizationTitle", organization.name),
|
||||
content: {
|
||||
key: "suspendedManagedOrgMessage",
|
||||
placeholders: [organization.providerName],
|
||||
},
|
||||
type: "danger",
|
||||
acceptButtonText: this.i18nService.t("close"),
|
||||
cancelButtonText: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "add_payment_method": {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: this.i18nService.t("suspendedOrganizationTitle", organization.name),
|
||||
content: { key: "suspendedOwnerOrgMessage" },
|
||||
type: "danger",
|
||||
acceptButtonText: this.i18nService.t("continue"),
|
||||
cancelButtonText: this.i18nService.t("close"),
|
||||
});
|
||||
if (confirmed) {
|
||||
await this.router.navigate(
|
||||
["organizations", `${organization.id}`, "billing", "payment-method"],
|
||||
{
|
||||
state: { launchPaymentModalAutomatically: true },
|
||||
},
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "resubscribe": {
|
||||
const subscription = await this.organizationApiService.getSubscription(organization.id);
|
||||
const dialogReference = openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
subscription: subscription,
|
||||
productTierType: organization.productTierType,
|
||||
},
|
||||
});
|
||||
await lastValueFrom(dialogReference.closed);
|
||||
break;
|
||||
}
|
||||
case "contact_owner": {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: this.i18nService.t("suspendedOrganizationTitle", organization.name),
|
||||
content: { key: "suspendedUserOrgMessage" },
|
||||
type: "danger",
|
||||
acceptButtonText: this.i18nService.t("close"),
|
||||
cancelButtonText: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
private getResponse$ = (organization: Organization): Observable<OrganizationWarningsResponse> => {
|
||||
const existing = this.cache$.get(organization.id as OrganizationId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const response$ = from(this.organizationBillingApiService.getWarnings(organization.id)).pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
this.cache$.set(organization.id as OrganizationId, response$);
|
||||
return response$;
|
||||
};
|
||||
|
||||
private getWarning$ = <T>(
|
||||
organization: Organization,
|
||||
extract: (response: OrganizationWarningsResponse) => T | null | undefined,
|
||||
): Observable<T> =>
|
||||
this.getResponse$(organization).pipe(
|
||||
map(extract),
|
||||
takeWhile((warning): warning is T => !!warning),
|
||||
take(1),
|
||||
);
|
||||
}
|
||||
@@ -10,10 +10,10 @@
|
||||
{{ "sponsoredFamiliesEligible" | i18n }}
|
||||
</p>
|
||||
<div bitTypography="body1">
|
||||
{{ "sponsoredFamiliesInclude" | i18n }}:
|
||||
{{ "sponsoredFamiliesIncludeMessage" | i18n }}:
|
||||
<ul class="tw-list-outside">
|
||||
<li>{{ "sponsoredFamiliesPremiumAccess" | i18n }}</li>
|
||||
<li>{{ "sponsoredFamiliesSharedCollections" | i18n }}</li>
|
||||
<li>{{ "sponsoredFamiliesSharedCollectionsForFamilyMembers" | i18n }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form [formGroup]="sponsorshipForm" [bitSubmit]="submit" *ngIf="anyOrgsAvailable$ | async">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="revokeSponsorship()"
|
||||
[attr.aria-label]="'revokeAccount' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
[attr.aria-label]="'revokeAccountMessage' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
|
||||
>
|
||||
<span class="tw-text-danger">{{ "remove" | i18n }}</span>
|
||||
</button>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
@@ -37,6 +38,7 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
private organizationSponsorshipApiService: OrganizationSponsorshipApiServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -73,7 +75,10 @@ export class SponsoringOrgRowComponent implements OnInit {
|
||||
}
|
||||
|
||||
async resendEmail() {
|
||||
await this.apiService.postResendSponsorshipOffer(this.sponsoringOrg.id);
|
||||
await this.organizationSponsorshipApiService.postResendSponsorshipOffer(
|
||||
this.sponsoringOrg.id,
|
||||
this.sponsoringOrg.familySponsorshipFriendlyName,
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<!-- Bank Account -->
|
||||
<ng-container *ngIf="showBankAccount && usingBankAccount">
|
||||
<bit-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
|
||||
{{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }}
|
||||
{{ bankAccountWarning }}
|
||||
</bit-callout>
|
||||
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4" formGroupName="bankInformation">
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { takeUntil } from "rxjs/operators";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { BillingServicesModule, BraintreeService, StripeService } from "../../services";
|
||||
@@ -37,6 +38,8 @@ export class PaymentComponent implements OnInit, OnDestroy {
|
||||
/** If provided, will be invoked with the tokenized payment source during form submission. */
|
||||
@Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise<void>;
|
||||
|
||||
@Input() private bankAccountWarningOverride?: string;
|
||||
|
||||
@Output() submitted = new EventEmitter<PaymentMethodType>();
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
@@ -56,6 +59,7 @@ export class PaymentComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private braintreeService: BraintreeService,
|
||||
private i18nService: I18nService,
|
||||
private stripeService: StripeService,
|
||||
) {}
|
||||
|
||||
@@ -200,4 +204,12 @@ export class PaymentComponent implements OnInit, OnDestroy {
|
||||
private get usingStripe(): boolean {
|
||||
return this.usingBankAccount || this.usingCard;
|
||||
}
|
||||
|
||||
get bankAccountWarning(): string {
|
||||
if (this.bankAccountWarningOverride) {
|
||||
return this.bankAccountWarningOverride;
|
||||
} else {
|
||||
return this.i18nService.t("verifyBankAccountWithStatementDescriptorWarning");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AnchorLinkDirective, BannerComponent } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
FreeTrialWarning,
|
||||
OrganizationWarningsService,
|
||||
} from "../services/organization-warnings.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-free-trial-warning",
|
||||
template: `
|
||||
@let warning = freeTrialWarning$ | async;
|
||||
|
||||
@if (warning) {
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="premium"
|
||||
[showClose]="false"
|
||||
>
|
||||
{{ warning.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="clicked.emit()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ "clickHereToAddPaymentMethod" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
}
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [AnchorLinkDirective, AsyncPipe, BannerComponent, I18nPipe],
|
||||
})
|
||||
export class FreeTrialWarningComponent implements OnInit {
|
||||
@Input({ required: true }) organization!: Organization;
|
||||
@Output() clicked = new EventEmitter<void>();
|
||||
|
||||
freeTrialWarning$!: Observable<FreeTrialWarning>;
|
||||
|
||||
constructor(private organizationWarningsService: OrganizationWarningsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.freeTrialWarning$ = this.organizationWarningsService.getFreeTrialWarning$(
|
||||
this.organization,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { AsyncPipe } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { BannerComponent } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
OrganizationWarningsService,
|
||||
ResellerRenewalWarning,
|
||||
} from "../services/organization-warnings.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-reseller-renewal-warning",
|
||||
template: `
|
||||
@let warning = resellerRenewalWarning$ | async;
|
||||
|
||||
@if (warning) {
|
||||
<bit-banner
|
||||
id="reseller-warning-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="info"
|
||||
[showClose]="false"
|
||||
>
|
||||
{{ warning.message }}
|
||||
</bit-banner>
|
||||
}
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, BannerComponent],
|
||||
})
|
||||
export class ResellerRenewalWarningComponent implements OnInit {
|
||||
@Input({ required: true }) organization!: Organization;
|
||||
|
||||
resellerRenewalWarning$!: Observable<ResellerRenewalWarning>;
|
||||
|
||||
constructor(private organizationWarningsService: OrganizationWarningsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.resellerRenewalWarning$ = this.organizationWarningsService.getResellerRenewalWarning$(
|
||||
this.organization,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceType, EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EventResponse } from "@bitwarden/common/models/response/event.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@Injectable()
|
||||
@@ -21,8 +19,7 @@ export class EventService {
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
policyService: PolicyService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
accountService.activeAccount$
|
||||
.pipe(
|
||||
@@ -463,20 +460,10 @@ export class EventService {
|
||||
msg = humanReadableMsg = this.i18nService.t("removedDomain", ev.domainName);
|
||||
break;
|
||||
case EventType.OrganizationDomain_Verified:
|
||||
msg = humanReadableMsg = this.i18nService.t(
|
||||
(await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning))
|
||||
? "domainClaimedEvent"
|
||||
: "domainVerifiedEvent",
|
||||
ev.domainName,
|
||||
);
|
||||
msg = humanReadableMsg = this.i18nService.t("domainClaimedEvent", ev.domainName);
|
||||
break;
|
||||
case EventType.OrganizationDomain_NotVerified:
|
||||
msg = humanReadableMsg = this.i18nService.t(
|
||||
(await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning))
|
||||
? "domainNotClaimedEvent"
|
||||
: "domainNotVerifiedEvent",
|
||||
ev.domainName,
|
||||
);
|
||||
msg = humanReadableMsg = this.i18nService.t("domainNotClaimedEvent", ev.domainName);
|
||||
break;
|
||||
// Secrets Manager
|
||||
case EventType.Secret_Retrieved:
|
||||
|
||||
@@ -67,7 +67,7 @@ export class CipherReportComponent implements OnDestroy {
|
||||
protected i18nService: I18nService,
|
||||
private syncService: SyncService,
|
||||
private cipherFormConfigService: CipherFormConfigService,
|
||||
private adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService,
|
||||
protected adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService,
|
||||
) {
|
||||
this.organizations$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
@@ -207,7 +207,7 @@ export class CipherReportComponent implements OnDestroy {
|
||||
|
||||
// If the dialog was closed by deleting the cipher, refresh the report.
|
||||
if (result === VaultItemDialogResult.Deleted || result === VaultItemDialogResult.Saved) {
|
||||
await this.load();
|
||||
await this.refresh(result, cipher);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,10 @@ export class CipherReportComponent implements OnDestroy {
|
||||
this.allCiphers = [];
|
||||
}
|
||||
|
||||
protected async refresh(result: VaultItemDialogResult, cipher: CipherView) {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
protected async repromptCipher(c: CipherView) {
|
||||
return (
|
||||
c.reprompt === CipherRepromptType.None ||
|
||||
@@ -58,7 +58,7 @@
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && row.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
class="bwi bwi-collection-shared"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
@@ -62,7 +62,7 @@
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && r.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
class="bwi bwi-collection-shared"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
@@ -60,7 +60,7 @@
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && row.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
class="bwi bwi-collection-shared"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
@@ -62,7 +62,7 @@
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && r.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
class="bwi bwi-collection-shared"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
@@ -62,7 +62,7 @@
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!organization && row.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection"
|
||||
class="bwi bwi-collection-shared"
|
||||
appStopProp
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
@@ -1,18 +1,22 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { BadgeVariant, DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { VaultItemDialogResult } from "@bitwarden/web-vault/app/vault/components/vault-item-dialog/vault-item-dialog.component";
|
||||
|
||||
import { AdminConsoleCipherFormConfigService } from "../../../vault/org-vault/services/admin-console-cipher-form-config.service";
|
||||
|
||||
@@ -40,7 +44,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
i18nService: I18nService,
|
||||
syncService: SyncService,
|
||||
cipherFormConfigService: CipherFormConfigService,
|
||||
adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService,
|
||||
protected adminConsoleCipherFormConfigService: AdminConsoleCipherFormConfigService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
@@ -66,62 +70,112 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
|
||||
this.findWeakPasswords(allCiphers);
|
||||
}
|
||||
|
||||
protected findWeakPasswords(ciphers: CipherView[]): void {
|
||||
ciphers.forEach((ciph) => {
|
||||
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
protected async refresh(result: VaultItemDialogResult, cipher: CipherView) {
|
||||
if (result === VaultItemDialogResult.Deleted) {
|
||||
// remove the cipher from the list
|
||||
this.weakPasswordCiphers = this.weakPasswordCiphers.filter((c) => c.id !== cipher.id);
|
||||
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == VaultItemDialogResult.Saved) {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
let updatedCipher = await this.cipherService.get(cipher.id, activeUserId);
|
||||
|
||||
if (this.isAdminConsoleActive) {
|
||||
updatedCipher = await this.adminConsoleCipherFormConfigService.getCipher(
|
||||
cipher.id as CipherId,
|
||||
this.organization,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedCipherView = await updatedCipher.decrypt(
|
||||
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
|
||||
);
|
||||
// update the cipher views
|
||||
const updatedReportResult = this.determineWeakPasswordScore(updatedCipherView);
|
||||
const index = this.weakPasswordCiphers.findIndex((c) => c.id === updatedCipherView.id);
|
||||
|
||||
if (updatedReportResult == null) {
|
||||
// the password is no longer weak
|
||||
// remove the cipher from the list
|
||||
this.weakPasswordCiphers.splice(index, 1);
|
||||
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUserName = this.isUserNameNotEmpty(ciph);
|
||||
let userInput: string[] = [];
|
||||
if (hasUserName) {
|
||||
const atPosition = login.username.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput
|
||||
.concat(
|
||||
login.username
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/),
|
||||
)
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = login.username
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
.filter((i) => i.length >= 3);
|
||||
}
|
||||
if (index > -1) {
|
||||
// update the existing cipher
|
||||
this.weakPasswordCiphers[index] = updatedReportResult;
|
||||
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||
}
|
||||
const result = this.passwordStrengthService.getPasswordStrength(
|
||||
login.password,
|
||||
null,
|
||||
userInput.length > 0 ? userInput : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.score != null && result.score <= 2) {
|
||||
const scoreValue = this.scoreKey(result.score);
|
||||
const row = {
|
||||
...ciph,
|
||||
score: result.score,
|
||||
reportValue: scoreValue,
|
||||
scoreKey: scoreValue.sortOrder,
|
||||
} as ReportResult;
|
||||
protected findWeakPasswords(ciphers: CipherView[]): void {
|
||||
ciphers.forEach((ciph) => {
|
||||
const row = this.determineWeakPasswordScore(ciph);
|
||||
if (row != null) {
|
||||
this.weakPasswordCiphers.push(row);
|
||||
}
|
||||
});
|
||||
this.filterCiphersByOrg(this.weakPasswordCiphers);
|
||||
}
|
||||
|
||||
protected determineWeakPasswordScore(ciph: CipherView): ReportResult | null {
|
||||
const { type, login, isDeleted, edit, viewPassword } = ciph;
|
||||
if (
|
||||
type !== CipherType.Login ||
|
||||
login.password == null ||
|
||||
login.password === "" ||
|
||||
isDeleted ||
|
||||
(!this.organization && !edit) ||
|
||||
!viewPassword
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasUserName = this.isUserNameNotEmpty(ciph);
|
||||
let userInput: string[] = [];
|
||||
if (hasUserName) {
|
||||
const atPosition = login.username.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput
|
||||
.concat(
|
||||
login.username
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/),
|
||||
)
|
||||
.filter((i) => i.length >= 3);
|
||||
} else {
|
||||
userInput = login.username
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
.filter((i) => i.length >= 3);
|
||||
}
|
||||
}
|
||||
const result = this.passwordStrengthService.getPasswordStrength(
|
||||
login.password,
|
||||
null,
|
||||
userInput.length > 0 ? userInput : null,
|
||||
);
|
||||
|
||||
if (result.score != null && result.score <= 2) {
|
||||
const scoreValue = this.scoreKey(result.score);
|
||||
return {
|
||||
...ciph,
|
||||
score: result.score,
|
||||
reportValue: scoreValue,
|
||||
scoreKey: scoreValue.sortOrder,
|
||||
} as ReportResult;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected canManageCipher(c: CipherView): boolean {
|
||||
// this will only ever be false from the org view;
|
||||
return true;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user