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:
@@ -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,
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SponsoredFamily {
|
||||
sponsorshipEmail?: string;
|
||||
sponsorshipNote?: string;
|
||||
status?: string;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
@@ -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])),
|
||||
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-abm-enterprise-content",
|
||||
templateUrl: "abm-enterprise-content.component.html",
|
||||
})
|
||||
export class AbmEnterpriseContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-abm-teams-content",
|
||||
templateUrl: "abm-teams-content.component.html",
|
||||
})
|
||||
export class AbmTeamsContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-cnet-enterprise-content",
|
||||
templateUrl: "cnet-enterprise-content.component.html",
|
||||
})
|
||||
export class CnetEnterpriseContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-cnet-individual-content",
|
||||
templateUrl: "cnet-individual-content.component.html",
|
||||
})
|
||||
export class CnetIndividualContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-cnet-teams-content",
|
||||
templateUrl: "cnet-teams-content.component.html",
|
||||
})
|
||||
export class CnetTeamsContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-default-content",
|
||||
templateUrl: "default-content.component.html",
|
||||
})
|
||||
export class DefaultContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-enterprise-content",
|
||||
templateUrl: "enterprise-content.component.html",
|
||||
})
|
||||
export class EnterpriseContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-enterprise1-content",
|
||||
templateUrl: "enterprise1-content.component.html",
|
||||
})
|
||||
export class Enterprise1ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-enterprise2-content",
|
||||
templateUrl: "enterprise2-content.component.html",
|
||||
})
|
||||
export class Enterprise2ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-badges",
|
||||
templateUrl: "logo-badges.component.html",
|
||||
})
|
||||
export class LogoBadgesComponent {}
|
||||
@@ -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>
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-cnet",
|
||||
templateUrl: "logo-cnet.component.html",
|
||||
})
|
||||
export class LogoCnetComponent {}
|
||||
@@ -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>
|
||||
“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.”
|
||||
</blockquote>
|
||||
<p class="tw-font-bold">Best Password Manager in 2024</p>
|
||||
</figure>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-company-testimonial",
|
||||
templateUrl: "logo-company-testimonial.component.html",
|
||||
})
|
||||
export class LogoCompanyTestimonialComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-forbes",
|
||||
templateUrl: "logo-forbes.component.html",
|
||||
})
|
||||
export class LogoForbesComponent {}
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-logo-us-news",
|
||||
templateUrl: "logo-us-news.component.html",
|
||||
})
|
||||
export class LogoUSNewsComponent {}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams-content",
|
||||
templateUrl: "teams-content.component.html",
|
||||
})
|
||||
export class TeamsContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams1-content",
|
||||
templateUrl: "teams1-content.component.html",
|
||||
})
|
||||
export class Teams1ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams2-content",
|
||||
templateUrl: "teams2-content.component.html",
|
||||
})
|
||||
export class Teams2ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-teams3-content",
|
||||
templateUrl: "teams3-content.component.html",
|
||||
})
|
||||
export class Teams3ContentComponent {}
|
||||
@@ -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>
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user