1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 21:20:27 +00:00

Merge branch 'main' into auth/pm-18458/create-change-existing-password-component

This commit is contained in:
rr-bw
2025-04-24 22:24:37 -07:00
513 changed files with 8863 additions and 14140 deletions

View File

@@ -69,6 +69,8 @@ import {
ToastService,
} from "@bitwarden/components";
import {
AttachmentDialogResult,
AttachmentsV2Component,
CipherFormConfig,
CipherFormConfigService,
CollectionAssignmentResult,
@@ -92,10 +94,6 @@ import {
} from "../../../vault/components/vault-item-dialog/vault-item-dialog.component";
import { VaultItemEvent } from "../../../vault/components/vault-items/vault-item-event";
import { VaultItemsModule } from "../../../vault/components/vault-items/vault-items.module";
import {
AttachmentDialogResult,
AttachmentsV2Component,
} from "../../../vault/individual-vault/attachments-v2.component";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,

View File

@@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => {
it("permits navigation if the user has permissions", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((_org) => true);
permissionsCallback.mockReturnValue(true);
const actual = await TestBed.runInInjectionContext(
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId }));
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
it("handles a Promise returned from the callback", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockReturnValue(Promise.resolve(true));
const actual = await TestBed.runInInjectionContext(() =>
organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
it("handles an Observable returned from the callback", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockReturnValue(of(true));
const actual = await TestBed.runInInjectionContext(() =>
organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
describe("if the user does not have permissions", () => {
it("and there is no Item ID, block navigation", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((_org) => false);
permissionsCallback.mockReturnValue(false);
state = mock<RouterStateSnapshot>({
root: mock<ActivatedRouteSnapshot>({

View File

@@ -1,13 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core";
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs";
import {
canAccessOrgAdmin,
@@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components";
* proceeds as expected.
*/
export function organizationPermissionsGuard(
permissionsCallback?: (organization: Organization) => boolean,
permissionsCallback?: (
organization: Organization,
) => boolean | Promise<boolean> | Observable<boolean>,
): CanActivateFn {
return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const router = inject(Router);
@@ -51,6 +53,7 @@ export function organizationPermissionsGuard(
const i18nService = inject(I18nService);
const syncService = inject(SyncService);
const accountService = inject(AccountService);
const environmentInjector = inject(EnvironmentInjector);
// TODO: We need to fix issue once and for all.
if ((await syncService.getLastSync()) == null) {
@@ -78,7 +81,22 @@ export function organizationPermissionsGuard(
return router.createUrlTree(["/"]);
}
const hasPermissions = permissionsCallback == null || permissionsCallback(org);
if (permissionsCallback == null) {
// No additional permission checks required, allow navigation
return true;
}
const callbackResult = runInInjectionContext(environmentInjector, () =>
permissionsCallback(org),
);
const hasPermissions = isObservable(callbackResult)
? await firstValueFrom(callbackResult) // handles observables
: await Promise.resolve(callbackResult); // handles promises and boolean values
if (hasPermissions !== true && hasPermissions !== false) {
throw new Error("Permission callback did not resolve to a boolean.");
}
if (!hasPermissions) {
// Handle linkable ciphers for organizations the user only has view access to

View File

@@ -19,12 +19,26 @@
*ngIf="canShowVaultTab(organization)"
>
</bit-nav-item>
<bit-nav-item
icon="bwi-user"
[text]="'members' | i18n"
route="members"
*ngIf="canShowMembersTab(organization)"
></bit-nav-item>
<ng-container *ngIf="canShowMembersTab(organization)">
<ng-container *ngIf="showSponsoredFamiliesDropdown$ | async; else regularMembersItem">
<bit-nav-group icon="bwi-user" [text]="'members' | i18n" route="members">
<bit-nav-item
[text]="'members' | i18n"
route="members"
[routerLinkActiveOptions]="{ exact: true }"
></bit-nav-item>
<bit-nav-item
[text]="'sponsoredFamilies' | i18n"
route="members/sponsored-families"
></bit-nav-item>
</bit-nav-group>
</ng-container>
<ng-template #regularMembersItem>
<bit-nav-item icon="bwi-user" [text]="'members' | i18n" route="members"></bit-nav-item>
</ng-template>
</ng-container>
<bit-nav-item
icon="bwi-users"
[text]="'groups' | i18n"
@@ -83,7 +97,7 @@
<bit-nav-item
[text]="'policies' | i18n"
route="settings/policies"
*ngIf="organization.canManagePolicies"
*ngIf="canShowPoliciesTab$ | async"
></bit-nav-item>
<bit-nav-item
[text]="'twoStepLogin' | i18n"

View File

@@ -22,6 +22,7 @@ import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/
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 { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -29,6 +30,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { getById } from "@bitwarden/common/platform/misc";
import { BannerModule, IconModule } from "@bitwarden/components";
import { FreeFamiliesPolicyService } from "../../../billing/services/free-families-policy.service";
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
import { WebLayoutModule } from "../../../layouts/web-layout.module";
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
@@ -66,6 +68,8 @@ export class OrganizationLayoutComponent implements OnInit {
showAccountDeprovisioningBanner$: Observable<boolean>;
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
protected canShowPoliciesTab$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
@@ -76,6 +80,8 @@ export class OrganizationLayoutComponent implements OnInit {
private providerService: ProviderService,
protected bannerService: AccountDeprovisioningBannerService,
private accountService: AccountService,
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
) {}
async ngOnInit() {
@@ -92,6 +98,8 @@ export class OrganizationLayoutComponent implements OnInit {
),
filter((org) => org != null),
);
this.showSponsoredFamiliesDropdown$ =
this.freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(this.organization$);
this.showAccountDeprovisioningBanner$ = combineLatest([
this.bannerService.showBanner$,
@@ -143,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit {
))
? "claimedDomains"
: "domainVerification";
this.canShowPoliciesTab$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService
.isBreadcrumbingPoliciesEnabled$(organization)
.pipe(
map(
(isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies,
),
),
),
);
}
canShowVaultTab(organization: Organization): boolean {

View File

@@ -1,6 +1,12 @@
@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async);
<app-header>
<span bitBadge variant="primary" slot="title-suffix" *ngIf="usePlaceHolderEvents">
<span
bitBadge
variant="primary"
slot="title-suffix"
class="tw-ml-2 tw-mt-1.5 tw-inline-flex tw-items-center"
*ngIf="usePlaceHolderEvents"
>
{{ "upgrade" | i18n }}
</span>
</app-header>

View File

@@ -3,8 +3,10 @@ import { RouterModule, Routes } from "@angular/router";
import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component";
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard";
import { MembersComponent } from "./members.component";
const routes: Routes = [
@@ -16,6 +18,14 @@ const routes: Routes = [
titleId: "members",
},
},
{
path: "sponsored-families",
component: FreeBitwardenFamiliesComponent,
canActivate: [organizationPermissionsGuard(canAccessMembersTab), canAccessSponsoredFamilies],
data: {
titleId: "sponsoredFamilies",
},
},
];
@NgModule({

View File

@@ -1,4 +1,17 @@
<app-header></app-header>
<app-header>
@let organization = organization$ | async;
<button
bitBadge
class="!tw-align-middle"
(click)="changePlan(organization)"
*ngIf="isBreadcrumbingEnabled$ | async"
slot="title-suffix"
type="button"
variant="primary"
>
{{ "upgrade" | i18n }}
</button>
</app-header>
<bit-container>
<ng-container *ngIf="loading">

View File

@@ -2,8 +2,8 @@
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, lastValueFrom } from "rxjs";
import { first, map } from "rxjs/operators";
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
import { first } from "rxjs/operators";
import {
getOrganizationById,
@@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { DialogService } from "@bitwarden/components";
import {
ChangePlanDialogResultType,
openChangePlanDialog,
} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { PolicyListService } from "../../core/policy-list.service";
import { BasePolicy } from "../policies";
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
@@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit {
loading = true;
organizationId: string;
policies: BasePolicy[];
organization: Organization;
protected organization$: Observable<Organization>;
private orgPolicies: PolicyResponse[];
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
protected isBreadcrumbingEnabled$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private accountService: AccountService,
private organizationService: OrganizationService,
private policyApiService: PolicyApiServiceAbstraction,
private policyListService: PolicyListService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private dialogService: DialogService,
) {}
@@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit {
const userId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.organization = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(this.organizationId)),
);
this.organization$ = this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(this.organizationId));
this.policies = this.policyListService.getPolicies();
await this.load();
@@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit {
this.orgPolicies.forEach((op) => {
this.policiesEnabledMap.set(op.type, op.enabled);
});
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
),
);
this.loading = false;
}
@@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit {
});
const result = await lastValueFrom(dialogRef.closed);
if (result === PolicyEditDialogResult.Saved) {
await this.load();
switch (result) {
case PolicyEditDialogResult.Saved:
await this.load();
break;
case PolicyEditDialogResult.UpgradePlan:
await this.changePlan(await firstValueFrom(this.organization$));
break;
}
}
protected readonly CollectionDialogTabType = CollectionDialogTabType;
protected readonly All = All;
protected async changePlan(organization: Organization) {
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: organization.id,
subscription: null,
productTierType: organization.productTierType,
},
});
const result = await lastValueFrom(reference.closed);
if (result === ChangePlanDialogResultType.Closed) {
return;
}
await this.load();
}
}

View File

@@ -1,5 +1,17 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
<ng-container bitDialogTitle>
<button
bitBadge
class="!tw-align-middle"
(click)="upgradePlan()"
*ngIf="isBreadcrumbingEnabled$ | async"
type="button"
variant="primary"
>
{{ "planNameEnterprise" | i18n }}
</button>
</ng-container>
<ng-container bitDialogContent>
<div *ngIf="loading">
<i
@@ -16,6 +28,7 @@
</ng-container>
<ng-container bitDialogFooter>
<button
*ngIf="!(isBreadcrumbingEnabled$ | async); else breadcrumbing"
bitButton
buttonType="primary"
[disabled]="saveDisabled$ | async"
@@ -24,6 +37,11 @@
>
{{ "save" | i18n }}
</button>
<ng-template #breadcrumbing>
<button bitButton buttonType="primary" bitFormButton type="button" (click)="upgradePlan()">
{{ "upgrade" | i18n }}
</button>
</ng-template>
<button bitButton buttonType="secondary" bitDialogClose type="button">
{{ "cancel" | i18n }}
</button>

View File

@@ -9,12 +9,20 @@ import {
ViewContainerRef,
} from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Observable, map } from "rxjs";
import { map, Observable, switchMap } from "rxjs";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
DIALOG_DATA,
@@ -35,6 +43,7 @@ export type PolicyEditDialogData = {
export enum PolicyEditDialogResult {
Saved = "saved",
UpgradePlan = "upgrade-plan",
}
@Component({
selector: "app-policy-edit",
@@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit {
loading = true;
enabled = false;
saveDisabled$: Observable<boolean>;
defaultTypes: any[];
policyComponent: BasePolicyComponent;
private policyResponse: PolicyResponse;
formGroup = this.formBuilder.group({
enabled: [this.enabled],
});
protected organization$: Observable<Organization>;
protected isBreadcrumbingEnabled$: Observable<boolean>;
constructor(
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
private accountService: AccountService,
private policyApiService: PolicyApiServiceAbstraction,
private organizationService: OrganizationService,
private i18nService: I18nService,
private cdr: ChangeDetectorRef,
private formBuilder: FormBuilder,
private dialogRef: DialogRef<PolicyEditDialogResult>,
private toastService: ToastService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
) {}
get policy(): BasePolicy {
return this.data.policy;
}
@@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit {
throw e;
}
}
this.organization$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.organizationService.organizations$(userId)),
getOrganizationById(this.data.organizationId),
);
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
),
);
}
submit = async () => {
@@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit {
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
};
protected upgradePlan(): void {
this.dialogRef.close(PolicyEditDialogResult.UpgradePlan);
}
}

View File

@@ -1,8 +1,10 @@
import { NgModule } from "@angular/core";
import { NgModule, inject } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { map } from "rxjs";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
@@ -41,7 +43,14 @@ const routes: Routes = [
{
path: "policies",
component: PoliciesComponent,
canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)],
canActivate: [
organizationPermissionsGuard((o: Organization) => {
const organizationBillingService = inject(OrganizationBillingServiceAbstraction);
return organizationBillingService
.isBreadcrumbingPoliciesEnabled$(o)
.pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled));
}),
],
data: {
titleId: "policies",
},

View File

@@ -1,6 +1,7 @@
<div class="tw-mt-10 tw-flex tw-justify-center" *ngIf="loading">
<div>
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n">
</bit-icon>
<div class="tw-flex tw-justify-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"

View File

@@ -43,6 +43,8 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
value.plan = PlanType.FamiliesAnnually;
value.productTier = ProductTierType.Families;
value.acceptingSponsorship = true;
value.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this));
}

View File

@@ -36,7 +36,7 @@ describe("RotateableKeySetService", () => {
keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
keyService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey as any);
encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any);
encryptService.wrapEncapsulationKey.mockResolvedValue(encryptedPublicKey as any);
const result = await service.createKeySet(externalKey as any);

View File

@@ -29,7 +29,10 @@ export class RotateableKeySetService {
userKey,
rawPublicKey,
);
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey);
const encryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
rawPublicKey,
userKey,
);
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
}
@@ -62,7 +65,10 @@ export class RotateableKeySetService {
if (publicKey == null) {
throw new Error("failed to rotate key set: could not decrypt public key");
}
const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey);
const newEncryptedPublicKey = await this.encryptService.wrapEncapsulationKey(
publicKey,
newUserKey,
);
const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
newUserKey,
publicKey,

