1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 08:33:29 +00:00

merge in refactor branch

This commit is contained in:
Jimmy Vo
2024-12-11 16:01:51 -05:00
1936 changed files with 17834 additions and 4316 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2024.11.2",
"version": "2024.12.0",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
import { FormControl } from "@angular/forms";
import { firstValueFrom, concatMap, map, lastValueFrom, startWith, debounceTime } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
OrganizationUserStatusType,
ProviderUserStatusType,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
export class GroupRequest {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
import { View } from "@bitwarden/common/models/view/view";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { View } from "@bitwarden/common/models/view/view";
import { GroupResponse } from "../services/group/responses/group.response";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
CollectionAccessSelectionView,
OrganizationUserDetailsResponse,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
OrganizationUserUserDetailsResponse,
CollectionAccessSelectionView,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { UntypedFormGroup } from "@angular/forms";
import { firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { provideRouter } from "@angular/router";
@@ -59,8 +61,14 @@ describe("Is Enterprise Org Guard", () => {
{
path: "organizations/:organizationId/enterpriseOrgsOnly",
component: IsEnterpriseOrganizationComponent,
canActivate: [isEnterpriseOrgGuard()],
canActivate: [isEnterpriseOrgGuard(true)],
},
{
path: "organizations/:organizationId/enterpriseOrgsOnlyNoError",
component: IsEnterpriseOrganizationComponent,
canActivate: [isEnterpriseOrgGuard(false)],
},
{
path: "organizations/:organizationId/billing/subscription",
component: OrganizationUpgradeScreenComponent,
@@ -115,6 +123,24 @@ describe("Is Enterprise Org Guard", () => {
);
});
it.each([
ProductTierType.Free,
ProductTierType.Families,
ProductTierType.Teams,
ProductTierType.TeamsStarter,
])("does not proceed with the navigation for productTierType '%s'", async (productTierType) => {
const org = orgFactory({
type: OrganizationUserType.User,
productTierType: productTierType,
});
organizationService.get.calledWith(org.id).mockResolvedValue(org);
await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnlyNoError`);
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
expect(
routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "",
).not.toBe("This component can only be accessed by a enterprise organization!");
});
it("proceeds with navigation if the organization in question is a enterprise organization", async () => {
const org = orgFactory({ productTierType: ProductTierType.Enterprise });
organizationService.get.calledWith(org.id).mockResolvedValue(org);

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import {
ActivatedRouteSnapshot,
@@ -17,7 +19,7 @@ import { DialogService } from "@bitwarden/components";
* if they have access to upgrade the organization. If the organization is
* enterprise routing proceeds."
*/
export function isEnterpriseOrgGuard(): CanActivateFn {
export function isEnterpriseOrgGuard(showError: boolean = true): CanActivateFn {
return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
const router = inject(Router);
const organizationService = inject(OrganizationService);
@@ -29,7 +31,7 @@ export function isEnterpriseOrgGuard(): CanActivateFn {
return router.createUrlTree(["/"]);
}
if (org.productTierType != ProductTierType.Enterprise) {
if (org.productTierType != ProductTierType.Enterprise && showError) {
// Users without billing permission can't access billing
if (!org.canEditSubscription) {
await dialogService.openSimpleDialog({

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { provideRouter } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import {
ActivatedRouteSnapshot,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TestBed } from "@angular/core/testing";
import {
ActivatedRouteSnapshot,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import {
ActivatedRouteSnapshot,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { provideRouter } from "@angular/router";

View File

@@ -0,0 +1,66 @@
<app-header> </app-header>
<bit-tab-group [(selectedIndex)]="tabIndex">
<bit-tab [label]="'singleSignOn' | i18n">
<section class="tw-mb-9">
<h2 bitTypography="h2">{{ "singleSignOn" | i18n }}</h2>
<p bitTypography="body1">
{{ "ssoDescStart" | i18n }}
<a bitLink routerLink="../settings/sso" class="tw-lowercase">{{ "singleSignOn" | i18n }}</a>
{{ "ssoDescEnd" | i18n }}
</p>
<app-integration-grid
[integrations]="integrationsList | filterIntegrations: IntegrationType.SSO"
></app-integration-grid>
</section>
</bit-tab>
<bit-tab [label]="'userProvisioning' | i18n">
<section class="tw-mb-9">
<h2 bitTypography="h2">
{{ "scimIntegration" | i18n }}
</h2>
<p bitTypography="body1">
{{ "scimIntegrationDescStart" | i18n }}
<a bitLink routerLink="../settings/scim">{{ "scimIntegration" | i18n }}</a>
{{ "scimIntegrationDescEnd" | i18n }}
</p>
<app-integration-grid
[integrations]="integrationsList | filterIntegrations: IntegrationType.SCIM"
></app-integration-grid>
</section>
<section class="tw-mb-9">
<h2 bitTypography="h2">
{{ "bwdc" | i18n }}
</h2>
<p bitTypography="body1">{{ "bwdcDesc" | i18n }}</p>
<app-integration-grid
[integrations]="integrationsList | filterIntegrations: IntegrationType.BWDC"
></app-integration-grid>
</section>
</bit-tab>
<bit-tab [label]="'eventManagement' | i18n">
<section class="tw-mb-9">
<h2 bitTypography="h2">
{{ "eventManagement" | i18n }}
</h2>
<p bitTypography="body1">{{ "eventManagementDesc" | i18n }}</p>
<app-integration-grid
[integrations]="integrationsList | filterIntegrations: IntegrationType.EVENT"
></app-integration-grid>
</section>
</bit-tab>
<bit-tab [label]="'deviceManagement' | i18n">
<section class="tw-mb-9">
<h2 bitTypography="h2">
{{ "deviceManagement" | i18n }}
</h2>
<p bitTypography="body1">{{ "deviceManagementDesc" | i18n }}</p>
<app-integration-grid
[integrations]="integrationsList | filterIntegrations: IntegrationType.DEVICE"
></app-integration-grid>
</section>
</bit-tab>
</bit-tab-group>

View File

@@ -0,0 +1,209 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { IntegrationType } from "@bitwarden/common/enums";
import { HeaderModule } from "../../../layouts/header/header.module";
import { FilterIntegrationsPipe, IntegrationGridComponent, Integration } from "../../../shared/";
import { SharedModule } from "../../../shared/shared.module";
import { SharedOrganizationModule } from "../shared";
@Component({
selector: "ac-integrations",
templateUrl: "./integrations.component.html",
standalone: true,
imports: [
SharedModule,
SharedOrganizationModule,
IntegrationGridComponent,
HeaderModule,
FilterIntegrationsPipe,
],
})
export class AdminConsoleIntegrationsComponent {
integrationsList: Integration[] = [];
tabIndex: number;
constructor() {
this.integrationsList = [
{
name: "AD FS",
linkURL: "https://bitwarden.com/help/saml-adfs/",
image: "../../../../../../../images/integrations/azure-active-directory.svg",
type: IntegrationType.SSO,
},
{
name: "Auth0",
linkURL: "https://bitwarden.com/help/saml-auth0/",
image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "AWS",
linkURL: "https://bitwarden.com/help/saml-aws/",
image: "../../../../../../../images/integrations/aws-color.svg",
imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/saml-azure/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.SSO,
},
{
name: "Duo",
linkURL: "https://bitwarden.com/help/saml-duo/",
image: "../../../../../../../images/integrations/logo-duo-color.svg",
type: IntegrationType.SSO,
},
{
name: "Google",
linkURL: "https://bitwarden.com/help/saml-google/",
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "JumpCloud",
linkURL: "https://bitwarden.com/help/saml-jumpcloud/",
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "KeyCloak",
linkURL: "https://bitwarden.com/help/saml-keycloak/",
image: "../../../../../../../images/integrations/logo-keycloak-icon.svg",
type: IntegrationType.SSO,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/saml-okta/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/saml-onelogin/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.SSO,
},
{
name: "PingFederate",
linkURL: "https://bitwarden.com/help/saml-pingfederate/",
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
type: IntegrationType.SSO,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.SCIM,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/okta-scim-integration/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/onelogin-scim-integration/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "JumpCloud",
linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/",
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
type: IntegrationType.SCIM,
},
{
name: "Ping Identity",
linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/",
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
type: IntegrationType.SCIM,
},
{
name: "Active Directory",
linkURL: "https://bitwarden.com/help/ldap-directory/",
image: "../../../../../../../images/integrations/azure-active-directory.svg",
type: IntegrationType.BWDC,
},
{
name: "Microsoft Entra ID",
linkURL: "https://bitwarden.com/help/microsoft-entra-id/",
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
type: IntegrationType.BWDC,
},
{
name: "Google Workspace",
linkURL: "https://bitwarden.com/help/workspace-directory/",
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
type: IntegrationType.BWDC,
},
{
name: "Okta",
linkURL: "https://bitwarden.com/help/okta-directory/",
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
type: IntegrationType.BWDC,
},
{
name: "OneLogin",
linkURL: "https://bitwarden.com/help/onelogin-directory/",
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
type: IntegrationType.BWDC,
},
{
name: "Splunk",
linkURL: "https://bitwarden.com/help/splunk-siem/",
image: "../../../../../../../images/integrations/logo-splunk-black.svg",
imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg",
type: IntegrationType.EVENT,
},
{
name: "Microsoft Sentinel",
linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/",
image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Rapid7",
linkURL: "https://bitwarden.com/help/rapid7-siem/",
image: "../../../../../../../images/integrations/logo-rapid7-black.svg",
imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg",
type: IntegrationType.EVENT,
},
{
name: "Elastic",
linkURL: "https://bitwarden.com/help/elastic-siem/",
image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Panther",
linkURL: "https://bitwarden.com/help/panther-siem/",
image: "../../../../../../../images/integrations/logo-panther-round-color.svg",
type: IntegrationType.EVENT,
},
{
name: "Microsoft Intune",
linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
type: IntegrationType.DEVICE,
},
];
}
get IntegrationType(): typeof IntegrationType {
return IntegrationType;
}
}

View File

@@ -4,7 +4,7 @@
<org-switcher [filter]="orgFilter" [hideNewButton]="hideNewOrgButton$ | async"></org-switcher>
<bit-nav-group
icon="bwi-filter"
*ngIf="isAccessIntelligenceFeatureEnabled"
*ngIf="organization.useRiskInsights"
[text]="'accessIntelligence' | i18n"
>
<bit-nav-item
@@ -60,6 +60,12 @@
<bit-nav-item [text]="'billingHistory' | i18n" route="billing/history"></bit-nav-item>
</ng-container>
</bit-nav-group>
<bit-nav-item
icon="bwi-providers"
[text]="'integrations' | i18n"
route="integrations"
*ngIf="integrationPageEnabled$ | async"
></bit-nav-item>
<bit-nav-group
icon="bwi-cog"
[text]="'settings' | i18n"
@@ -92,7 +98,7 @@
*ngIf="canAccessExport$ | async"
></bit-nav-item>
<bit-nav-item
[text]="'domainVerification' | i18n"
[text]="domainVerificationNavigationTextKey | i18n"
route="settings/domain-verification"
*ngIf="organization?.canManageDomainVerification"
></bit-nav-item>

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router";
@@ -18,8 +20,10 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
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";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { getById } from "@bitwarden/common/platform/misc";
import { BannerModule, IconModule } from "@bitwarden/components";
@@ -46,6 +50,9 @@ export class OrganizationLayoutComponent implements OnInit {
protected readonly logo = AdminConsoleLogo;
protected orgFilter = (org: Organization) => canAccessOrgAdmin(org);
protected domainVerificationNavigationTextKey: string;
protected integrationPageEnabled$: Observable<boolean>;
organization$: Observable<Organization>;
canAccessExport$: Observable<boolean>;
@@ -53,6 +60,7 @@ export class OrganizationLayoutComponent implements OnInit {
hideNewOrgButton$: Observable<boolean>;
organizationIsUnmanaged$: Observable<boolean>;
isAccessIntelligenceFeatureEnabled = false;
enterpriseOrganization$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
@@ -61,15 +69,12 @@ export class OrganizationLayoutComponent implements OnInit {
private configService: ConfigService,
private policyService: PolicyService,
private providerService: ProviderService,
private i18nService: I18nService,
) {}
async ngOnInit() {
document.body.classList.remove("layout_frontend");
this.isAccessIntelligenceFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccessIntelligence,
);
this.organization$ = this.route.params.pipe(
map((p) => p.organizationId),
switchMap((id) => this.organizationService.organizations$.pipe(getById(id))),
@@ -104,6 +109,22 @@ export class OrganizationLayoutComponent implements OnInit {
provider.providerStatus !== ProviderStatusType.Billable,
),
);
this.integrationPageEnabled$ = combineLatest(
this.organization$,
this.configService.getFeatureFlag$(FeatureFlag.PM14505AdminConsoleIntegrationPage),
).pipe(
map(
([org, featureFlagEnabled]) =>
org.productTierType === ProductTierType.Enterprise && featureFlagEnabled,
),
);
this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag(
FeatureFlag.AccountDeprovisioning,
))
? "claimedDomains"
: "domainVerification";
}
canShowVaultTab(organization: Organization): boolean {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// 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, Router } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
import {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive } from "@angular/core";
import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

View File

@@ -8,7 +8,7 @@
</bit-callout>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error">
<p bitTypography="body1">{{ "deleteOrganizationUserWarning" | i18n }}</p>
<p bitTypography="body1">{{ "deleteManyOrganizationUsersWarningDesc" | i18n }}</p>
</bit-callout>
<bit-table>
<ng-container header>

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";

View File

@@ -1,4 +1,4 @@
<bit-dialog dialogSize="large" [title]="'removeUsers' | i18n">
<bit-dialog dialogSize="large" [title]="'removeMembers' | i18n">
<ng-container bitDialogContent>
<bit-callout type="danger" *ngIf="users.length <= 0">
{{ "noSelectedUsersApplicable" | i18n }}
@@ -79,7 +79,7 @@
[disabled]="loading"
[bitAction]="submit"
>
{{ "removeUsers" | i18n }}
{{ "removeMembers" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";

View File

@@ -1,7 +1,14 @@
<bit-dialog>
<bit-dialog
dialogSize="large"
*ngIf="{ enabled: accountDeprovisioningEnabled$ | async } as accountDeprovisioning"
>
<ng-container bitDialogTitle>
<h1>{{ bulkTitle }}</h1>
<h1 *ngIf="accountDeprovisioning.enabled; else nonMemberTitle">{{ bulkMemberTitle }}</h1>
<ng-template #nonMemberTitle>
<h1>{{ bulkTitle }}</h1>
</ng-template>
</ng-container>
<div bitDialogContent>
<bit-callout type="danger" *ngIf="users.length <= 0">
{{ "noSelectedUsersApplicable" | i18n }}
@@ -11,15 +18,81 @@
{{ error }}
</bit-callout>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeUsersWarning" | i18n }}</p>
<p *ngIf="this.showNoMasterPasswordWarning">
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
</p>
</bit-callout>
<bit-callout
type="danger"
*ngIf="nonCompliantMembers && accountDeprovisioning.enabled"
title="{{ 'nonCompliantMembersTitle' | i18n }}"
>
{{ "nonCompliantMembersError" | i18n }}
</bit-callout>
<bit-table>
<ng-container *ngIf="!done">
<ng-container *ngIf="accountDeprovisioning.enabled">
<div *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeMembersWarning" | i18n }}</p>
<ul>
<li>
{{ "claimedAccountRevoke" | i18n }}
</li>
<li>
{{ "unclaimedAccountRevoke" | i18n }}
</li>
</ul>
<p>
{{ "restoreMembersInstructions" | i18n }}
</p>
<p *ngIf="this.showNoMasterPasswordWarning">
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
</p>
</div>
</ng-container>
<ng-container *ngIf="!accountDeprovisioning.enabled">
<bit-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeUsersWarning" | i18n }}</p>
</bit-callout>
</ng-container>
<bit-table *ngIf="accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell *ngIf="isRevoking">{{ "details" | i18n }}</th>
<th bitCell *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
</td>
<td *ngIf="isRevoking" bitCell width="30">
{{
user.managedByOrganization ? ("claimedAccount" | i18n) : ("unclaimedAccount" | i18n)
}}
</td>
<td bitCell *ngIf="this.showNoMasterPasswordWarning">
<span class="tw-block tw-lowercase tw-text-muted">
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
<ng-container *ngIf="user.hasMasterPassword === false">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
{{ "noMasterPassword" | i18n }}
</ng-container>
</span>
</td>
</tr>
</ng-template>
</bit-table>
<bit-table *ngIf="!accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell colspan="2">{{ "user" | i18n }}</th>
@@ -50,21 +123,55 @@
</ng-container>
<ng-container *ngIf="done">
<bit-table>
<bit-table *ngIf="accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell colspan="2">{{ "user" | i18n }}</th>
<th>{{ "status" | i18n }}</th>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell>{{ "status" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
</td>
<td bitCell>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
<td bitCell *ngIf="statuses.has(user.id)">
{{ statuses.get(user.id) }}
</td>
<td bitCell *ngIf="!statuses.has(user.id)">
{{ "bulkFilteredMessage" | i18n }}
</td>
</tr>
</ng-template>
</bit-table>
<bit-table *ngIf="!accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell>{{ "status" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
</td>
<td bitCell *ngIf="statuses.has(user.id)">
{{ statuses.get(user.id) }}
@@ -79,7 +186,7 @@
</div>
<ng-container bitDialogFooter>
<button type="button" bitButton *ngIf="!done && users.length > 0" [bitAction]="submit">
{{ bulkTitle }}
{{ accountDeprovisioning.enabled ? bulkMemberTitle : bulkTitle }}
</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}

View File

@@ -1,8 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { Observable } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
@@ -29,10 +34,13 @@ export class BulkRestoreRevokeComponent {
done = false;
error: string;
showNoMasterPasswordWarning = false;
nonCompliantMembers: boolean = false;
accountDeprovisioningEnabled$: Observable<boolean>;
constructor(
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
) {
this.isRevoking = data.isRevoking;
@@ -41,6 +49,9 @@ export class BulkRestoreRevokeComponent {
this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
get bulkTitle() {
@@ -48,14 +59,26 @@ export class BulkRestoreRevokeComponent {
return this.i18nService.t(titleKey);
}
get bulkMemberTitle() {
const titleKey = this.isRevoking ? "revokeMembers" : "restoreMembers";
return this.i18nService.t(titleKey);
}
submit = async () => {
try {
const response = await this.performBulkUserAction();
const bulkMessage = this.isRevoking ? "bulkRevokedMessage" : "bulkRestoredMessage";
response.data.forEach((entry) => {
const error = entry.error !== "" ? entry.error : this.i18nService.t(bulkMessage);
response.data.forEach(async (entry) => {
const error =
entry.error !== ""
? this.i18nService.t("cannotRestoreAccessError")
: this.i18nService.t(bulkMessage);
this.statuses.set(entry.id, error);
if (entry.error !== "") {
this.nonCompliantMembers = true;
}
});
this.done = true;
} catch (e) {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
@@ -21,6 +23,7 @@ export interface BulkUserDetails {
email: string;
status: OrganizationUserStatusType | ProviderUserStatusType;
hasMasterPassword?: boolean;
managedByOrganization?: boolean;
}
type BulkStatusEntry = {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
@@ -50,6 +52,7 @@ import {
} from "../../../shared/components/access-selector";
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
import { inputEmailLimitValidator } from "./validators/input-email-limit.validator";
import { orgSeatLimitReachedValidator } from "./validators/org-seat-limit-reached.validator";
export enum MemberDialogTab {
@@ -172,8 +175,21 @@ export class MemberDialogComponent implements OnDestroy {
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.editMode = this.params.organizationUserId != null;
let userDetails$;
if (this.editMode) {
this.title = this.i18nService.t("editMember");
userDetails$ = this.userService.get(
this.params.organizationId,
this.params.organizationUserId,
);
} else {
this.title = this.i18nService.t("inviteMember");
userDetails$ = of(null);
}
this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role;
this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember");
this.isOnSecretsManagerStandalone = this.params.isOnSecretsManagerStandalone;
if (this.isOnSecretsManagerStandalone) {
@@ -190,10 +206,6 @@ export class MemberDialogComponent implements OnDestroy {
),
);
const userDetails$ = this.params.organizationUserId
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
: of(null);
this.allowAdminAccessToAllCollectionItems$ = this.organization$.pipe(
map((organization) => {
return organization.allowAdminAccessToAllCollectionItems;
@@ -275,97 +287,28 @@ export class MemberDialogComponent implements OnDestroy {
}
private setFormValidators(organization: Organization) {
if (this.params.kind === "InviteMemberDialogParams") {
const emailsControlValidators = [
Validators.required,
commaSeparatedEmails,
orgSeatLimitReachedValidator(
organization,
this.params.allOrganizationUserEmails,
this.getSeatLimitErrorMessageForPlan(organization),
this.params.occupiedSeatCount,
),
];
const _orgSeatLimitReachedValidator = [
Validators.required,
commaSeparatedEmails,
orgSeatLimitReachedValidator(
organization,
this.params.allOrganizationUserEmails,
this.i18nService.t("subscriptionUpgrade", organization.seats),
),
];
const emailsControl = this.formGroup.get("emails");
emailsControl.setValidators(emailsControlValidators);
emailsControl.updateValueAndValidity();
}
}
const _inputEmailLimitValidator = [
Validators.required,
commaSeparatedEmails,
inputEmailLimitValidator(organization, (maxEmailsCount: number) =>
this.i18nService.t("tooManyEmails", maxEmailsCount),
),
];
private getSeatLimitErrorMessageForPlan(organization: Organization): string {
const { seats, hasReseller } = organization;
if (hasReseller) {
return this.i18nService.t("seatLimitReachedContactYourProvider", seats);
}
return this.i18nService.t("subscriptionUpgrade", seats);
}
private async handleInviteUsers(userView: OrganizationUserAdminView, organization: Organization) {
userView.id = this.params.organizationUserId;
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
if (this.enforceEmailCountLimit(emails, organization)) {
return;
}
await this.userService.invite(emails, userView);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("invitedUsers", this.params.name),
});
this.close(MemberDialogResult.Saved);
}
private enforceEmailCountLimit(emails: string[], organization: Organization): boolean {
const maxEmailsCount = organization.productTierType === ProductTierType.TeamsStarter ? 10 : 20;
if (emails.length > maxEmailsCount) {
this.formGroup.controls.emails.setErrors({
tooManyEmails: { message: this.i18nService.t("tooManyEmails", maxEmailsCount) },
});
return true;
}
return false;
}
private async handleEditUser(userView: OrganizationUserAdminView) {
await this.userService.save(userView);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("editedUserId", this.params.name),
});
this.close(MemberDialogResult.Saved);
}
private async getUserView(): Promise<OrganizationUserAdminView> {
const userView = new OrganizationUserAdminView();
userView.id = this.params.organizationUserId;
userView.organizationId = this.params.organizationId;
userView.type = this.formGroup.value.type;
userView.permissions = this.setRequestPermissions(
userView.permissions ?? new PermissionsApi(),
userView.type !== OrganizationUserType.Custom,
);
userView.collections = this.formGroup.value.access
.filter((v) => v.type === AccessItemType.Collection)
.map(convertToSelectionView);
userView.groups = (await firstValueFrom(this.restrictEditingSelf$))
? null
: this.formGroup.value.groups.map((m) => m.id);
userView.accessSecretsManager = this.formGroup.value.accessSecretsManager;
return userView;
const emailsControl = this.formGroup.get("emails");
emailsControl.setValidators(_orgSeatLimitReachedValidator);
emailsControl.setValidators(_inputEmailLimitValidator);
emailsControl.updateValueAndValidity();
}
private loadOrganizationUser(
@@ -519,6 +462,55 @@ export class MemberDialogComponent implements OnDestroy {
}
};
private async getUserView(): Promise<OrganizationUserAdminView> {
const userView = new OrganizationUserAdminView();
userView.organizationId = this.params.organizationId;
userView.type = this.formGroup.value.type;
userView.permissions = this.setRequestPermissions(
userView.permissions ?? new PermissionsApi(),
userView.type !== OrganizationUserType.Custom,
);
userView.collections = this.formGroup.value.access
.filter((v) => v.type === AccessItemType.Collection)
.map(convertToSelectionView);
userView.groups = (await firstValueFrom(this.restrictEditingSelf$))
? null
: this.formGroup.value.groups.map((m) => m.id);
userView.accessSecretsManager = this.formGroup.value.accessSecretsManager;
return userView;
}
private async handleEditUser(userView: OrganizationUserAdminView) {
userView.id = this.params.organizationUserId;
await this.userService.save(userView);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("editedUserId", this.params.name),
});
this.close(MemberDialogResult.Saved);
}
private async handleInviteUsers(userView: OrganizationUserAdminView, organization: Organization) {
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
await this.userService.invite(emails, userView);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("invitedUsers"),
});
this.close(MemberDialogResult.Saved);
}
remove = async () => {
if (!this.editMode) {
return;
@@ -626,7 +618,10 @@ export class MemberDialogComponent implements OnDestroy {
key: "deleteOrganizationUser",
placeholders: [this.params.name],
},
content: { key: "deleteOrganizationUserWarning" },
content: {
key: "deleteOrganizationUserWarningDesc",
placeholders: [this.params.name],
},
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { KeyValue } from "@angular/common";
import { Component, Input, OnInit, OnDestroy } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FormControl } from "@angular/forms";
import { commaSeparatedEmails } from "./comma-separated-emails.validator";

View File

@@ -0,0 +1,191 @@
import { AbstractControl, FormControl } from "@angular/forms";
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { inputEmailLimitValidator } from "./input-email-limit.validator";
const orgFactory = (props: Partial<Organization> = {}) =>
Object.assign(
new Organization(),
{
id: "myOrgId",
enabled: true,
type: OrganizationUserType.Admin,
},
props,
);
describe("inputEmailLimitValidator", () => {
const getErrorMessage = (max: number) => `You can only add up to ${max} unique emails.`;
const createUniqueEmailString = (numberOfEmails: number) =>
Array(numberOfEmails)
.fill(null)
.map((_, i) => `email${i}@example.com`)
.join(", ");
const createIdenticalEmailString = (numberOfEmails: number) =>
Array(numberOfEmails)
.fill(null)
.map(() => `email@example.com`)
.join(", ");
describe("TeamsStarter limit validation", () => {
let teamsStarterOrganization: Organization;
beforeEach(() => {
teamsStarterOrganization = orgFactory({
productTierType: ProductTierType.TeamsStarter,
seats: 10,
});
});
it("should return null if unique email count is within the limit", () => {
// Arrange
const control = new FormControl(createUniqueEmailString(3));
const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toBeNull();
});
it("should return null if unique email count is equal the limit", () => {
// Arrange
const control = new FormControl(createUniqueEmailString(10));
const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toBeNull();
});
it("should return an error if unique email count exceeds the limit", () => {
// Arrange
const control = new FormControl(createUniqueEmailString(11));
const validatorFn = inputEmailLimitValidator(teamsStarterOrganization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toEqual({
tooManyEmails: { message: "You can only add up to 10 unique emails." },
});
});
});
describe("Non-TeamsStarter limit validation", () => {
let nonTeamsStarterOrganization: Organization;
beforeEach(() => {
nonTeamsStarterOrganization = orgFactory({
productTierType: ProductTierType.Enterprise,
seats: 100,
});
});
it("should return null if unique email count is within the limit", () => {
// Arrange
const control = new FormControl(createUniqueEmailString(3));
const validatorFn = inputEmailLimitValidator(nonTeamsStarterOrganization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toBeNull();
});
it("should return null if unique email count is equal the limit", () => {
// Arrange
const control = new FormControl(createUniqueEmailString(10));
const validatorFn = inputEmailLimitValidator(nonTeamsStarterOrganization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toBeNull();
});
it("should return an error if unique email count exceeds the limit", () => {
// Arrange
const control = new FormControl(createUniqueEmailString(21));
const validatorFn = inputEmailLimitValidator(nonTeamsStarterOrganization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toEqual({
tooManyEmails: { message: "You can only add up to 20 unique emails." },
});
});
});
describe("input email validation", () => {
let organization: Organization;
beforeEach(() => {
organization = orgFactory({
productTierType: ProductTierType.Enterprise,
seats: 100,
});
});
it("should ignore duplicate emails and validate only unique ones", () => {
// Arrange
const sixUniqueEmails = createUniqueEmailString(6);
const sixDuplicateEmails = createIdenticalEmailString(6);
const control = new FormControl(sixUniqueEmails + sixDuplicateEmails);
const validatorFn = inputEmailLimitValidator(organization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toBeNull();
});
it("should return null if input is null", () => {
// Arrange
const control: AbstractControl = new FormControl(null);
const validatorFn = inputEmailLimitValidator(organization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toBeNull();
});
it("should return null if input is empty", () => {
// Arrange
const control: AbstractControl = new FormControl("");
const validatorFn = inputEmailLimitValidator(organization, getErrorMessage);
// Act
const result = validatorFn(control);
// Assert
expect(result).toBeNull();
});
});
});

View File

@@ -0,0 +1,40 @@
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
function getUniqueInputEmails(control: AbstractControl): string[] {
const emails: string[] = control.value
.split(",")
.filter((email: string) => email && email.trim() !== "");
const uniqueEmails: string[] = Array.from(new Set(emails));
return uniqueEmails;
}
/**
* Ensure the number of unique emails in an input does not exceed the allowed maximum.
* @param organization An object representing the organization
* @param getErrorMessage A callback function that generates the error message. It takes the `maxEmailsCount` as a parameter.
* @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null`
*/
export function inputEmailLimitValidator(
organization: Organization,
getErrorMessage: (maxEmailsCount: number) => string,
): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (!control.value?.trim()) {
return null;
}
const maxEmailsCount = organization.productTierType === ProductTierType.TeamsStarter ? 10 : 20;
const uniqueEmails = getUniqueInputEmails(control);
if (uniqueEmails.length <= maxEmailsCount) {
return null;
}
return { tooManyEmails: { message: getErrorMessage(maxEmailsCount) } };
};
}

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
@@ -463,9 +465,51 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
firstValueFrom(simpleDialog.closed).then(this.handleDialogClose.bind(this));
}
async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) {
private async handleInviteDialog() {
const dialog = openUserAddEditDialog(this.dialogService, {
data: {
name: null,
organizationId: this.organization.id,
organizationUserId: null,
allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [],
usesKeyConnector: null,
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: MemberDialogTab.Role,
numConfirmedMembers: this.dataSource.confirmedUserCount,
managedByOrganization: null,
},
});
const result = await lastValueFrom(dialog.closed);
if (result === MemberDialogResult.Saved) {
await this.load();
}
}
private async handleSeatLimitForFixedTiers() {
if (!this.organization.canEditSubscription) {
await this.showSeatLimitReachedDialog();
return;
}
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: this.organization.id,
subscription: null,
productTierType: this.organization.productTierType,
},
});
const result = await lastValueFrom(reference.closed);
if (result === ChangePlanDialogResultType.Submitted) {
await this.load();
}
}
async invite() {
if (
!user &&
this.organization.hasReseller &&
this.organization.seats === this.dataSource.occupiedSeatCount
) {
@@ -474,46 +518,36 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
title: this.i18nService.t("seatLimitReached"),
message: this.i18nService.t("contactYourProvider"),
});
return;
}
// Invite User: Add Flow
// Click on user email: Edit Flow
// User attempting to invite new users in a free org with max users
if (
!user &&
this.dataSource.occupiedSeatCount === this.organization.seats &&
isFixedSeatPlan(this.organization.productTierType)
) {
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: this.organization.id,
subscription: null,
productTierType: this.organization.productTierType,
},
});
await this.handleSeatLimitForFixedTiers();
const result = await lastValueFrom(reference.closed);
if (result === ChangePlanDialogResultType.Submitted) {
await this.load();
}
return;
}
await this.handleInviteDialog();
}
async edit(user: OrganizationUserView, initialTab: MemberDialogTab = MemberDialogTab.Role) {
const dialog = openUserAddEditDialog(this.dialogService, {
data: {
kind: "InviteMemberDialogParams",
name: this.userNamePipe.transform(user),
organizationId: this.organization.id,
organizationUserId: user != null ? user.id : null,
organizationUserId: user.id,
occupiedSeatCount: this.dataSource.occupiedSeatCount,
allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [],
usesKeyConnector: user?.usesKeyConnector,
usesKeyConnector: user.usesKeyConnector,
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: initialTab,
managedByOrganization: user?.managedByOrganization,
numConfirmedMembers: this.dataSource.confirmedUserCount,
managedByOrganization: user.managedByOrganization,
},
});
@@ -525,9 +559,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
case MemberDialogResult.Saved:
case MemberDialogResult.Revoked:
case MemberDialogResult.Restored:
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
await this.load();
break;
}
}
@@ -742,7 +774,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
key: "deleteOrganizationUser",
placeholders: [this.userNamePipe.transform(user)],
},
content: { key: "deleteOrganizationUserWarning" },
content: {
key: "deleteOrganizationUserWarningDesc",
placeholders: [this.userNamePipe.transform(user)],
},
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock, MockProxy } from "jest-mock-extended";
import {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import {

View File

@@ -1,7 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { authGuard } from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
canAccessOrgAdmin,
canAccessGroupsTab,
@@ -11,6 +14,7 @@ import {
canAccessSettingsTab,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
@@ -18,6 +22,8 @@ import { OrganizationLayoutComponent } from "../../admin-console/organizations/l
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
import { VaultModule } from "../../vault/org-vault/vault.module";
import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard";
import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component";
import { GroupsComponent } from "./manage/groups.component";
const routes: Routes = [
@@ -36,6 +42,17 @@ const routes: Routes = [
path: "vault",
loadChildren: () => VaultModule,
},
{
path: "integrations",
canActivate: [
canAccessFeature(FeatureFlag.PM14505AdminConsoleIntegrationPage),
isEnterpriseOrgGuard(false),
],
component: AdminConsoleIntegrationsComponent,
data: {
titleId: "integrations",
},
},
{
path: "settings",
loadChildren: () =>

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, Input, OnInit } from "@angular/core";
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
@@ -45,7 +47,7 @@ export abstract class BasePolicyComponent implements OnInit {
return null;
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>) {
buildRequest() {
const request = new PolicyRequest();
request.enabled = this.enabled.value;
request.type = this.policy.type;

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { UntypedFormBuilder, Validators } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { lastValueFrom } from "rxjs";
@@ -87,7 +89,6 @@ export class PoliciesComponent implements OnInit {
data: {
policy: policy,
organizationId: this.organizationId,
policiesEnabledMap: this.policiesEnabledMap,
},
});

View File

@@ -15,7 +15,13 @@
</div>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" [disabled]="saveDisabled" bitFormButton type="submit">
<button
bitButton
buttonType="primary"
[disabled]="saveDisabled$ | async"
bitFormButton
type="submit"
>
{{ "save" | i18n }}
</button>
<button bitButton buttonType="secondary" bitDialogClose type="button">

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import {
AfterViewInit,
@@ -8,13 +10,13 @@ import {
ViewContainerRef,
} from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Observable, map } from "rxjs";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { BasePolicy, BasePolicyComponent } from "../policies";
@@ -24,8 +26,6 @@ export type PolicyEditDialogData = {
policy: BasePolicy;
/** Returns a unique organization id */
organizationId: string;
/** A map indicating whether each policy type is enabled or disabled. */
policiesEnabledMap: Map<PolicyType, boolean>;
};
export enum PolicyEditDialogResult {
@@ -42,7 +42,7 @@ export class PolicyEditComponent implements AfterViewInit {
policyType = PolicyType;
loading = true;
enabled = false;
saveDisabled = false;
saveDisabled$: Observable<boolean>;
defaultTypes: any[];
policyComponent: BasePolicyComponent;
@@ -54,7 +54,6 @@ export class PolicyEditComponent implements AfterViewInit {
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
private policyApiService: PolicyApiServiceAbstraction,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cdr: ChangeDetectorRef,
private formBuilder: FormBuilder,
private dialogRef: DialogRef<PolicyEditDialogResult>,
@@ -73,7 +72,9 @@ export class PolicyEditComponent implements AfterViewInit {
this.policyComponent.policy = this.data.policy;
this.policyComponent.policyResponse = this.policyResponse;
this.saveDisabled = !this.policyResponse.canToggleState;
this.saveDisabled$ = this.policyComponent.data.statusChanges.pipe(
map((status) => status !== "VALID" || !this.policyResponse.canToggleState),
);
this.cdr.detectChanges();
}
@@ -96,7 +97,7 @@ export class PolicyEditComponent implements AfterViewInit {
submit = async () => {
let request: PolicyRequest;
try {
request = await this.policyComponent.buildRequest(this.data.policiesEnabledMap);
request = await this.policyComponent.buildRequest();
} catch (e) {
this.toastService.showToast({ variant: "error", title: null, message: e.message });
return;

View File

@@ -2,10 +2,6 @@ import { Component } from "@angular/core";
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -24,25 +20,4 @@ export class RequireSsoPolicy extends BasePolicy {
selector: "policy-require-sso",
templateUrl: "require-sso.component.html",
})
export class RequireSsoPolicyComponent extends BasePolicyComponent {
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {
super();
}
async buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
if (await this.configService.getFeatureFlag(FeatureFlag.Pm13322AddPolicyDefinitions)) {
// We are now relying on server-side validation only
return super.buildRequest(policiesEnabledMap);
}
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && !singleOrgEnabled) {
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
}
return super.buildRequest(policiesEnabledMap);
}
}
export class RequireSsoPolicyComponent extends BasePolicyComponent {}

View File

@@ -2,10 +2,8 @@ import { Component, OnInit } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -21,10 +19,7 @@ export class SingleOrgPolicy extends BasePolicy {
templateUrl: "single-org.component.html",
})
export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit {
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {
constructor(private configService: ConfigService) {
super();
}
@@ -44,39 +39,4 @@ export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnI
this.enabled.disable();
}
}
async buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
if (await this.configService.getFeatureFlag(FeatureFlag.Pm13322AddPolicyDefinitions)) {
// We are now relying on server-side validation only
return super.buildRequest(policiesEnabledMap);
}
if (!this.enabled.value) {
if (policiesEnabledMap.get(PolicyType.RequireSso) ?? false) {
throw new Error(
this.i18nService.t("disableRequiredError", this.i18nService.t("requireSso")),
);
}
if (policiesEnabledMap.get(PolicyType.MaximumVaultTimeout) ?? false) {
throw new Error(
this.i18nService.t(
"disableRequiredError",
this.i18nService.t("maximumVaultTimeoutLabel"),
),
);
}
if (
(await firstValueFrom(this.accountDeprovisioningEnabled$)) &&
!this.policyResponse.canToggleState
) {
throw new Error(
this.i18nService.t("disableRequiredError", this.i18nService.t("singleOrg")),
);
}
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { filter, map, Observable, startWith, concatMap } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject, NgModule } from "@angular/core";
import { CanMatchFn, RouterModule, Routes } from "@angular/router";
import { map } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef } from "@angular/cdk/dialog";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Meta, StoryObj } from "@storybook/angular";
import { AccessSelectorComponent, PermissionMode } from "./access-selector.component";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { FormBuilder, FormControl, FormGroup } from "@angular/forms";
import { Meta, StoryObj } from "@storybook/angular";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, forwardRef, Input, OnDestroy, OnInit } from "@angular/core";
import {
ControlValueAccessor,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
CollectionAccessSelectionView,
OrganizationUserUserDetailsResponse,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, inject } from "@angular/core";
import { Params } from "@angular/router";
import { firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,

View File

@@ -1,3 +1,5 @@
// 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 { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
LoginDecryptionOptionsService,
DefaultLoginDecryptionOptionsService,

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
/**

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { WebauthnLoginAttestationResponseRequest } from "./webauthn-login-attestation-response.request";
/**

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { RotateableKeySet } from "@bitwarden/auth/common";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { randomBytes } from "crypto";
import { mock, MockProxy } from "jest-mock-extended";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable, Optional } from "@angular/core";
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
import { KdfType } from "@bitwarden/key-management";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export class EmergencyAccessAcceptRequest {
token: string;
}

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export class EmergencyAccessConfirmRequest {
key: string;
}

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EmergencyAccessType } from "../enums/emergency-access-type";
export class EmergencyAccessInviteRequest {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
export class EmergencyAccessPasswordRequest {
newMasterPasswordHash: string;
key: string;

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { EmergencyAccessType } from "../enums/emergency-access-type";
export class EmergencyAccessUpdateRequest {

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { MockProxy } from "jest-mock-extended";
import mock from "jest-mock-extended/lib/Mock";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";

View File

@@ -1,3 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";

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