View File

@@ -24,7 +24,6 @@ import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
import { I18nService } from "../../core/i18n.service";
import {
@@ -93,6 +92,9 @@ describe("AcceptOrganizationInviteService", () => {
"orgPublicKey",
{ encryptedString: "string" } as EncString,
]);
encryptService.wrapDecapsulationKey.mockResolvedValue({
encryptedString: "string",
} as EncString);
encryptService.encrypt.mockResolvedValue({ encryptedString: "string" } as EncString);
const invite = createOrgInvite({ initOrganization: true });
@@ -200,11 +202,6 @@ describe("AcceptOrganizationInviteService", () => {
encryptedString: "encryptedString",
} as EncString);
jest.mock("../../admin-console/organizations/manage/organization-trust.component");
OrganizationTrustComponent.open = jest.fn().mockReturnValue({
closed: new BehaviorSubject(true),
});
await globalState.update(() => invite);
policyService.getResetPasswordPolicyOptions.mockReturnValue([
@@ -217,7 +214,6 @@ describe("AcceptOrganizationInviteService", () => {
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(OrganizationTrustComponent.open).toHaveBeenCalled();
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
{ key: "userKey" },
Utils.fromB64ToArray("publicKey"),

View File

@@ -31,8 +31,6 @@ import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
import { OrganizationInvite } from "./organization-invite";
// We're storing the organization invite for 2 reasons:
@@ -189,15 +187,6 @@ export class AcceptOrganizationInviteService {
}
const publicKey = Utils.fromB64ToArray(response.publicKey);
const dialogRef = OrganizationTrustComponent.open(this.dialogService, {
name: invite.organizationName,
orgId: invite.organizationId,
publicKey,
});
const result = await firstValueFrom(dialogRef.closed);
if (result !== true) {
throw new Error("Organization not trusted, aborting user key rotation");
}
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));

View File

@@ -2,6 +2,7 @@ import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
@@ -21,8 +22,6 @@ import {
DefaultChangeLoginPasswordService,
} from "@bitwarden/vault";
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
export interface EmergencyViewDialogParams {
/** The cipher being viewed. */
cipher: CipherView;
@@ -42,7 +41,7 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
standalone: true,
imports: [ButtonModule, CipherViewComponent, DialogModule, CommonModule, JslibModule],
providers: [
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
{ provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService },
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
],

View File

@@ -0,0 +1,26 @@
import { inject } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivateFn } from "@angular/router";
import { firstValueFrom, switchMap, filter } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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 { getById } from "@bitwarden/common/platform/misc";
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
export const canAccessSponsoredFamilies: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
const freeFamiliesPolicyService = inject(FreeFamiliesPolicyService);
const organizationService = inject(OrganizationService);
const accountService = inject(AccountService);
const org = accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => organizationService.organizations$(userId)),
getById(route.params.organizationId),
filter((org): org is Organization => org !== undefined),
);
return await firstValueFrom(freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(org));
};

View File

@@ -0,0 +1,45 @@
<form>
<bit-dialog>
<span bitDialogTitle>{{ "addSponsorship" | i18n }}</span>
<div bitDialogContent>
<form [formGroup]="sponsorshipForm">
<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>
<input
bitInput
inputmode="email"
formControlName="sponsorshipEmail"
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
appInputStripSpaces
/>
</bit-form-field>
</div>
<div class="tw-col-span-12">
<bit-form-field>
<bit-label>{{ "notes" | i18n }}:</bit-label>
<input
bitInput
inputmode="text"
formControlName="sponsorshipNote"
[attr.aria-invalid]="sponsorshipNoteControl.invalid"
appInputStripSpaces
/>
</bit-form-field>
</div>
</div>
</form>
</div>
<ng-container bitDialogFooter>
<button bitButton bitFormButton type="button" buttonType="primary" (click)="save()">
{{ "save" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" [bitDialogClose]="false">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,135 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Component } from "@angular/core";
import {
AbstractControl,
FormBuilder,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
ValidationErrors,
Validators,
} from "@angular/forms";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ButtonModule, DialogModule, DialogService, FormFieldModule } 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",
}
@Component({
templateUrl: "add-sponsorship-dialog.component.html",
standalone: true,
imports: [
JslibModule,
ButtonModule,
DialogModule,
FormsModule,
ReactiveFormsModule,
FormFieldModule,
],
})
export class AddSponsorshipDialogComponent {
sponsorshipForm: FormGroup<RequestSponsorshipForm>;
loading = false;
constructor(
private dialogRef: DialogRef<AddSponsorshipDialogResult>,
private formBuilder: FormBuilder,
private accountService: AccountService,
private i18nService: I18nService,
) {
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
sponsorshipEmail: new FormControl<string | null>("", {
validators: [Validators.email, Validators.required],
asyncValidators: [this.validateNotCurrentUserEmail.bind(this)],
updateOn: "change",
}),
sponsorshipNote: new FormControl<string | null>("", {}),
});
}
static open(dialogService: DialogService): DialogRef<AddSponsorshipDialogResult> {
return dialogService.open<AddSponsorshipDialogResult>(AddSponsorshipDialogComponent);
}
protected async save() {
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 ?? "",
};
this.dialogRef.close({
action: AddSponsorshipDialogAction.Saved,
value: dialogValue,
});
this.loading = false;
}
protected close = () => {
this.dialogRef.close({ action: AddSponsorshipDialogAction.Canceled, value: null });
};
get sponsorshipEmailControl() {
return this.sponsorshipForm.controls.sponsorshipEmail;
}
get sponsorshipNoteControl() {
return this.sponsorshipForm.controls.sponsorshipNote;
}
private async validateNotCurrentUserEmail(
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 ?? "")),
);
if (!currentUserEmail) {
return null;
}
if (value.toLowerCase() === currentUserEmail.toLowerCase()) {
return { currentUserEmail: true };
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
<app-header>
<button type="button" (click)="addSponsorship()" bitButton buttonType="primary">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "addSponsorship" | i18n }}
</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-tab [label]="'memberFamilies' | i18n">
<app-organization-member-families
[memberFamilies]="sponsoredFamilies"
></app-organization-member-families>
</bit-tab>
</bit-tab-group>
<p class="tw-px-4" bitTypography="body2">{{ "sponsoredFamiliesRemoveActiveSponsorship" | i18n }}</p>

View File

@@ -0,0 +1,62 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { DialogService } from "@bitwarden/components";
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
import {
AddSponsorshipDialogComponent,
AddSponsorshipDialogResult,
} from "./add-sponsorship-dialog.component";
import { SponsoredFamily } from "./types/sponsored-family";
@Component({
selector: "app-free-bitwarden-families",
templateUrl: "free-bitwarden-families.component.html",
})
export class FreeBitwardenFamiliesComponent implements OnInit {
tabIndex = 0;
sponsoredFamilies: SponsoredFamily[] = [];
constructor(
private router: Router,
private dialogService: DialogService,
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
) {}
async ngOnInit() {
await this.preventAccessToFreeFamiliesPage();
}
async addSponsorship() {
const addSponsorshipDialogRef: DialogRef<AddSponsorshipDialogResult> =
AddSponsorshipDialogComponent.open(this.dialogService);
const dialogRef = await firstValueFrom(addSponsorshipDialogRef.closed);
if (dialogRef?.value) {
this.sponsoredFamilies = [dialogRef.value, ...this.sponsoredFamilies];
}
}
removeSponsorhip(sponsorship: any) {
const index = this.sponsoredFamilies.findIndex(
(e) => e.sponsorshipEmail == sponsorship.sponsorshipEmail,
);
this.sponsoredFamilies.splice(index, 1);
}
private async preventAccessToFreeFamiliesPage() {
const showFreeFamiliesPage = await firstValueFrom(
this.freeFamiliesPolicyService.showFreeFamilies$,
);
if (!showFreeFamiliesPage) {
await this.router.navigate(["/"]);
return;
}
}
}

View File

@@ -0,0 +1,47 @@
<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>

View File

@@ -0,0 +1,34 @@
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();
}
}

View File

@@ -0,0 +1,87 @@
<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>

View File

@@ -0,0 +1,39 @@
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();
}
}

View File

@@ -0,0 +1,5 @@
export interface SponsoredFamily {
sponsorshipEmail?: string;
sponsorshipNote?: string;
status?: string;
}

View File

@@ -34,7 +34,12 @@ 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";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import {
PaymentMethodType,
PlanSponsorshipType,
PlanType,
ProductTierType,
} from "@bitwarden/common/billing/enums";
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
@@ -83,6 +88,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
@Input() showFree = true;
@Input() showCancel = false;
@Input() acceptingSponsorship = false;
@Input() planSponsorshipType?: PlanSponsorshipType;
@Input() currentPlan: PlanResponse;
selectedFile: File;
@@ -682,11 +688,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
private refreshSalesTax(): void {
if (this.formGroup.controls.plan.value == PlanType.Free) {
this.estimatedTax = 0;
return;
}
if (!this.taxComponent.validate()) {
return;
}
@@ -696,6 +697,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
passwordManager: {
additionalStorage: this.formGroup.controls.additionalStorage.value,
plan: this.formGroup.controls.plan.value,
sponsoredPlan: this.planSponsorshipType,
seats: this.formGroup.controls.additionalSeats.value,
},
taxInformation: {
@@ -810,7 +812,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
);
const providerKey = await this.keyService.getProviderKey(this.providerId);
providerRequest.organizationCreateRequest.key = (
await this.encryptService.encrypt(orgKey.key, providerKey)
await this.encryptService.wrapSymmetricKey(orgKey, providerKey)
).encryptedString;
const orgId = (
await this.apiService.postProviderCreateOrganization(this.providerId, providerRequest)

View File

@@ -0,0 +1,193 @@
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { FreeFamiliesPolicyService } from "./free-families-policy.service";
describe("FreeFamiliesPolicyService", () => {
let service: FreeFamiliesPolicyService;
let organizationService: MockProxy<OrganizationService>;
let policyService: MockProxy<PolicyService>;
let configService: MockProxy<ConfigService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;
beforeEach(() => {
organizationService = mock<OrganizationService>();
policyService = mock<PolicyService>();
configService = mock<ConfigService>();
accountService = mockAccountServiceWith(userId);
service = new FreeFamiliesPolicyService(
policyService,
organizationService,
accountService,
configService,
);
});
describe("showSponsoredFamiliesDropdown$", () => {
it("should return true when all conditions are met", async () => {
// Configure mocks
configService.getFeatureFlag$.mockReturnValue(of(true));
policyService.policiesByType$.mockReturnValue(of([]));
// Create a test organization that meets all criteria
const organization = {
id: "org-id",
productTierType: ProductTierType.Enterprise,
useAdminSponsoredFamilies: true,
isAdmin: true,
isOwner: false,
canManageUsers: false,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(true);
});
it("should return false when organization is not Enterprise", async () => {
// Configure mocks
configService.getFeatureFlag$.mockReturnValue(of(true));
policyService.policiesByType$.mockReturnValue(of([]));
// Create a test organization that is not Enterprise tier
const organization = {
id: "org-id",
productTierType: ProductTierType.Teams,
useAdminSponsoredFamilies: true,
isAdmin: true,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(false);
});
it("should return false when feature flag is disabled", async () => {
// Configure mocks to disable feature flag
configService.getFeatureFlag$.mockReturnValue(of(false));
policyService.policiesByType$.mockReturnValue(of([]));
// Create a test organization that meets other criteria
const organization = {
id: "org-id",
productTierType: ProductTierType.Enterprise,
useAdminSponsoredFamilies: true,
isAdmin: true,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(false);
});
it("should return false when families feature is disabled by policy", async () => {
// Configure mocks with a policy that disables the feature
configService.getFeatureFlag$.mockReturnValue(of(true));
policyService.policiesByType$.mockReturnValue(
of([{ organizationId: "org-id", enabled: true } as Policy]),
);
// Create a test organization
const organization = {
id: "org-id",
productTierType: ProductTierType.Enterprise,
useAdminSponsoredFamilies: true,
isAdmin: true,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(false);
});
it("should return false when useAdminSponsoredFamilies is false", async () => {
// Configure mocks
configService.getFeatureFlag$.mockReturnValue(of(true));
policyService.policiesByType$.mockReturnValue(of([]));
// Create a test organization with useAdminSponsoredFamilies set to false
const organization = {
id: "org-id",
productTierType: ProductTierType.Enterprise,
useAdminSponsoredFamilies: false,
isAdmin: true,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(false);
});
it("should return true when user is an owner but not admin", async () => {
// Configure mocks
configService.getFeatureFlag$.mockReturnValue(of(true));
policyService.policiesByType$.mockReturnValue(of([]));
// Create a test organization where user is owner but not admin
const organization = {
id: "org-id",
productTierType: ProductTierType.Enterprise,
useAdminSponsoredFamilies: true,
isAdmin: false,
isOwner: true,
canManageUsers: false,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(true);
});
it("should return true when user can manage users but is not admin or owner", async () => {
// Configure mocks
configService.getFeatureFlag$.mockReturnValue(of(true));
policyService.policiesByType$.mockReturnValue(of([]));
// Create a test organization where user can manage users but is not admin or owner
const organization = {
id: "org-id",
productTierType: ProductTierType.Enterprise,
useAdminSponsoredFamilies: true,
isAdmin: false,
isOwner: false,
canManageUsers: true,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(true);
});
it("should return false when user has no admin permissions", async () => {
// Configure mocks
configService.getFeatureFlag$.mockReturnValue(of(true));
policyService.policiesByType$.mockReturnValue(of([]));
// Create a test organization where user has no admin permissions
const organization = {
id: "org-id",
productTierType: ProductTierType.Enterprise,
useAdminSponsoredFamilies: true,
isAdmin: false,
isOwner: false,
canManageUsers: false,
} as Organization;
// Test the method
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
expect(result).toBe(false);
});
});
});

View File

@@ -7,6 +7,9 @@ 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 { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
interface EnterpriseOrgStatus {
isFreeFamilyPolicyEnabled: boolean;
@@ -26,6 +29,7 @@ export class FreeFamiliesPolicyService {
private policyService: PolicyService,
private organizationService: OrganizationService,
private accountService: AccountService,
private configService: ConfigService,
) {}
organizations$ = this.accountService.activeAccount$.pipe(
@@ -42,6 +46,48 @@ export class FreeFamiliesPolicyService {
return this.getFreeFamiliesVisibility$();
}
/**
* Determines whether to show the sponsored families dropdown in the organization layout
* @param organization The organization to check
* @returns Observable<boolean> indicating whether to show the dropdown
*/
showSponsoredFamiliesDropdown$(organization: Observable<Organization>): Observable<boolean> {
const enterpriseOrganization$ = organization.pipe(
map((org) => org.productTierType === ProductTierType.Enterprise),
);
return this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => {
const policies$ = this.policyService.policiesByType$(
PolicyType.FreeFamiliesSponsorshipPolicy,
userId,
);
return combineLatest([
enterpriseOrganization$,
this.configService.getFeatureFlag$(FeatureFlag.PM17772_AdminInitiatedSponsorships),
organization,
policies$,
]).pipe(
map(([isEnterprise, featureFlagEnabled, org, policies]) => {
const familiesFeatureDisabled = policies.some(
(policy) => policy.organizationId === org.id && policy.enabled,
);
return (
isEnterprise &&
featureFlagEnabled &&
!familiesFeatureDisabled &&
org.useAdminSponsoredFamilies &&
(org.isAdmin || org.isOwner || org.canManageUsers)
);
}),
);
}),
);
}
private getFreeFamiliesVisibility$(): Observable<boolean> {
return combineLatest([
this.checkEnterpriseOrganizationsAndFetchPolicy(),

View File

@@ -76,7 +76,7 @@ export class AddCreditDialogComponent implements OnInit {
async ngOnInit() {
if (this.organizationId != null) {
if (this.creditAmount == null) {
this.creditAmount = "20.00";
this.creditAmount = "0.00";
}
this.ppButtonCustomField = "organization_id:" + this.organizationId;
const userId = await firstValueFrom(
@@ -93,7 +93,7 @@ export class AddCreditDialogComponent implements OnInit {
}
} else {
if (this.creditAmount == null) {
this.creditAmount = "10.00";
this.creditAmount = "0.00";
}
const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-abm-enterprise-content",
templateUrl: "abm-enterprise-content.component.html",
})
export class AbmEnterpriseContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-abm-teams-content",
templateUrl: "abm-teams-content.component.html",
})
export class AbmTeamsContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Start Your Enterprise Free Trial Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Collaborate and share securely</li>
<li>Deploy and manage quickly and easily</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-cnet></app-logo-cnet>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-cnet-enterprise-content",
templateUrl: "cnet-enterprise-content.component.html",
})
export class CnetEnterpriseContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Start Your Premium Account Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Secure your account with advanced two-step login</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-cnet></app-logo-cnet>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-cnet-individual-content",
templateUrl: "cnet-individual-content.component.html",
})
export class CnetIndividualContentComponent {}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Start Your Teams Free Trial Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Collaborate and share securely</li>
<li>Deploy and manage quickly and easily</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-cnet></app-logo-cnet>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-cnet-teams-content",
templateUrl: "cnet-teams-content.component.html",
})
export class CnetTeamsContentComponent {}

View File

@@ -1,16 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28">
<app-logo-company-testimonial></app-logo-company-testimonial>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-default-content",
templateUrl: "default-content.component.html",
})
export class DefaultContentComponent {}

View File

@@ -1,44 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day Enterprise free trial</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Bitwarden is the most trusted password manager designed for seamless administration and employee
usability.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Strengthen company-wide security through centralized administrative control and
policies</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Streamline user onboarding and automate account provisioning with flexible SSO and SCIM
integrations</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Give all Enterprise users the gift of 360º security with a free Families plan</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise-content",
templateUrl: "enterprise-content.component.html",
})
export class EnterpriseContentComponent {}

View File

@@ -1,44 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day Enterprise free trial</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Bitwarden is the most trusted password manager designed for seamless administration and employee
usability.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Strengthen company-wide security through centralized administrative control and
policies</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Streamline user onboarding and automate account provisioning with flexible SSO and SCIM
integrations</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Give all Enterprise users the gift of 360º security with a free Families plan</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise1-content",
templateUrl: "enterprise1-content.component.html",
})
export class Enterprise1ContentComponent {}

View File

@@ -1,44 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day Enterprise free trial</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Bitwarden is the most trusted password manager designed for seamless administration and employee
usability.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Strengthen company-wide security through centralized administrative control and
policies</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Streamline user onboarding and automate account provisioning with flexible SSO and SCIM
integrations</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Give all Enterprise users the gift of 360º security with a free Families plan</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise2-content",
templateUrl: "enterprise2-content.component.html",
})
export class Enterprise2ContentComponent {}

View File

@@ -1,11 +0,0 @@
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/vault-signup-badges.png"
class="tw-mx-auto tw-block tw-w-full"
alt="third party awards"
/>
</cite>
</figcaption>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-badges",
templateUrl: "logo-badges.component.html",
})
export class LogoBadgesComponent {}

View File

@@ -1,23 +0,0 @@
<figure>
<div class="tw-flex tw-justify-center tw-gap-4 tw-text-[#eab308] tw-text-5xl">
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
</div>
<blockquote class="tw-mx-auto tw-my-2 tw-max-w-xl tw-px-4 tw-text-center">
“Bitwarden scores points for being fully open-source, secure and audited annually by third-party
cybersecurity firms, giving it a level of transparency that sets it apart from its peers.”
</blockquote>
<figcaption>
<cite>
<img
src="../../images/register-layout/cnet-logo.svg"
class="tw-mx-auto tw-block tw-w-40"
alt="CNET Logo"
/>
</cite>
<p class="tw-text-center tw-font-bold -tw-translate-y-4">Best Password Manager in 2024</p>
</figcaption>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-cnet-5-stars",
templateUrl: "logo-cnet-5-stars.component.html",
})
export class LogoCnet5StarsComponent {}

View File

@@ -1,15 +0,0 @@
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/cnet-logo.svg"
class="tw-mx-auto tw-block tw-w-40"
alt="CNET Logo"
/>
</cite>
</figcaption>
<blockquote class="tw-mx-auto tw-mt-2 tw-max-w-xl tw-px-4 tw-text-center">
"No more excuses; start using Bitwarden today. The identity you save could be your own. The
money definitely will be."
</blockquote>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-cnet",
templateUrl: "logo-cnet.component.html",
})
export class LogoCnetComponent {}

View File

@@ -1,28 +0,0 @@
<figure class="tw-text-center">
<p class="tw-mx-auto tw-my-2 tw-max-w-xl tw-px-4 tw-text-center">
Recommended by industry experts
</p>
<div class="tw-flex tw-flex-wrap tw-gap-8 tw-items-center tw-justify-center tw-mb-4">
<div class="tw-flex tw-gap-8">
<img src="../../images/register-layout/cnet-logo.svg" class="tw-w-32" alt="CNET Logo" />
<img
src="../../images/register-layout/wired-logo.png"
class="tw-w-32 tw-object-contain"
alt="WIRED Logo"
/>
</div>
<div class="tw-flex tw-gap-8">
<img
src="../../images/register-layout/new-york-times-logo.svg"
class="tw-w-32"
alt="New York Times Logo"
/>
<img src="../../images/register-layout/pcmag-logo.svg" class="tw-w-32" alt="PC Mag Logo" />
</div>
</div>
<blockquote>
&ldquo;Bitwarden is currently CNET's top pick for the best password manager, thanks in part to
its commitment to transparency and its unbeatable free tier.&rdquo;
</blockquote>
<p class="tw-font-bold">Best Password Manager in 2024</p>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-company-testimonial",
templateUrl: "logo-company-testimonial.component.html",
})
export class LogoCompanyTestimonialComponent {}

View File

@@ -1,15 +0,0 @@
<figure>
<figcaption>
<cite>
<img
src="../../images/register-layout/forbes-logo.svg"
class="tw-mx-auto tw-block tw-w-40"
alt="Forbes Logo"
/>
</cite>
</figcaption>
<blockquote class="tw-mx-auto tw-mt-2 tw-max-w-xl tw-px-4 tw-text-center">
“Bitwarden boasts the backing of some of the world's best security experts and an attractive,
easy-to-use interface”
</blockquote>
</figure>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-forbes",
templateUrl: "logo-forbes.component.html",
})
export class LogoForbesComponent {}

View File

@@ -1,5 +0,0 @@
<img
src="../../images/register-layout/usnews-360-badge.svg"
class="tw-mx-auto tw-block tw-w-48"
alt="US News 360 Reviews Best Password Manager"
/>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-logo-us-news",
templateUrl: "logo-us-news.component.html",
})
export class LogoUSNewsComponent {}

View File

@@ -1,13 +0,0 @@
<figure>
<h2 class="tw-mx-auto tw-pb-2 tw-max-w-xl tw-font-semibold tw-text-center">
{{ header }}
</h2>
<blockquote class="tw-mx-auto tw-my-2 tw-max-w-xl tw-px-4 tw-text-center">
"{{ quote }}"
</blockquote>
<figcaption>
<cite>
<p class="tw-mx-auto tw-text-center tw-font-bold">{{ source }}</p>
</cite>
</figcaption>
</figure>

View File

@@ -1,13 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
@Component({
selector: "app-review-blurb",
templateUrl: "review-blurb.component.html",
})
export class ReviewBlurbComponent {
@Input() header: string;
@Input() quote: string;
@Input() source: string;
}

View File

@@ -1,18 +0,0 @@
<div class="tw-flex tw-flex-col tw-items-center tw-justify-center">
<img class="tw-mb-2" [ngClass]="logoClass" [src]="logoSrc" [alt]="logoAlt" />
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-justify-center tw-text-[#eab308] tw-text-2xl">
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<i class="bwi bwi-star-f"></i>
<div class="tw-relative">
<div class="tw-absolute tw-inset-0 tw-w-3 tw-overflow-hidden">
<i class="bwi bwi-star-f"></i>
</div>
<i class="bwi bwi-star-f tw-text-[#cbd5e1]"></i>
</div>
</div>
<span class="tw-ml-2">4.7</span>
</div>
</div>

View File

@@ -1,13 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
@Component({
selector: "review-logo",
templateUrl: "review-logo.component.html",
})
export class ReviewLogoComponent {
@Input() logoClass: string;
@Input() logoSrc: string;
@Input() logoAlt: string;
}

View File

@@ -1,30 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">{{ header }}</h1>
<div class="tw-pt-16">
<h2 class="tw-text-2xl tw-font-semibold">
{{ headline }}
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li *ngFor="let primaryPoint of primaryPoints">
{{ primaryPoint }}
</li>
</ul>
<div class="tw-mt-12 tw-flex tw-flex-col">
<div class="tw-rounded-[32px] tw-bg-background">
<div class="tw-my-8 tw-mx-6">
<h2 class="tw-pl-5 tw-font-semibold">{{ calloutHeadline }}</h2>
<ul class="tw-space-y-4 tw-mt-4 tw-pl-10">
<li *ngFor="let callout of callouts">
{{ callout }}
</li>
</ul>
</div>
</div>
</div>
<div class="tw-mt-12 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-review-blurb
header="Businesses trust Bitwarden to secure their infrastructure"
quote="At this point, it would be almost impossible to leak our secrets. It's just one less thing we have to worry about."
source="Titanom Technologies"
></app-review-blurb>
</div>

View File

@@ -1,80 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
@Component({
selector: "app-secrets-manager-content",
templateUrl: "secrets-manager-content.component.html",
})
export class SecretsManagerContentComponent implements OnInit, OnDestroy {
header: string;
headline =
"A simpler, faster way to secure and automate secrets across code and infrastructure deployments";
primaryPoints: string[];
calloutHeadline: string;
callouts: string[];
private paidPrimaryPoints = [
"Unlimited secrets, users, and projects",
"Simple and transparent pricing",
"Zero-knowledge, end-to-end encryption",
];
private paidCalloutHeadline = "Limited time offer";
private paidCallouts = [
"Sign up today and receive a complimentary 12-month subscription to Bitwarden Password Manager",
"Experience complete security across your organization",
"Secure all your sensitive credentials, from user applications to machine secrets",
];
private freePrimaryPoints = [
"Unlimited secrets",
"Simple and transparent pricing",
"Zero-knowledge, end-to-end encryption",
];
private freeCalloutHeadline = "Go beyond developer security!";
private freeCallouts = [
"Your Bitwarden account will also grant complimentary access to Bitwarden Password Manager",
"Extend end-to-end encryption to your personal passwords, addresses, credit cards and notes",
];
private destroy$ = new Subject<void>();
constructor(private activatedRoute: ActivatedRoute) {}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngOnInit(): void {
this.activatedRoute.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
switch (queryParameters.org) {
case "enterprise":
this.header = "Secrets Manager for Enterprise";
this.primaryPoints = this.paidPrimaryPoints;
this.calloutHeadline = this.paidCalloutHeadline;
this.callouts = this.paidCallouts;
break;
case "free":
this.header = "Bitwarden Secrets Manager";
this.primaryPoints = this.freePrimaryPoints;
this.calloutHeadline = this.freeCalloutHeadline;
this.callouts = this.freeCallouts;
break;
case "teams":
case "teamsStarter":
this.header = "Secrets Manager for Teams";
this.primaryPoints = this.paidPrimaryPoints;
this.calloutHeadline = this.paidCalloutHeadline;
this.callouts = this.paidCallouts;
break;
}
});
}
}

View File

@@ -1,17 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">The Bitwarden Password Manager</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>Store logins, secure notes, and more</li>
<li>Collaborate and share securely</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams-content",
templateUrl: "teams-content.component.html",
})
export class TeamsContentComponent {}

View File

@@ -1,35 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day free trial for Teams</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Strengthen business security with an easy-to-use password manager your team will love.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Save time and increase productivity with autofill and instant device syncing</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Enhance security practices across your team with easy user management</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams1-content",
templateUrl: "teams1-content.component.html",
})
export class Teams1ContentComponent {}

View File

@@ -1,35 +0,0 @@
<h1 class="tw-text-3xl !tw-text-alt2">Start your 7-day free trial for Teams</h1>
<div class="tw-pt-20">
<h2 class="tw-text-2xl">
Strengthen business security with an easy-to-use password manager your team will love.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main tw-list-none tw-pl-0">
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Instantly and securely share credentials with the groups and individuals who need them</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Migrate to Bitwarden in minutes with comprehensive import options</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Save time and increase productivity with autofill and instant device syncing</span
>
</li>
<li class="tw-flex tw-items-center">
<i class="bwi bwi-lg bwi-check-circle tw-mr-4 tw-flex-none"></i
><span class="tw-flex-auto"
>Enhance security practices across your team with easy user management</span
>
</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-badges></app-logo-badges>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams2-content",
templateUrl: "teams2-content.component.html",
})
export class Teams2ContentComponent {}

View File

@@ -1,26 +0,0 @@
<h1 class="tw-text-4xl !tw-text-alt2">Begin Teams Starter Free Trial Now</h1>
<div class="tw-pt-32">
<h2 class="tw-text-2xl">
Millions of individuals, teams, and organizations worldwide trust Bitwarden for secure password
storage and sharing.
</h2>
</div>
<ul class="tw-mt-12 tw-flex tw-flex-col tw-gap-10 tw-text-2xl tw-text-main">
<li>
Powerful security for up to 10 users
<div class="tw-mt-2 tw-text-base">
Have more than 10 users?
<a routerLink="/register" [queryParams]="{ org: 'teams', layout: 'teams1' }"
>Start a Teams trial</a
>
</div>
</li>
<li>Collaborate and share securely</li>
<li>Deploy and manage quickly and easily</li>
<li>Access anywhere on any device</li>
<li>Create your account to get started</li>
</ul>
<div class="tw-mt-28 tw-flex tw-flex-col tw-items-center tw-gap-5">
<app-logo-forbes></app-logo-forbes>
<app-logo-us-news></app-logo-us-news>
</div>

View File

@@ -1,7 +0,0 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams3-content",
templateUrl: "teams3-content.component.html",
})
export class Teams3ContentComponent {}

View File

@@ -1,45 +0,0 @@
<app-vertical-stepper #stepper linear>
<app-vertical-step
label="{{ 'organizationInformation' | i18n | titlecase }}"
[subLabel]="subLabels.organizationInfo"
>
<app-org-info [nameOnly]="true" [formGroup]="formGroup"> </app-org-info>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
(click)="createOrganization()"
>
{{ "next" | i18n }}
</button>
</app-vertical-step>
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
<div class="tw-pb-6 tw-pl-6">
<p class="tw-text-xl">{{ "smFreeTrialThankYou" | i18n }}</p>
<ul class="tw-list-disc">
<li>
<p>
{{ "smFreeTrialConfirmationEmail" | i18n }}
<span class="tw-font-bold">{{ formGroup.get("email").value }}</span
>.
</p>
</li>
</ul>
</div>
<div class="tw-mb-3 tw-flex">
<button type="button" bitButton buttonType="primary" (click)="navigateToSecretsManager()">
{{ "getStarted" | i18n | titlecase }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateToMembers()"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
>
{{ "inviteUsers" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>

View File

@@ -1,90 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild } from "@angular/core";
import { UntypedFormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PlanType } from "@bitwarden/common/billing/enums";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
@Component({
selector: "app-secrets-manager-trial-free-stepper",
templateUrl: "secrets-manager-trial-free-stepper.component.html",
})
export class SecretsManagerTrialFreeStepperComponent implements OnInit {
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
formGroup = this.formBuilder.group({
name: [
"",
{
validators: [Validators.required, Validators.maxLength(50)],
updateOn: "change",
},
],
email: [
"",
{
validators: [Validators.email],
},
],
});
subLabels = {
createAccount:
"Before creating your free organization, you first need to log in or create a personal account.",
organizationInfo: "Enter your organization information",
};
organizationId: string;
referenceEventRequest: ReferenceEventRequest;
constructor(
protected formBuilder: UntypedFormBuilder,
protected i18nService: I18nService,
protected organizationBillingService: OrganizationBillingService,
protected router: Router,
) {}
ngOnInit(): void {
this.referenceEventRequest = new ReferenceEventRequest();
this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website";
}
accountCreated(email: string): void {
this.formGroup.get("email")?.setValue(email);
this.subLabels.createAccount = email;
this.verticalStepper.next();
}
async createOrganization(): Promise<void> {
const response = await this.organizationBillingService.startFree({
organization: {
name: this.formGroup.get("name").value,
billingEmail: this.formGroup.get("email").value,
},
plan: {
type: PlanType.Free,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
},
});
this.organizationId = response.id;
this.subLabels.organizationInfo = response.name;
this.verticalStepper.next();
}
async navigateToMembers(): Promise<void> {
await this.router.navigate(["organizations", this.organizationId, "members"]);
}
async navigateToSecretsManager(): Promise<void> {
await this.router.navigate(["sm", this.organizationId]);
}
}

View File

@@ -1,67 +0,0 @@
<app-vertical-stepper #stepper linear>
<app-vertical-step
label="{{ 'organizationInformation' | i18n | titlecase }}"
[subLabel]="subLabels.organizationInfo"
>
<app-org-info [nameOnly]="true" [formGroup]="formGroup"></app-org-info>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
[loading]="createOrganizationLoading"
(click)="createOrganizationOnTrial()"
*ngIf="enableTrialPayment$ | async"
>
{{ "startTrial" | i18n }}
</button>
<button
type="button"
bitButton
buttonType="primary"
[disabled]="formGroup.get('name').invalid"
[loading]="createOrganizationLoading"
cdkStepperNext
*ngIf="!(enableTrialPayment$ | async)"
>
{{ "next" | i18n }}
</button>
</app-vertical-step>
<app-vertical-step
label="{{ 'billing' | i18n | titlecase }}"
[subLabel]="billingSubLabel"
*ngIf="!(enableTrialPayment$ | async)"
>
<app-trial-billing-step
*ngIf="stepper.selectedIndex === 2"
[organizationInfo]="{
name: formGroup.get('name').value,
email: formGroup.get('email').value,
type: productType,
}"
[subscriptionProduct]="SubscriptionProduct.SecretsManager"
(steppedBack)="steppedBack()"
(organizationCreated)="organizationCreated($event)"
></app-trial-billing-step>
</app-vertical-step>
<app-vertical-step label="{{ 'confirmationDetails' | i18n | titlecase }}">
<app-trial-confirmation-details
[email]="formGroup.get('email').value"
[orgLabel]="organizationTypeQueryParameter"
></app-trial-confirmation-details>
<div class="tw-mb-3 tw-flex">
<button type="button" bitButton buttonType="primary" (click)="navigateToSecretsManager()">
{{ "getStarted" | i18n | titlecase }}
</button>
<button
type="button"
bitButton
buttonType="secondary"
(click)="navigateToMembers()"
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
>
{{ "inviteUsers" | i18n }}
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>

View File

@@ -1,144 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input, OnInit, ViewChild } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
OrganizationCreatedEvent,
SubscriptionProduct,
TrialOrganizationType,
} from "../../../billing/accounts/trial-initiation/trial-billing-step.component";
import { VerticalStepperComponent } from "../../trial-initiation/vertical-stepper/vertical-stepper.component";
import { SecretsManagerTrialFreeStepperComponent } from "../secrets-manager/secrets-manager-trial-free-stepper.component";
export enum ValidOrgParams {
families = "families",
enterprise = "enterprise",
teams = "teams",
teamsStarter = "teamsStarter",
individual = "individual",
premium = "premium",
free = "free",
}
const trialFlowOrgs = [
ValidOrgParams.teams,
ValidOrgParams.teamsStarter,
ValidOrgParams.enterprise,
ValidOrgParams.families,
];
@Component({
selector: "app-secrets-manager-trial-paid-stepper",
templateUrl: "secrets-manager-trial-paid-stepper.component.html",
})
export class SecretsManagerTrialPaidStepperComponent
extends SecretsManagerTrialFreeStepperComponent
implements OnInit
{
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
@Input() organizationTypeQueryParameter: string;
plan: PlanType;
createOrganizationLoading = false;
billingSubLabel = this.i18nService.t("billingTrialSubLabel");
organizationId: string;
private destroy$ = new Subject<void>();
protected enableTrialPayment$ = this.configService.getFeatureFlag$(
FeatureFlag.TrialPaymentOptional,
);
constructor(
private route: ActivatedRoute,
private configService: ConfigService,
protected formBuilder: UntypedFormBuilder,
protected i18nService: I18nService,
protected organizationBillingService: OrganizationBillingService,
protected router: Router,
) {
super(formBuilder, i18nService, organizationBillingService, router);
}
async ngOnInit(): Promise<void> {
this.referenceEventRequest = new ReferenceEventRequest();
this.referenceEventRequest.initiationPath = "Secrets Manager trial from marketing website";
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((qParams) => {
if (trialFlowOrgs.includes(qParams.org)) {
if (qParams.org === ValidOrgParams.teamsStarter) {
this.plan = PlanType.TeamsStarter;
} else if (qParams.org === ValidOrgParams.teams) {
this.plan = PlanType.TeamsAnnually;
} else if (qParams.org === ValidOrgParams.enterprise) {
this.plan = PlanType.EnterpriseAnnually;
}
}
});
}
organizationCreated(event: OrganizationCreatedEvent) {
this.organizationId = event.organizationId;
this.billingSubLabel = event.planDescription;
this.verticalStepper.next();
}
steppedBack() {
this.verticalStepper.previous();
}
async createOrganizationOnTrial(): Promise<void> {
this.createOrganizationLoading = true;
const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({
organization: {
name: this.formGroup.get("name").value,
billingEmail: this.formGroup.get("email").value,
initiationPath: "Secrets Manager trial from marketing website",
},
plan: {
type: this.plan,
subscribeToSecretsManager: true,
isFromSecretsManagerTrial: true,
passwordManagerSeats: 1,
secretsManagerSeats: 1,
},
});
this.organizationId = response?.id;
this.subLabels.organizationInfo = response?.name;
this.createOrganizationLoading = false;
this.verticalStepper.next();
}
get createAccountLabel() {
const organizationType =
this.productType === ProductTierType.TeamsStarter
? "Teams Starter"
: ProductTierType[this.productType];
return `Before creating your ${organizationType} organization, you first need to log in or create a personal account.`;
}
get productType(): TrialOrganizationType {
switch (this.organizationTypeQueryParameter) {
case "enterprise":
return ProductTierType.Enterprise;
case "families":
return ProductTierType.Families;
case "teams":
return ProductTierType.Teams;
case "teamsStarter":
return ProductTierType.TeamsStarter;
}
}
protected readonly SubscriptionProduct = SubscriptionProduct;
}

View File

@@ -1,44 +0,0 @@
<!-- eslint-disable tailwindcss/no-custom-classname -->
<ng-container>
<div class="tw-absolute tw--z-10 tw--mt-48 tw-h-[28rem] tw-w-full tw-bg-background-alt2"></div>
<div class="tw-min-w-4xl tw-mx-auto tw-flex tw-max-w-screen-xl tw-gap-12 tw-px-4">
<div class="tw-w-1/2">
<img
alt="Bitwarden"
style="height: 50px; width: 335px"
class="tw-mt-6"
src="../../../../images/register-layout/logo-horizontal-white.svg"
/>
<div class="tw-pt-12">
<app-secrets-manager-content></app-secrets-manager-content>
</div>
</div>
<div class="tw-w-1/2">
<div class="tw-pt-44">
<div class="tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background">
<div
*ngIf="!freeOrganization"
class="tw-flex tw-h-auto tw-w-full tw-gap-5 tw-rounded-t tw-bg-secondary-100"
>
<h2 class="tw-pb-4 tw-pl-4 tw-pt-5 tw-text-base tw-font-bold tw-uppercase">
{{
"startYour7DayFreeTrialOfBitwardenSecretsManagerFor"
| i18n: organizationTypeQueryParameter
}}
</h2>
<environment-selector
class="tw-mr-4 tw-mt-6 tw-flex-shrink-0 tw-text-end"
></environment-selector>
</div>
<app-secrets-manager-trial-free-stepper
*ngIf="freeOrganization"
></app-secrets-manager-trial-free-stepper>
<app-secrets-manager-trial-paid-stepper
*ngIf="!freeOrganization"
[organizationTypeQueryParameter]="organizationTypeQueryParameter"
></app-secrets-manager-trial-paid-stepper>
</div>
</div>
</div>
</div>
</ng-container>

View File

@@ -1,32 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
@Component({
selector: "app-secrets-manager-trial",
templateUrl: "secrets-manager-trial.component.html",
})
export class SecretsManagerTrialComponent implements OnInit, OnDestroy {
organizationTypeQueryParameter: string;
private destroy$ = new Subject<void>();
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((queryParameters) => {
this.organizationTypeQueryParameter = queryParameters.org;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
get freeOrganization() {
return this.organizationTypeQueryParameter === "free";
}
}

View File

@@ -7,36 +7,10 @@ import { FormFieldModule } from "@bitwarden/components";
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
import { SecretsManagerTrialPaidStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component";
import { SecretsManagerTrialComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial.component";
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
import { SharedModule } from "../../shared";
import { CompleteTrialInitiationComponent } from "./complete-trial-initiation/complete-trial-initiation.component";
import { ConfirmationDetailsComponent } from "./confirmation-details.component";
import { AbmEnterpriseContentComponent } from "./content/abm-enterprise-content.component";
import { AbmTeamsContentComponent } from "./content/abm-teams-content.component";
import { CnetEnterpriseContentComponent } from "./content/cnet-enterprise-content.component";
import { CnetIndividualContentComponent } from "./content/cnet-individual-content.component";
import { CnetTeamsContentComponent } from "./content/cnet-teams-content.component";
import { DefaultContentComponent } from "./content/default-content.component";
import { EnterpriseContentComponent } from "./content/enterprise-content.component";
import { Enterprise1ContentComponent } from "./content/enterprise1-content.component";
import { Enterprise2ContentComponent } from "./content/enterprise2-content.component";
import { LogoBadgesComponent } from "./content/logo-badges.component";
import { LogoCnet5StarsComponent } from "./content/logo-cnet-5-stars.component";
import { LogoCnetComponent } from "./content/logo-cnet.component";
import { LogoCompanyTestimonialComponent } from "./content/logo-company-testimonial.component";
import { LogoForbesComponent } from "./content/logo-forbes.component";
import { LogoUSNewsComponent } from "./content/logo-us-news.component";
import { ReviewBlurbComponent } from "./content/review-blurb.component";
import { ReviewLogoComponent } from "./content/review-logo.component";
import { SecretsManagerContentComponent } from "./content/secrets-manager-content.component";
import { TeamsContentComponent } from "./content/teams-content.component";
import { Teams1ContentComponent } from "./content/teams1-content.component";
import { Teams2ContentComponent } from "./content/teams2-content.component";
import { Teams3ContentComponent } from "./content/teams3-content.component";
import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.module";
@NgModule({
@@ -46,41 +20,10 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
VerticalStepperModule,
FormFieldModule,
OrganizationCreateModule,
EnvironmentSelectorModule,
TrialBillingStepComponent,
InputPasswordComponent,
],
declarations: [
CompleteTrialInitiationComponent,
EnterpriseContentComponent,
TeamsContentComponent,
ConfirmationDetailsComponent,
DefaultContentComponent,
EnterpriseContentComponent,
Enterprise1ContentComponent,
Enterprise2ContentComponent,
TeamsContentComponent,
Teams1ContentComponent,
Teams2ContentComponent,
Teams3ContentComponent,
CnetEnterpriseContentComponent,
CnetIndividualContentComponent,
CnetTeamsContentComponent,
AbmEnterpriseContentComponent,
AbmTeamsContentComponent,
LogoBadgesComponent,
LogoCnet5StarsComponent,
LogoCompanyTestimonialComponent,
LogoCnetComponent,
LogoForbesComponent,
LogoUSNewsComponent,
ReviewLogoComponent,
SecretsManagerContentComponent,
ReviewBlurbComponent,
SecretsManagerTrialComponent,
SecretsManagerTrialFreeStepperComponent,
SecretsManagerTrialPaidStepperComponent,
],
declarations: [CompleteTrialInitiationComponent, ConfirmationDetailsComponent],
exports: [CompleteTrialInitiationComponent],
providers: [TitleCasePipe],
})

View File

@@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
@Component({
selector: "app-remove-password",

View File

@@ -183,7 +183,10 @@ describe("KeyRotationService", () => {
mockKeyService.hashMasterKey.mockResolvedValue("mockMasterPasswordHash");
mockConfigService.getFeatureFlag.mockResolvedValue(true);
mockEncryptService.encrypt.mockResolvedValue({
mockEncryptService.wrapSymmetricKey.mockResolvedValue({
encryptedString: "mockEncryptedData",
} as any);
mockEncryptService.wrapDecapsulationKey.mockResolvedValue({
encryptedString: "mockEncryptedData",
} as any);

View File

@@ -145,7 +145,9 @@ export class UserKeyRotationService {
const { privateKey, publicKey } = keyPair;
const accountKeysRequest = new AccountKeysRequest(
(await this.encryptService.encrypt(privateKey, newUnencryptedUserKey)).encryptedString!,
(
await this.encryptService.wrapDecapsulationKey(privateKey, newUnencryptedUserKey)
).encryptedString!,
Utils.fromBufferToB64(publicKey),
);
@@ -427,6 +429,6 @@ export class UserKeyRotationService {
if (privateKey == null) {
throw new Error("No private key found for user key rotation");
}
return (await this.encryptService.encrypt(privateKey, newUserKey)).encryptedString;
return (await this.encryptService.wrapDecapsulationKey(privateKey, newUserKey)).encryptedString;
}
}

View File

@@ -52,6 +52,22 @@ describe("WebLockComponentService", () => {
});
});
describe("popOutBrowserExtension", () => {
it("throws platform not supported error", () => {
expect(() => service.popOutBrowserExtension()).toThrow(
"Method not supported on this platform.",
);
});
});
describe("closeBrowserExtensionPopout", () => {
it("throws platform not supported error", () => {
expect(() => service.closeBrowserExtensionPopout()).toThrow(
"Method not supported on this platform.",
);
});
});
describe("isWindowVisible", () => {
it("throws an error", async () => {
await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented.");

View File

@@ -24,6 +24,14 @@ export class WebLockComponentService implements LockComponentService {
return null;
}
popOutBrowserExtension(): Promise<void> {
throw new Error("Method not supported on this platform.");
}
closeBrowserExtensionPopout(): void {
throw new Error("Method not supported on this platform.");
}
async isWindowVisible(): Promise<boolean> {
throw new Error("Method not implemented.");
}

View File

@@ -12,7 +12,7 @@
<h1
bitTypography="h1"
noMargin
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex"
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
[title]="title || (routeData.titleId | i18n)"
>
<div class="tw-truncate">

View File

@@ -58,7 +58,6 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
import { RemovePasswordComponent } from "./auth/remove-password.component";
import { SetPasswordComponent } from "./auth/set-password.component";
import { AccountComponent } from "./auth/settings/account/account.component";
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
@@ -73,6 +72,7 @@ import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/com
import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver";
import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component";
import { RouteDataProperties } from "./core";
import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component";
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
import { UserLayoutComponent } from "./layouts/user-layout.component";
import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component";

View File

@@ -19,7 +19,13 @@ export class WebCommunicationProvider implements CommunicationBackend {
return;
}
await this.queue.enqueue({ ...message.message, source: "BrowserBackground" });
void this.queue.enqueue(
new IncomingMessage(
message.message.payload,
message.message.destination,
"BrowserBackground",
),
);
});
}

View File

@@ -15,7 +15,6 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/
import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component";
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { AccountComponent } from "../auth/settings/account/account.component";
import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component";
@@ -42,6 +41,7 @@ import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-famili
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
import { HeaderModule } from "../layouts/header/header.module";
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";
@@ -62,6 +62,9 @@ import { OrganizationBadgeModule } from "../vault/individual-vault/organization-
import { PipesModule } from "../vault/individual-vault/pipes/pipes.module";
import { PurgeVaultComponent } from "../vault/settings/purge-vault.component";
import { FreeBitwardenFamiliesComponent } from "./../billing/members/free-bitwarden-families.component";
import { OrganizationMemberFamiliesComponent } from "./../billing/members/organization-member-families.component";
import { OrganizationSponsoredFamiliesComponent } from "./../billing/members/organization-sponsored-families.component";
import { EnvironmentSelectorModule } from "./../components/environment-selector/environment-selector.module";
import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component";
import { SharedModule } from "./shared.module";
@@ -128,6 +131,9 @@ import { SharedModule } from "./shared.module";
SelectableAvatarComponent,
SetPasswordComponent,
SponsoredFamiliesComponent,
OrganizationSponsoredFamiliesComponent,
OrganizationMemberFamiliesComponent,
FreeBitwardenFamiliesComponent,
SponsoringOrgRowComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,
@@ -175,6 +181,9 @@ import { SharedModule } from "./shared.module";
SelectableAvatarComponent,
SetPasswordComponent,
SponsoredFamiliesComponent,
OrganizationSponsoredFamiliesComponent,
OrganizationMemberFamiliesComponent,
FreeBitwardenFamiliesComponent,
SponsoringOrgRowComponent,
UpdateTempPasswordComponent,
UpdatePasswordComponent,

View File

@@ -7,7 +7,7 @@
<bit-label>{{ "username" | i18n }}</bit-label>
<input id="username" type="text" formControlName="username" bitInput />
</bit-form-field>
<small class="form-text text-muted tw-mb-4">{{ "breachCheckUsernameEmail" | i18n }}</small>
<small class="tw-mb-4 tw-block tw-text-muted">{{ "breachCheckUsernameEmail" | i18n }}</small>
<button type="submit" buttonType="primary" bitButton [loading]="loading">
{{ "checkBreaches" | i18n }}
</button>
@@ -21,32 +21,33 @@
<bit-callout type="danger" title="{{ 'breachFound' | i18n }}" *ngIf="breachedAccounts.length">
{{ "breachUsernameFound" | i18n: checkedUsername : breachedAccounts.length }}
</bit-callout>
<ul class="list-group list-group-breach" *ngIf="breachedAccounts.length">
<li *ngFor="let a of breachedAccounts" class="list-group-item min-height-fix">
<div class="row">
<div class="col-2 tw-text-center">
<img [src]="a.logoPath" alt="" class="img-fluid" />
</div>
<div class="col-7">
<h3 class="tw-text-lg">{{ a.title }}</h3>
<p [innerHTML]="a.description"></p>
<p class="tw-mb-1">{{ "compromisedData" | i18n }}:</p>
<ul>
<li *ngFor="let d of a.dataClasses">{{ d }}</li>
</ul>
</div>
<div class="col-3">
<dl>
<dt>{{ "website" | i18n }}</dt>
<dd>{{ a.domain }}</dd>
<dt>{{ "affectedUsers" | i18n }}</dt>
<dd>{{ a.pwnCount | number }}</dd>
<dt>{{ "breachOccurred" | i18n }}</dt>
<dd>{{ a.breachDate | date: "mediumDate" }}</dd>
<dt>{{ "breachReported" | i18n }}</dt>
<dd>{{ a.addedDate | date: "mediumDate" }}</dd>
</dl>
</div>
<ul
class="tw-list-none tw-flex-col tw-divide-x-0 tw-divide-y tw-divide-solid tw-divide-secondary-300 tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-p-0"
*ngIf="breachedAccounts.length"
>
<li *ngFor="let a of breachedAccounts" class="tw-flex tw-gap-4 tw-p-4">
<div class="tw-w-32 tw-flex-none">
<img [src]="a.logoPath" alt="" class="tw-max-w-32 tw-items-stretch" />
</div>
<div class="tw-flex-auto">
<h3 class="tw-text-lg">{{ a.title }}</h3>
<p [innerHTML]="a.description"></p>
<p class="tw-mb-1">{{ "compromisedData" | i18n }}:</p>
<ul>
<li *ngFor="let d of a.dataClasses">{{ d }}</li>
</ul>
</div>
<div class="tw-w-48 tw-flex-none">
<dl>
<dt>{{ "website" | i18n }}</dt>
<dd>{{ a.domain }}</dd>
<dt>{{ "affectedUsers" | i18n }}</dt>
<dd>{{ a.pwnCount | number }}</dd>
<dt>{{ "breachOccurred" | i18n }}</dt>
<dd>{{ a.breachDate | date: "mediumDate" }}</dd>
<dt>{{ "breachReported" | i18n }}</dt>
<dd>{{ a.addedDate | date: "mediumDate" }}</dd>
</dl>
</div>
</li>
</ul>

View File

@@ -7,6 +7,7 @@ import { firstValueFrom, Subject, switchMap } from "rxjs";
import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -39,6 +40,9 @@ import {
ToastService,
} from "@bitwarden/components";
import {
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
ChangeLoginPasswordService,
CipherFormComponent,
CipherFormConfig,
@@ -50,16 +54,10 @@ import {
} from "@bitwarden/vault";
import { SharedModule } from "../../../shared/shared.module";
import {
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
} from "../../individual-vault/attachments-v2.component";
import { WebVaultPremiumUpgradePromptService } from "../../../vault/services/web-premium-upgrade-prompt.service";
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service";
import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service";
import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service";
export type VaultItemDialogMode = "view" | "form";
@@ -135,7 +133,7 @@ export enum VaultItemDialogResult {
],
providers: [
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
{ provide: ViewPasswordHistoryService, useClass: VaultViewPasswordHistoryService },
{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService },
RoutedVaultFilterService,
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },

View File

@@ -21,6 +21,7 @@ import {
ItemModule,
} from "@bitwarden/components";
import {
AttachmentsV2Component,
CipherAttachmentsComponent,
CipherFormConfig,
CipherFormGenerationService,
@@ -31,8 +32,6 @@ import {
import { SharedModule } from "../../shared/shared.module";
import { WebCipherFormGenerationService } from "../services/web-cipher-form-generation.service";
import { AttachmentsV2Component } from "./attachments-v2.component";
/**
* The result of the AddEditCipherDialogV2 component.
*/

View File

@@ -1,19 +0,0 @@
<bit-dialog dialogSize="default" background="alt">
<span bitDialogTitle>
{{ "attachments" | i18n }}
</span>
<ng-container bitDialogContent>
<app-cipher-attachments
*ngIf="cipherId"
[cipherId]="cipherId"
[submitBtn]="submitBtn"
(onUploadSuccess)="uploadSuccessful()"
(onRemoveSuccess)="removalSuccessful()"
></app-cipher-attachments>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton type="submit" buttonType="primary" [attr.form]="attachmentFormId" #submitBtn>
{{ "upload" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -1,65 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { mock } from "jest-mock-extended";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DIALOG_DATA, DialogRef } from "@bitwarden/components";
import {
AttachmentsV2Component,
AttachmentDialogResult,
AttachmentsDialogParams,
} from "./attachments-v2.component";
describe("AttachmentsV2Component", () => {
let component: AttachmentsV2Component;
let fixture: ComponentFixture<AttachmentsV2Component>;
const mockCipherId: CipherId = "cipher-id" as CipherId;
const mockParams: AttachmentsDialogParams = {
cipherId: mockCipherId,
};
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AttachmentsV2Component, NoopAnimationsModule],
providers: [
{ provide: DIALOG_DATA, useValue: mockParams },
{ provide: DialogRef, useValue: mock<DialogRef>() },
{ provide: I18nService, useValue: mock<I18nService>() },
{ provide: CipherService, useValue: mock<CipherService>() },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: AccountService, useValue: mock<AccountService>() },
],
}).compileComponents();
fixture = TestBed.createComponent(AttachmentsV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("initializes without errors and with the correct cipherId", () => {
expect(component).toBeTruthy();
expect(component.cipherId).toBe(mockParams.cipherId);
});
it("closes the dialog with 'uploaded' result on uploadSuccessful", () => {
const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close");
component.uploadSuccessful();
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: AttachmentDialogResult.Uploaded });
});
it("closes the dialog with 'removed' result on removalSuccessful", () => {
const dialogRefCloseSpy = jest.spyOn(component["dialogRef"], "close");
component.removalSuccessful();
expect(dialogRefCloseSpy).toHaveBeenCalledWith({ action: AttachmentDialogResult.Removed });
});
});

View File

@@ -1,88 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Inject } from "@angular/core";
import { CipherId } from "@bitwarden/common/types/guid";
import { DialogRef, DIALOG_DATA, DialogService } from "@bitwarden/components";
import { CipherAttachmentsComponent } from "@bitwarden/vault";
import { SharedModule } from "../../shared/shared.module";
export interface AttachmentsDialogParams {
cipherId: CipherId;
}
/**
* Enum representing the possible results of the attachment dialog.
*/
export enum AttachmentDialogResult {
Uploaded = "uploaded",
Removed = "removed",
Closed = "closed",
}
export interface AttachmentDialogCloseResult {
action: AttachmentDialogResult;
}
/**
* Component for the attachments dialog.
*/
@Component({
selector: "app-vault-attachments-v2",
templateUrl: "attachments-v2.component.html",
standalone: true,
imports: [CommonModule, SharedModule, CipherAttachmentsComponent],
})
export class AttachmentsV2Component {
cipherId: CipherId;
attachmentFormId = CipherAttachmentsComponent.attachmentFormID;
/**
* Constructor for AttachmentsV2Component.
* @param dialogRef - Reference to the dialog.
* @param params - Parameters passed to the dialog.
*/
constructor(
private dialogRef: DialogRef<AttachmentDialogCloseResult>,
@Inject(DIALOG_DATA) public params: AttachmentsDialogParams,
) {
this.cipherId = params.cipherId;
}
/**
* Opens the attachments dialog.
* @param dialogService - The dialog service.
* @param params - The parameters for the dialog.
* @returns The dialog reference.
*/
static open(
dialogService: DialogService,
params: AttachmentsDialogParams,
): DialogRef<AttachmentDialogCloseResult> {
return dialogService.open(AttachmentsV2Component, {
data: params,
});
}
/**
* Called when an attachment is successfully uploaded.
* Closes the dialog with an 'uploaded' result.
*/
uploadSuccessful() {
this.dialogRef.close({
action: AttachmentDialogResult.Uploaded,
});
}
/**
* Called when an attachment is successfully removed.
* Closes the dialog with a 'removed' result.
*/
removalSuccessful() {
this.dialogRef.close({
action: AttachmentDialogResult.Removed,
});
}
}

Some files were not shown because too many files have changed in this diff Show More