1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 07:13:32 +00:00

[EC-19] Update Organization Settings Page (#3251)

* [EC-19] Refactor existing organization settings components to its own module

* [EC-19] Move SSO page to settings tab

* [EC-19] Move Policies page to Settings tab

Refactor Policy components into its own module

* [EC-19] Move ImageSubscriptionHiddenComponent

* [EC-19] Lazy load org settings module

* [EC-19] Add SSO Id to SSO config view

* [EC-19] Remove SSO identfier from org info page

* [EC-19] Update org settings/policies to follow ADR-0011

* [EC-19] Update two-step login setup description

* [EC-19] Revert nested policy components folder

* [EC-19] Revert nested org setting components folder

* [EC-19] Remove left over image component

* [EC-19] Prettier

* [EC-19] Fix missing i18n

* [EC-19] Update SSO form to use CL

* [EC-19] Remove unused SSO input components

* [EC-19] Fix bad SSO locale identifier

* [EC-19] Fix import order linting

* [EC-19] Add explicit whitespace check for launch click directive

* [EC-19] Add restricted import paths to eslint config

* [EC-19] Tag deprecated field with Jira issue to cleanup in future release

* [EC-19] Remove out of date comment

* [EC-19] Move policy components to policies module

* [EC-19] Remove dityRequired validator

* [EC-19] Use explicit type for SSO config form

* [EC-19] Fix rxjs linter errors

* [EC-19] Fix RxJS eslint comments in org settings component

* [EC-19] Use explicit ControlsOf<T> helper for nested SSO form groups.

* [EC-19] Attribute source of ControlsOf<T> helper

* [EC-19] Fix missing settings side nav links

* [EC-19] Fix member/user language for policy modals
This commit is contained in:
Shane Melton
2022-09-27 14:40:04 -07:00
committed by GitHub
parent 38204bf4dd
commit 75965b080f
48 changed files with 750 additions and 641 deletions

View File

@@ -6,7 +6,13 @@
"no-restricted-imports": [
"error",
{
"patterns": ["**/app/core/*", "**/reports/*", "**/app/shared/*"]
"patterns": [
"**/app/core/*",
"**/reports/*",
"**/app/shared/*",
"**/organizations/settings/*",
"**/organizations/policies/*"
]
}
]
}

View File

@@ -27,15 +27,17 @@ import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.ab
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { PolicyListService, RouterService } from "./core";
import { DisableSendPolicy } from "./organizations/policies/disable-send.component";
import { MasterPasswordPolicy } from "./organizations/policies/master-password.component";
import { PasswordGeneratorPolicy } from "./organizations/policies/password-generator.component";
import { PersonalOwnershipPolicy } from "./organizations/policies/personal-ownership.component";
import { RequireSsoPolicy } from "./organizations/policies/require-sso.component";
import { ResetPasswordPolicy } from "./organizations/policies/reset-password.component";
import { SendOptionsPolicy } from "./organizations/policies/send-options.component";
import { SingleOrgPolicy } from "./organizations/policies/single-org.component";
import { TwoFactorAuthenticationPolicy } from "./organizations/policies/two-factor-authentication.component";
import {
DisableSendPolicy,
MasterPasswordPolicy,
PasswordGeneratorPolicy,
PersonalOwnershipPolicy,
RequireSsoPolicy,
ResetPasswordPolicy,
SendOptionsPolicy,
SingleOrgPolicy,
TwoFactorAuthenticationPolicy,
} from "./organizations/policies";
const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes

View File

@@ -1,4 +1,4 @@
import { BasePolicy } from "../organizations/policies/base-policy.component";
import { BasePolicy } from "../organizations/policies";
export class PolicyListService {
private policies: BasePolicy[] = [];

View File

@@ -4,6 +4,7 @@ import { LooseComponentsModule } from "../../shared/loose-components.module";
import { SharedModule } from "../../shared/shared.module";
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
import { ImageSubscriptionHiddenComponent } from "./image-subscription-hidden.component";
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
import { OrganizationBillingRoutingModule } from "./organization-billing-routing.module";
import { OrganizationBillingTabComponent } from "./organization-billing-tab.component";
@@ -14,6 +15,7 @@ import { OrganizationSubscriptionComponent } from "./organization-subscription.c
declarations: [
BillingSyncApiKeyComponent,
OrganizationBillingTabComponent,
ImageSubscriptionHiddenComponent,
OrganizationSubscriptionComponent,
OrgBillingHistoryViewComponent,
],

View File

@@ -11,11 +11,7 @@ import {
canAccessGroupsTab,
canAccessMembersTab,
canAccessOrgAdmin,
canAccessSettingsTab,
} from "./navigation-permissions";
import { AccountComponent } from "./settings/account.component";
import { SettingsComponent } from "./settings/settings.component";
import { TwoFactorSetupComponent } from "./settings/two-factor-setup.component";
import { VaultModule } from "./vault/vault.module";
const routes: Routes = [
@@ -34,18 +30,7 @@ const routes: Routes = [
},
{
path: "settings",
component: SettingsComponent,
canActivate: [OrganizationPermissionsGuard],
data: { organizationPermissions: canAccessSettingsTab },
children: [
{ path: "", pathMatch: "full", redirectTo: "account" },
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
{
path: "two-factor",
component: TwoFactorSetupComponent,
data: { titleId: "twoStepLogin" },
},
],
loadChildren: () => import("./settings").then((m) => m.OrganizationSettingsModule),
},
{
path: "members",

View File

@@ -0,0 +1,12 @@
export * from "./policies.module";
export { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export { DisableSendPolicy } from "./disable-send.component";
export { MasterPasswordPolicy } from "./master-password.component";
export { PasswordGeneratorPolicy } from "./password-generator.component";
export { PersonalOwnershipPolicy } from "./personal-ownership.component";
export { RequireSsoPolicy } from "./require-sso.component";
export { ResetPasswordPolicy } from "./reset-password.component";
export { SendOptionsPolicy } from "./send-options.component";
export { SingleOrgPolicy } from "./single-org.component";
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
export { PoliciesComponent } from "./policies.component";

View File

@@ -10,7 +10,7 @@ import { Organization } from "@bitwarden/common/models/domain/organization";
import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse";
import { PolicyListService } from "../../core";
import { BasePolicy } from "../policies/base-policy.component";
import { BasePolicy } from "../policies";
import { PolicyEditComponent } from "./policy-edit.component";

View File

@@ -0,0 +1,46 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule, SharedModule } from "../../shared";
import { DisableSendPolicyComponent } from "./disable-send.component";
import { MasterPasswordPolicyComponent } from "./master-password.component";
import { PasswordGeneratorPolicyComponent } from "./password-generator.component";
import { PersonalOwnershipPolicyComponent } from "./personal-ownership.component";
import { PoliciesComponent } from "./policies.component";
import { PolicyEditComponent } from "./policy-edit.component";
import { RequireSsoPolicyComponent } from "./require-sso.component";
import { ResetPasswordPolicyComponent } from "./reset-password.component";
import { SendOptionsPolicyComponent } from "./send-options.component";
import { SingleOrgPolicyComponent } from "./single-org.component";
import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authentication.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule],
declarations: [
DisableSendPolicyComponent,
MasterPasswordPolicyComponent,
PasswordGeneratorPolicyComponent,
PersonalOwnershipPolicyComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
SendOptionsPolicyComponent,
SingleOrgPolicyComponent,
TwoFactorAuthenticationPolicyComponent,
PoliciesComponent,
PolicyEditComponent,
],
exports: [
DisableSendPolicyComponent,
MasterPasswordPolicyComponent,
PasswordGeneratorPolicyComponent,
PersonalOwnershipPolicyComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
SendOptionsPolicyComponent,
SingleOrgPolicyComponent,
TwoFactorAuthenticationPolicyComponent,
PoliciesComponent,
PolicyEditComponent,
],
})
export class PoliciesModule {}

View File

@@ -17,7 +17,7 @@ import { PolicyType } from "@bitwarden/common/enums/policyType";
import { PolicyRequest } from "@bitwarden/common/models/request/policyRequest";
import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse";
import { BasePolicy, BasePolicyComponent } from "../policies/base-policy.component";
import { BasePolicy, BasePolicyComponent } from "../policies";
@Component({
selector: "app-policy-edit",

View File

@@ -51,16 +51,6 @@
[disabled]="selfHosted || !canManageBilling"
/>
</div>
<div class="form-group">
<label for="identifier">{{ "identifier" | i18n }}</label>
<input
id="identifier"
class="form-control"
type="text"
name="Identifier"
[(ngModel)]="org.identifier"
/>
</div>
</div>
<div class="col-6">
<app-avatar data="{{ org.name }}" dynamic="true" size="75" fontSize="35"></app-avatar>

View File

@@ -0,0 +1,2 @@
export * from "./organization-settings.module";
export { DeleteOrganizationComponent } from "./delete-organization.component";

View File

@@ -0,0 +1,52 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
import { canAccessSettingsTab } from "../navigation-permissions";
import { PoliciesComponent } from "../policies";
import { AccountComponent } from "./account.component";
import { SettingsComponent } from "./settings.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
const routes: Routes = [
{
path: "",
component: SettingsComponent,
canActivate: [OrganizationPermissionsGuard],
data: { organizationPermissions: canAccessSettingsTab },
children: [
{ path: "", pathMatch: "full", redirectTo: "account" },
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
{
path: "two-factor",
component: TwoFactorSetupComponent,
data: { titleId: "twoStepLogin" },
},
{
path: "policies",
component: PoliciesComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
organizationPermissions: (org: Organization) => org.canManagePolicies,
titleId: "policies",
},
},
{
path: "tools",
loadChildren: () =>
import("../tools/import-export/org-import-export.module").then(
(m) => m.OrganizationImportExportModule
),
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrganizationSettingsRoutingModule {}

View File

@@ -0,0 +1,27 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule, SharedModule } from "../../shared";
import { PoliciesModule } from "../policies";
import { AccountComponent } from "./account.component";
import { AdjustSubscription } from "./adjust-subscription.component";
import { ChangePlanComponent } from "./change-plan.component";
import { DeleteOrganizationComponent } from "./delete-organization.component";
import { DownloadLicenseComponent } from "./download-license.component";
import { OrganizationSettingsRoutingModule } from "./organization-settings-routing.module";
import { SettingsComponent } from "./settings.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule],
declarations: [
SettingsComponent,
AccountComponent,
AdjustSubscription,
ChangePlanComponent,
DeleteOrganizationComponent,
DownloadLicenseComponent,
TwoFactorSetupComponent,
],
})
export class OrganizationSettingsModule {}

View File

@@ -7,14 +7,54 @@
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{ "organizationInfo" | i18n }}
</a>
<a
routerLink="policies"
class="list-group-item"
routerLinkActive="active"
*ngIf="organization?.canManagePolicies"
>
{{ "policies" | i18n }}
</a>
<a
routerLink="two-factor"
class="list-group-item"
routerLinkActive="active"
*ngIf="access2fa"
*ngIf="organization?.use2fa"
>
{{ "twoStepLogin" | i18n }}
</a>
<a
routerLink="tools/import"
class="list-group-item"
routerLinkActive="active"
*ngIf="organization?.canAccessImportExport"
>
{{ "importData" | i18n }}
</a>
<a
routerLink="tools/export"
class="list-group-item"
routerLinkActive="active"
*ngIf="organization?.canAccessImportExport"
>
{{ "exportVault" | i18n }}
</a>
<a
routerLink="sso"
class="list-group-item"
routerLinkActive="active"
*ngIf="organization?.canManageSso"
>
{{ "singleSignOn" | i18n }}
</a>
<a
routerLink="scim"
class="list-group-item"
routerLinkActive="active"
*ngIf="organization?.canManageScim"
>
{{ "scim" | i18n }}
</a>
</div>
</div>
</div>

View File

@@ -1,30 +1,34 @@
import { Component } from "@angular/core";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, switchMap, takeUntil } from "rxjs";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
@Component({
selector: "app-org-settings",
templateUrl: "settings.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SettingsComponent {
access2fa = false;
showBilling: boolean;
export class SettingsComponent implements OnInit, OnDestroy {
organization: Organization;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private platformUtilsService: PlatformUtilsService
) {}
private destroy$ = new Subject<void>();
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.params.subscribe(async (params) => {
const organization = await this.organizationService.get(params.organizationId);
this.showBilling = !this.platformUtilsService.isSelfHost() && organization.canManageBilling;
this.access2fa = organization.use2fa;
});
this.route.params
.pipe(
switchMap(async (params) => await this.organizationService.get(params.organizationId)),
takeUntil(this.destroy$)
)
.subscribe((organization) => {
this.organization = organization;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -15,8 +15,8 @@ import { ProductType } from "@bitwarden/common/enums/productType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationSponsorshipRedeemRequest } from "@bitwarden/common/models/request/organization/organizationSponsorshipRedeemRequest";
import { DeleteOrganizationComponent } from "../../organizations/settings";
import { OrganizationPlansComponent } from "../../settings/organization-plans.component";
import { DeleteOrganizationComponent } from "../settings/delete-organization.component";
@Component({
selector: "families-for-enterprise-setup",

View File

@@ -1,8 +1,17 @@
<div class="tabbed-header">
<h1>{{ "twoStepLogin" | i18n }}</h1>
<h1 *ngIf="!organizationId">{{ "twoStepLogin" | i18n }}</h1>
<h1 *ngIf="organizationId">{{ "twoStepLoginEnforcement" | i18n }}</h1>
</div>
<p *ngIf="!organizationId">{{ "twoStepLoginDesc" | i18n }}</p>
<p *ngIf="organizationId">{{ "twoStepLoginOrganizationDesc" | i18n }}</p>
<ng-container *ngIf="organizationId">
<p>
{{ "twoStepLoginOrganizationDescStart" | i18n }}
<a routerLink="../policies">{{ "twoStepLoginPolicy" | i18n }}.</a>
<br />
{{ "twoStepLoginOrganizationDuoDesc" | i18n }}
</p>
<p>{{ "twoStepLoginOrganizationSsoDesc" | i18n }}</p>
</ng-container>
<bit-callout type="warning" *ngIf="!organizationId">
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
<button bitButton buttonType="secondary" (click)="recoveryCode()">
@@ -55,7 +64,13 @@
</div>
</li>
</ul>
<form *ngIf="!loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<form
*ngIf="!loading && !organizationId"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="row">
<div class="col-12">
<h2 class="mt-5 spaced-header">

View File

@@ -42,29 +42,10 @@ import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizati
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component";
import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component";
import { PoliciesComponent as OrgPoliciesComponent } from "../organizations/manage/policies.component";
import { PolicyEditComponent as OrgPolicyEditComponent } from "../organizations/manage/policy-edit.component";
import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component";
import { UserAddEditComponent as OrgUserAddEditComponent } from "../organizations/manage/user-add-edit.component";
import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
import { UserGroupsComponent as OrgUserGroupsComponent } from "../organizations/manage/user-groups.component";
import { DisableSendPolicyComponent } from "../organizations/policies/disable-send.component";
import { MasterPasswordPolicyComponent } from "../organizations/policies/master-password.component";
import { PasswordGeneratorPolicyComponent } from "../organizations/policies/password-generator.component";
import { PersonalOwnershipPolicyComponent } from "../organizations/policies/personal-ownership.component";
import { RequireSsoPolicyComponent } from "../organizations/policies/require-sso.component";
import { ResetPasswordPolicyComponent } from "../organizations/policies/reset-password.component";
import { SendOptionsPolicyComponent } from "../organizations/policies/send-options.component";
import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.component";
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
import { ImageSubscriptionHiddenComponent as OrgSubscriptionHiddenComponent } from "../organizations/settings/image-subscription-hidden.component";
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
@@ -172,7 +153,6 @@ import { SharedModule } from ".";
AddEditCustomFieldsComponent,
AdjustPaymentComponent,
AdjustStorageComponent,
AdjustSubscription,
ApiKeyComponent,
AttachmentsComponent,
BillingSyncKeyComponent,
@@ -184,15 +164,11 @@ import { SharedModule } from ".";
ChangeEmailComponent,
ChangeKdfComponent,
ChangePasswordComponent,
ChangePlanComponent,
CollectionsComponent,
CreateOrganizationComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
DisableSendPolicyComponent,
DomainRulesComponent,
DownloadLicenseComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessComponent,
@@ -206,11 +182,9 @@ import { SharedModule } from ".";
FrontendLayoutComponent,
HintComponent,
LockComponent,
MasterPasswordPolicyComponent,
NavbarComponent,
NestedCheckboxComponent,
OrganizationSwitcherComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrganizationLayoutComponent,
OrganizationPlansComponent,
@@ -230,14 +204,9 @@ import { SharedModule } from ".";
OrgManageCollectionsComponent,
OrgManageComponent,
OrgPeopleComponent,
OrgPoliciesComponent,
OrgPolicyEditComponent,
OrgResetPasswordComponent,
OrgReusedPasswordsReportComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
OrgSubscriptionHiddenComponent,
OrgUnsecuredWebsitesReportComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
@@ -245,12 +214,10 @@ import { SharedModule } from ".";
OrgWeakPasswordsReportComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordGeneratorPolicyComponent,
PasswordRepromptComponent,
UserVerificationPromptComponent,
PaymentComponent,
PaymentMethodComponent,
PersonalOwnershipPolicyComponent,
PreferencesComponent,
PremiumBadgeComponent,
PremiumComponent,
@@ -261,25 +228,20 @@ import { SharedModule } from ".";
RecoverTwoFactorComponent,
RegisterComponent,
RemovePasswordComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
SecurityComponent,
SecurityKeysComponent,
SendAddEditComponent,
SendComponent,
SendEffluxDatesComponent,
SendOptionsPolicyComponent,
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SingleOrgPolicyComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
SubscriptionComponent,
TaxInfoComponent,
ToolsComponent,
TwoFactorAuthenticationPolicyComponent,
TwoFactorAuthenticatorComponent,
TwoFactorComponent,
TwoFactorDuoComponent,
@@ -316,7 +278,6 @@ import { SharedModule } from ".";
AddEditCustomFieldsComponent,
AdjustPaymentComponent,
AdjustStorageComponent,
AdjustSubscription,
ApiKeyComponent,
AttachmentsComponent,
BulkActionsComponent,
@@ -327,15 +288,11 @@ import { SharedModule } from ".";
ChangeEmailComponent,
ChangeKdfComponent,
ChangePasswordComponent,
ChangePlanComponent,
CollectionsComponent,
CreateOrganizationComponent,
DeauthorizeSessionsComponent,
DeleteAccountComponent,
DeleteOrganizationComponent,
DisableSendPolicyComponent,
DomainRulesComponent,
DownloadLicenseComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessComponent,
@@ -349,11 +306,9 @@ import { SharedModule } from ".";
FrontendLayoutComponent,
HintComponent,
LockComponent,
MasterPasswordPolicyComponent,
NavbarComponent,
NestedCheckboxComponent,
OrganizationSwitcherComponent,
OrgAccountComponent,
OrgAddEditComponent,
OrganizationLayoutComponent,
OrganizationPlansComponent,
@@ -373,13 +328,9 @@ import { SharedModule } from ".";
OrgManageCollectionsComponent,
OrgManageComponent,
OrgPeopleComponent,
OrgPoliciesComponent,
OrgPolicyEditComponent,
OrgResetPasswordComponent,
OrgReusedPasswordsReportComponent,
OrgSettingComponent,
OrgToolsComponent,
OrgTwoFactorSetupComponent,
OrgUnsecuredWebsitesReportComponent,
OrgUserAddEditComponent,
OrgUserConfirmComponent,
@@ -387,11 +338,9 @@ import { SharedModule } from ".";
OrgWeakPasswordsReportComponent,
GeneratorComponent,
PasswordGeneratorHistoryComponent,
PasswordGeneratorPolicyComponent,
PasswordRepromptComponent,
PaymentComponent,
PaymentMethodComponent,
PersonalOwnershipPolicyComponent,
PreferencesComponent,
PremiumBadgeComponent,
PremiumComponent,
@@ -402,25 +351,20 @@ import { SharedModule } from ".";
RecoverTwoFactorComponent,
RegisterComponent,
RemovePasswordComponent,
RequireSsoPolicyComponent,
ResetPasswordPolicyComponent,
SecurityComponent,
SecurityKeysComponent,
SendAddEditComponent,
SendComponent,
SendEffluxDatesComponent,
SendOptionsPolicyComponent,
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SingleOrgPolicyComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
SubscriptionComponent,
TaxInfoComponent,
ToolsComponent,
TwoFactorAuthenticationPolicyComponent,
TwoFactorAuthenticatorComponent,
TwoFactorComponent,
TwoFactorDuoComponent,

View File

@@ -1288,11 +1288,24 @@
"twoStepLogin": {
"message": "Two-step login"
},
"twoStepLoginEnforcement": {
"message": "Two-step Login Enforcement"
},
"twoStepLoginDesc": {
"message": "Secure your account by requiring an additional step when logging in."
},
"twoStepLoginOrganizationDesc": {
"message": "Require two-step login for your organization's users by configuring providers at the organization level."
"twoStepLoginOrganizationDescStart": {
"message": "Enforce Bitwarden Two-step Login options for members by using the ",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'"
},
"twoStepLoginPolicy": {
"message": "Two-step Login Policy"
},
"twoStepLoginOrganizationDuoDesc": {
"message": "To enforce Two-step Login through Duo, use the options below."
},
"twoStepLoginOrganizationSsoDesc": {
"message": "If you have setup SSO or plan to, Two-step Login may already be enforced through your Identity Provider."
},
"twoStepLoginRecoveryWarning": {
"message": "Enabling two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (ex. you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place."
@@ -3687,6 +3700,12 @@
"ssoIdentifierRequired": {
"message": "Organization Identifier is required."
},
"ssoIdentifier": {
"message": "SSO Identifier"
},
"ssoIdentifierHint": {
"message": "Provide this ID to your members to login with SSO."
},
"unlinkSso": {
"message": "Unlink SSO"
},
@@ -4032,7 +4051,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"disableSendExemption": {
"message": "Organization users that can manage the organization's policies are exempt from this policy's enforcement."
"message": "Organization members that can manage the organization's policies are exempt from this policy's enforcement."
},
"sendDisabled": {
"message": "Send disabled",
@@ -4051,7 +4070,7 @@
"description": "'Sends' is a plural noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendOptionsExemption": {
"message": "Organization users that can manage the organization's policies are exempt from this policy's enforcement."
"message": "Organization members that can manage the organization's policies are exempt from this policy's enforcement."
},
"disableHideEmail": {
"message": "Always show members email address with recipients when creating or editing a send.",
@@ -4362,19 +4381,19 @@
"message": "Allow admins to reset master passwords for members."
},
"resetPasswordPolicyWarning": {
"message": "Users in the organization will need to self-enroll or be auto-enrolled before administrators can reset their master password."
"message": "Members in the organization will need to self-enroll or be auto-enrolled before administrators can reset their master password."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic Enrollment"
},
"resetPasswordPolicyAutoEnrollDescription": {
"message": "All users will be automatically enrolled in password reset once their invite is accepted and will not be allowed to withdraw."
"message": "All members will be automatically enrolled in password reset once their invite is accepted and will not be allowed to withdraw."
},
"resetPasswordPolicyAutoEnrollWarning": {
"message": "Users already in the organization will not be retroactively enrolled in password reset. They will need to self-enroll before administrators can reset their master password."
"message": "Members already in the organization will not be retroactively enrolled in password reset. They will need to self-enroll before administrators can reset their master password."
},
"resetPasswordPolicyAutoEnrollCheckbox": {
"message": "Require new users to be enrolled automatically"
"message": "Require new members to be enrolled automatically"
},
"resetPasswordAutoEnrollInviteWarning": {
"message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password."
@@ -5377,6 +5396,15 @@
}
}
},
"inputMaxLength": {
"message": "Input must not exceed $COUNT$ characters in length.",
"placeholders": {
"count": {
"content": "$1",
"example": "20"
}
}
},
"fieldsNeedAttention": {
"message": "$COUNT$ field(s) above need your attention.",
"placeholders": {

View File

@@ -1,8 +1,6 @@
import { Directive, Input, OnInit, Self } from "@angular/core";
import { ControlValueAccessor, UntypedFormControl, NgControl, Validators } from "@angular/forms";
import { dirtyRequired } from "@bitwarden/angular/validators/dirty.validator";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Directive()
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
@@ -15,10 +13,7 @@ export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
}
get isRequired() {
return (
this.controlDir.control.hasValidator(Validators.required) ||
this.controlDir.control.hasValidator(dirtyRequired)
);
return this.controlDir.control.hasValidator(Validators.required);
}
@Input() label: string;

View File

@@ -1,26 +0,0 @@
<div class="form-group">
<label>{{ label }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="controlValue" />
<div class="input-group-append" *ngIf="showLaunch">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(controlValue)"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
</div>
<div class="input-group-append" *ngIf="showCopy">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(controlValue)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -1,25 +0,0 @@
import { Component, Input } from "@angular/core";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text-readonly",
templateUrl: "input-text-readonly.component.html",
})
export class InputTextReadOnlyComponent {
@Input() controlValue: string;
@Input() label: string;
@Input() showCopy = true;
@Input() showLaunch = false;
constructor(private platformUtilsService: PlatformUtilsService) {}
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
}

View File

@@ -1,33 +0,0 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<input
[formControl]="internalControl"
class="form-control"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[attr.aria-invalid]="controlDir.control.invalid"
(blur)="onBlurInternal()"
/>
<div *ngIf="showDescribedBy" [attr.id]="describedById">
<small
*ngIf="helperText != null && !controlDir.control.hasError(helperTextSameAsError)"
class="form-text text-muted"
>
{{ helperText }}
</small>
<small class="error-inline" *ngIf="controlDir.control.hasError('required')" role="alert">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
controlDir.control.hasError(helperTextSameAsError)
? helperText
: ("fieldRequiredError" | i18n: label)
}}
</small>
</div>
</div>

View File

@@ -1,48 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text[label][controlId]",
templateUrl: "input-text.component.html",
})
export class InputTextComponent extends BaseCvaComponent implements OnInit {
@Input() helperTextSameAsError: string;
@Input() requiredErrorMessage: string;
@Input() stripSpaces = false;
transformValue: (value: string) => string = null;
ngOnInit() {
super.ngOnInit();
if (this.stripSpaces) {
this.transformValue = this.doStripSpaces;
}
}
writeValue(value: string) {
this.internalControl.setValue(value == null ? "" : value);
}
protected onValueChangesInternal: any = (value: string) => {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
this.onChange(newValue);
};
protected onValueChangeInternal(value: string) {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
}
private doStripSpaces(value: string) {
return value.replace(/ /g, "");
}
}

View File

@@ -1,19 +0,0 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<select
class="form-control"
[attr.id]="controlId"
[attr.aria-invalid]="controlDir.control.invalid"
[formControl]="internalControl"
(blur)="onBlurInternal()"
>
<option *ngFor="let o of selectOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
{{ o.name }}
</option>
</select>
</div>

View File

@@ -1,14 +0,0 @@
import { Component, Input } from "@angular/core";
import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-select",
templateUrl: "select.component.html",
})
export class SelectComponent extends BaseCvaComponent {
@Input() selectOptions: SelectOptions[];
}

View File

@@ -35,6 +35,14 @@
[helperText]="'allowSsoDesc' | i18n"
></app-input-checkbox>
<bit-form-field>
<bit-label>{{ "ssoIdentifier" | i18n }}</bit-label>
<input bitInput type="text" [formControl]="ssoIdentifier" />
<bit-hint>{{ "ssoIdentifierHint" | i18n }}</bit-hint>
</bit-form-field>
<hr />
<div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="form-check form-check-block">
@@ -80,66 +88,55 @@
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group">
<label for="keyConnectorUrl">
{{ "keyConnectorUrl" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<div class="input-group">
<input
class="form-control"
formControlName="keyConnectorUrl"
id="keyConnectorUrl"
aria-describedby="keyConnectorUrlDesc"
(change)="haveTestedKeyConnector = false"
appInputStripSpaces
appA11yInvalid
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="validateKeyConnectorUrl()"
[disabled]="!enableTestKeyConnector"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
</div>
</div>
<div *ngIf="haveTestedKeyConnector" id="keyConnectorUrlDesc" aria-live="polite">
<small
class="error-inline"
*ngIf="keyConnectorUrl.hasError('invalidUrl'); else keyConnectorSuccess"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "keyConnectorTestFail" | i18n }}
<bit-form-field>
<bit-label>{{ "keyConnectorUrl" | i18n }}</bit-label>
<input
bitInput
type="text"
required
formControlName="keyConnectorUrl"
appInputStripSpaces
(input)="haveTestedKeyConnector = false"
/>
<button
bitSuffix
bitButton
[disabled]="!enableTestKeyConnector"
type="button"
(click)="validateKeyConnectorUrl()"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
<bit-hint
aria-live="polite"
*ngIf="haveTestedKeyConnector && !keyConnectorUrl.hasError('invalidUrl')"
>
<small class="text-success">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "keyConnectorTestSuccess" | i18n }}
</small>
<ng-template #keyConnectorSuccess>
<small class="text-success">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "keyConnectorTestSuccess" | i18n }}
</small>
</ng-template>
</div>
</div>
</bit-hint>
</bit-form-field>
</ng-container>
<app-select
controlId="type"
[label]="'type' | i18n"
[selectOptions]="ssoTypeOptions"
formControlName="configType"
>
</app-select>
<hr />
<bit-form-field>
<bit-label>{{ "type" | i18n }}</bit-label>
<select bitInput formControlName="configType">
<option *ngFor="let o of ssoTypeOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
{{ o.name }}
</option>
</select>
</bit-form-field>
</ng-container>
<!-- OIDC -->
@@ -150,52 +147,67 @@
<div class="config-section">
<h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2>
<app-input-text-readonly
[label]="'callbackPath' | i18n"
[controlValue]="callbackPath"
></app-input-text-readonly>
<bit-form-field>
<bit-label>{{ "callbackPath" | i18n }}</bit-label>
<input bitInput disabled [value]="callbackPath" />
<button
bitButton
bitSuffix
type="button"
[appCopyClick]="callbackPath"
[appA11yTitle]="'copyValue' | i18n"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field>
<app-input-text-readonly
[label]="'signedOutCallbackPath' | i18n"
[controlValue]="signedOutCallbackPath"
></app-input-text-readonly>
<bit-form-field>
<bit-label>{{ "signedOutCallbackPath" | i18n }}</bit-label>
<input bitInput disabled [value]="signedOutCallbackPath" />
<button
bitButton
bitSuffix
type="button"
[appCopyClick]="signedOutCallbackPath"
[appA11yTitle]="'copyValue' | i18n"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field>
<app-input-text
[label]="'authority' | i18n"
controlId="authority"
[stripSpaces]="true"
formControlName="authority"
></app-input-text>
<bit-form-field>
<bit-label>{{ "authority" | i18n }}</bit-label>
<input bitInput type="text" formControlName="authority" appInputStripSpaces />
</bit-form-field>
<app-input-text
[label]="'clientId' | i18n"
controlId="clientId"
[stripSpaces]="true"
formControlName="clientId"
></app-input-text>
<bit-form-field>
<bit-label>{{ "clientId" | i18n }}</bit-label>
<input bitInput type="text" formControlName="clientId" appInputStripSpaces />
</bit-form-field>
<app-input-text
[label]="'clientSecret' | i18n"
controlId="clientSecret"
[stripSpaces]="true"
formControlName="clientSecret"
></app-input-text>
<bit-form-field>
<bit-label>{{ "clientSecret" | i18n }}</bit-label>
<input bitInput type="text" formControlName="clientSecret" appInputStripSpaces />
</bit-form-field>
<app-input-text
[label]="'metadataAddress' | i18n"
controlId="metadataAddress"
[stripSpaces]="true"
[helperText]="'openIdAuthorityRequired' | i18n"
formControlName="metadataAddress"
></app-input-text>
<bit-form-field>
<bit-label>{{ "metadataAddress" | i18n }}</bit-label>
<input bitInput type="text" formControlName="metadataAddress" appInputStripSpaces />
<bit-hint>{{ "openIdAuthorityRequired" | i18n }}</bit-hint>
</bit-form-field>
<app-select
controlId="redirectBehavior"
[label]="'oidcRedirectBehavior' | i18n"
[selectOptions]="connectRedirectOptions"
formControlName="redirectBehavior"
>
</app-select>
<bit-form-field>
<bit-label>{{ "oidcRedirectBehavior" | i18n }}</bit-label>
<select bitInput formControlName="redirectBehavior">
<option
*ngFor="let o of connectRedirectOptions"
[ngValue]="o.value"
disabled="{{ o.disabled }}"
>
{{ o.name }}
</option>
</select>
</bit-form-field>
<app-input-checkbox
controlId="getClaimsFromUserInfoEndpoint"
@@ -231,47 +243,41 @@
</button>
</div>
<div id="customizations" [hidden]="!showOpenIdCustomizations">
<app-input-text
[label]="'additionalScopes' | i18n"
controlId="additionalScopes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalScopes"
></app-input-text>
<bit-form-field>
<bit-label>{{ "additionalScopes" | i18n }}</bit-label>
<input bitInput type="text" formControlName="additionalScopes" />
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
</bit-form-field>
<app-input-text
[label]="'additionalUserIdClaimTypes' | i18n"
controlId="additionalUserIdClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalUserIdClaimTypes"
></app-input-text>
<bit-form-field>
<bit-label>{{ "additionalUserIdClaimTypes" | i18n }}</bit-label>
<input bitInput type="text" formControlName="additionalUserIdClaimTypes" />
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
</bit-form-field>
<app-input-text
[label]="'additionalEmailClaimTypes' | i18n"
controlId="additionalEmailClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalEmailClaimTypes"
></app-input-text>
<bit-form-field>
<bit-label>{{ "additionalEmailClaimTypes" | i18n }}</bit-label>
<input bitInput type="text" formControlName="additionalEmailClaimTypes" />
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
</bit-form-field>
<app-input-text
[label]="'additionalNameClaimTypes' | i18n"
controlId="additionalNameClaimTypes"
[helperText]="'separateMultipleWithComma' | i18n"
formControlName="additionalNameClaimTypes"
></app-input-text>
<bit-form-field>
<bit-label>{{ "additionalNameClaimTypes" | i18n }}</bit-label>
<input bitInput type="text" formControlName="additionalNameClaimTypes" />
<bit-hint>{{ "separateMultipleWithComma" | i18n }}</bit-hint>
</bit-form-field>
<app-input-text
[label]="'acrValues' | i18n"
controlId="acrValues"
helperText="acr_values"
formControlName="acrValues"
></app-input-text>
<bit-form-field>
<bit-label>{{ "acrValues" | i18n }}</bit-label>
<input bitInput type="text" formControlName="acrValues" />
<bit-hint>acr_values</bit-hint>
</bit-form-field>
<app-input-text
[label]="'expectedReturnAcrValue' | i18n"
controlId="expectedReturnAcrValue"
helperText="acr_validation"
formControlName="expectedReturnAcrValue"
></app-input-text>
<bit-form-field>
<bit-label>{{ "expectedReturnAcrValue" | i18n }}</bit-label>
<input bitInput type="text" formControlName="expectedReturnAcrValue" />
<bit-hint>acr_validaton</bit-hint>
</bit-form-field>
</div>
</div>
</div>
@@ -282,53 +288,108 @@
<div class="config-section">
<h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2>
<app-input-text-readonly
[label]="'spEntityId' | i18n"
[controlValue]="spEntityId"
></app-input-text-readonly>
<bit-form-field>
<bit-label>{{ "spEntityId" | i18n }}</bit-label>
<input bitInput disabled [value]="spEntityId" />
<button
bitButton
bitSuffix
type="button"
[appCopyClick]="spEntityId"
[appA11yTitle]="'copyValue' | i18n"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field>
<app-input-text-readonly
[label]="'spMetadataUrl' | i18n"
[controlValue]="spMetadataUrl"
[showLaunch]="true"
></app-input-text-readonly>
<bit-form-field>
<bit-label>{{ "spMetadataUrl" | i18n }}</bit-label>
<input bitInput disabled [value]="spMetadataUrl" />
<button
bitButton
bitSuffix
type="button"
[appLaunchClick]="spMetadataUrl"
[appA11yTitle]="'launch' | i18n"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
<button
bitButton
bitSuffix
type="button"
[appCopyClick]="spMetadataUrl"
[appA11yTitle]="'copyValue' | i18n"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field>
<app-input-text-readonly
[label]="'spAcsUrl' | i18n"
[controlValue]="spAcsUrl"
></app-input-text-readonly>
<bit-form-field>
<bit-label>{{ "spAcsUrl" | i18n }}</bit-label>
<input bitInput disabled [value]="spAcsUrl" />
<button
bitButton
bitSuffix
type="button"
[appCopyClick]="spAcsUrl"
[appA11yTitle]="'copyValue' | i18n"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</bit-form-field>
<app-select
controlId="spNameIdFormat"
[label]="'spNameIdFormat' | i18n"
[selectOptions]="saml2NameIdFormatOptions"
formControlName="spNameIdFormat"
>
</app-select>
<bit-form-field>
<bit-label>{{ "spNameIdFormat" | i18n }}</bit-label>
<select bitInput formControlName="spNameIdFormat">
<option
*ngFor="let o of saml2NameIdFormatOptions"
[ngValue]="o.value"
disabled="{{ o.disabled }}"
>
{{ o.name }}
</option>
</select>
</bit-form-field>
<app-select
controlId="spOutboundSigningAlgorithm"
[label]="'spOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="spOutboundSigningAlgorithm"
>
</app-select>
<bit-form-field>
<bit-label>{{ "spOutboundSigningAlgorithm" | i18n }}</bit-label>
<select bitInput formControlName="spOutboundSigningAlgorithm">
<option
*ngFor="let o of samlSigningAlgorithmOptions"
[ngValue]="o.value"
disabled="{{ o.disabled }}"
>
{{ o.name }}
</option>
</select>
</bit-form-field>
<app-select
controlId="spSigningBehavior"
[label]="'spSigningBehavior' | i18n"
[selectOptions]="saml2SigningBehaviourOptions"
formControlName="spSigningBehavior"
>
</app-select>
<bit-form-field>
<bit-label>{{ "spSigningBehavior" | i18n }}</bit-label>
<select bitInput formControlName="spSigningBehavior">
<option
*ngFor="let o of saml2SigningBehaviourOptions"
[ngValue]="o.value"
disabled="{{ o.disabled }}"
>
{{ o.name }}
</option>
</select>
</bit-form-field>
<app-select
controlId="spMinIncomingSigningAlgorithm"
[label]="'spMinIncomingSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="spMinIncomingSigningAlgorithm"
>
</app-select>
<bit-form-field>
<bit-label>{{ "spMinIncomingSigningAlgorithm" | i18n }}</bit-label>
<select bitInput formControlName="spMinIncomingSigningAlgorithm">
<option
*ngFor="let o of samlSigningAlgorithmOptions"
[ngValue]="o.value"
disabled="{{ o.disabled }}"
>
{{ o.name }}
</option>
</select>
</bit-form-field>
<app-input-checkbox
controlId="spWantAssertionsSigned"
@@ -347,67 +408,62 @@
<div class="config-section">
<h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2>
<app-input-text
[label]="'idpEntityId' | i18n"
controlId="idpEntityId"
formControlName="idpEntityId"
></app-input-text>
<bit-form-field>
<bit-label>{{ "idpEntityId" | i18n }}</bit-label>
<input bitInput type="text" formControlName="idpEntityId" />
</bit-form-field>
<app-select
controlId="idpBindingType"
[label]="'idpBindingType' | i18n"
[selectOptions]="saml2BindingTypeOptions"
formControlName="idpBindingType"
>
</app-select>
<bit-form-field>
<bit-label>{{ "idpBindingType" | i18n }}</bit-label>
<select bitInput formControlName="idpBindingType">
<option
*ngFor="let o of saml2BindingTypeOptions"
[ngValue]="o.value"
disabled="{{ o.disabled }}"
>
{{ o.name }}
</option>
</select>
</bit-form-field>
<app-input-text
[label]="'idpSingleSignOnServiceUrl' | i18n"
controlId="idpSingleSignOnServiceUrl"
[helperText]="'idpSingleSignOnServiceUrlRequired' | i18n"
[stripSpaces]="true"
formControlName="idpSingleSignOnServiceUrl"
></app-input-text>
<bit-form-field>
<bit-label>{{ "idpSingleSignOnServiceUrl" | i18n }}</bit-label>
<input
bitInput
type="text"
formControlName="idpSingleSignOnServiceUrl"
appInputStripSpaces
/>
<bit-hint>{{ "idpSingleSignOnServiceUrlRequired" | i18n }}</bit-hint>
</bit-form-field>
<app-input-text
[label]="'idpSingleLogoutServiceUrl' | i18n"
controlId="idpSingleLogoutServiceUrl"
[stripSpaces]="true"
formControlName="idpSingleLogoutServiceUrl"
></app-input-text>
<bit-form-field>
<bit-label>{{ "idpSingleLogoutServiceUrl" | i18n }}</bit-label>
<input
bitInput
type="text"
formControlName="idpSingleLogoutServiceUrl"
appInputStripSpaces
/>
</bit-form-field>
<div class="form-group">
<label for="idpX509PublicCert">
{{ "idpX509PublicCert" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<textarea
formControlName="idpX509PublicCert"
class="form-control form-control-sm text-monospace"
rows="6"
id="idpX509PublicCert"
appA11yInvalid
aria-describedby="idpX509PublicCertDesc"
></textarea>
<small
id="idpX509PublicCertDesc"
class="error-inline"
role="alert"
*ngIf="samlForm.get('idpX509PublicCert').hasError('required')"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }}
</small>
</div>
<bit-form-field>
<bit-label>{{ "idpX509PublicCert" | i18n }}</bit-label>
<textarea bitInput rows="6" formControlName="idpX509PublicCert"></textarea>
</bit-form-field>
<app-select
controlId="idpOutboundSigningAlgorithm"
[label]="'idpOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="idpOutboundSigningAlgorithm"
>
</app-select>
<bit-form-field>
<bit-label>{{ "idpOutboundSigningAlgorithm" | i18n }}</bit-label>
<select bitInput formControlName="idpOutboundSigningAlgorithm">
<option
*ngFor="let o of samlSigningAlgorithmOptions"
[ngValue]="o.value"
disabled="{{ o.disabled }}"
>
{{ o.name }}
</option>
</select>
</bit-form-field>
<!--TODO: Uncomment once Unsolicited IdP Response is supported-->
<!-- <app-input-checkbox
@@ -430,19 +486,8 @@
</div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<div
id="errorSummary"
class="error-summary text-danger"
*ngIf="this.getErrorCount(ssoConfigForm) as errorCount"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
(errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount
}}
</div>
<bit-submit-button buttonType="primary" [loading]="form.loading">
{{ "save" | i18n }}
</bit-submit-button>
<bit-error-summary [formGroup]="ssoConfigForm"></bit-error-summary>
</form>

View File

@@ -1,9 +1,16 @@
import { Component, OnInit } from "@angular/core";
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
import { Component, OnDestroy, OnInit } from "@angular/core";
import {
AbstractControl,
FormBuilder,
FormControl,
UntypedFormGroup,
Validators,
} from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";
import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions";
import { dirtyRequired } from "@bitwarden/angular/validators/dirty.validator";
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
@@ -29,8 +36,7 @@ const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha2
selector: "app-org-manage-sso",
templateUrl: "sso.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class SsoComponent implements OnInit {
export class SsoComponent implements OnInit, OnDestroy {
readonly ssoType = SsoType;
readonly ssoTypeOptions: SelectOptions[] = [
@@ -75,6 +81,8 @@ export class SsoComponent implements OnInit {
{ name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost },
];
private destory$ = new Subject<void>();
showOpenIdCustomizations = false;
loading = true;
@@ -89,62 +97,69 @@ export class SsoComponent implements OnInit {
spMetadataUrl: string;
spAcsUrl: string;
enabled = this.formBuilder.control(false);
private enabled = this.formBuilder.control(false);
openIdForm = this.formBuilder.group(
private ssoIdentifier = this.formBuilder.control("", {
validators: [Validators.maxLength(50), Validators.required],
});
private openIdForm = this.formBuilder.group<ControlsOf<SsoConfigView["openId"]>>(
{
authority: ["", dirtyRequired],
clientId: ["", dirtyRequired],
clientSecret: ["", dirtyRequired],
metadataAddress: [],
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
authority: new FormControl("", Validators.required),
clientId: new FormControl("", Validators.required),
clientSecret: new FormControl("", Validators.required),
metadataAddress: new FormControl(),
redirectBehavior: new FormControl(
OpenIdConnectRedirectBehavior.RedirectGet,
Validators.required
),
getClaimsFromUserInfoEndpoint: new FormControl(),
additionalScopes: new FormControl(),
additionalUserIdClaimTypes: new FormControl(),
additionalEmailClaimTypes: new FormControl(),
additionalNameClaimTypes: new FormControl(),
acrValues: new FormControl(),
expectedReturnAcrValue: new FormControl(),
},
{
updateOn: "blur",
}
);
samlForm = this.formBuilder.group(
private samlForm = this.formBuilder.group<ControlsOf<SsoConfigView["saml"]>>(
{
spNameIdFormat: [Saml2NameIdFormat.NotConfigured],
spOutboundSigningAlgorithm: [defaultSigningAlgorithm],
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned],
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
spWantAssertionsSigned: [],
spValidateCertificates: [],
spNameIdFormat: new FormControl(Saml2NameIdFormat.NotConfigured),
spOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
spSigningBehavior: new FormControl(Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned),
spMinIncomingSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
spWantAssertionsSigned: new FormControl(),
spValidateCertificates: new FormControl(),
idpEntityId: ["", dirtyRequired],
idpBindingType: [Saml2BindingType.HttpRedirect],
idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [],
idpX509PublicCert: ["", dirtyRequired],
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm],
idpAllowUnsolicitedAuthnResponse: [],
idpAllowOutboundLogoutRequests: [true],
idpWantAuthnRequestsSigned: [],
idpEntityId: new FormControl("", Validators.required),
idpBindingType: new FormControl(Saml2BindingType.HttpRedirect),
idpSingleSignOnServiceUrl: new FormControl(),
idpSingleLogoutServiceUrl: new FormControl(),
idpX509PublicCert: new FormControl("", Validators.required),
idpOutboundSigningAlgorithm: new FormControl(defaultSigningAlgorithm),
idpAllowUnsolicitedAuthnResponse: new FormControl(),
idpAllowOutboundLogoutRequests: new FormControl(true),
idpWantAuthnRequestsSigned: new FormControl(),
},
{
updateOn: "blur",
}
);
ssoConfigForm = this.formBuilder.group({
configType: [SsoType.None],
keyConnectorEnabled: [false],
keyConnectorUrl: [""],
private ssoConfigForm = this.formBuilder.group<ControlsOf<SsoConfigView>>({
configType: new FormControl(SsoType.None),
keyConnectorEnabled: new FormControl(false),
keyConnectorUrl: new FormControl(""),
openId: this.openIdForm,
saml: this.samlForm,
});
constructor(
private formBuilder: UntypedFormBuilder,
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
@@ -154,32 +169,41 @@ export class SsoComponent implements OnInit {
) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.ssoConfigForm.get("configType").valueChanges.subscribe((newType: SsoType) => {
if (newType === SsoType.OpenIdConnect) {
this.openIdForm.enable();
this.samlForm.disable();
} else if (newType === SsoType.Saml2) {
this.openIdForm.disable();
this.samlForm.enable();
} else {
this.openIdForm.disable();
this.samlForm.disable();
}
});
this.ssoConfigForm
.get("configType")
.valueChanges.pipe(takeUntil(this.destory$))
.subscribe((newType: SsoType) => {
if (newType === SsoType.OpenIdConnect) {
this.openIdForm.enable();
this.samlForm.disable();
} else if (newType === SsoType.Saml2) {
this.openIdForm.disable();
this.samlForm.enable();
} else {
this.openIdForm.disable();
this.samlForm.disable();
}
});
this.samlForm
.get("spSigningBehavior")
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
.valueChanges.subscribe(() =>
this.samlForm.get("idpX509PublicCert").updateValueAndValidity()
);
.valueChanges.pipe(takeUntil(this.destory$))
.subscribe(() => this.samlForm.get("idpX509PublicCert").updateValueAndValidity());
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
});
this.route.params
.pipe(
concatMap(async (params) => {
this.organizationId = params.organizationId;
await this.load();
}),
takeUntil(this.destory$)
)
.subscribe();
}
ngOnDestroy(): void {
this.destory$.next();
this.destory$.complete();
}
async load() {
@@ -199,7 +223,8 @@ export class SsoComponent implements OnInit {
async submit() {
this.validateForm(this.ssoConfigForm);
if (this.ssoConfigForm.get("keyConnectorEnabled").value) {
if (this.ssoConfigForm.value.keyConnectorEnabled) {
this.haveTestedKeyConnector = false;
await this.validateKeyConnectorUrl();
}
@@ -210,7 +235,8 @@ export class SsoComponent implements OnInit {
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = SsoConfigApi.fromView(this.ssoConfigForm.value as SsoConfigView);
request.identifier = this.ssoIdentifier.value;
request.data = SsoConfigApi.fromView(this.ssoConfigForm.getRawValue());
this.formPromise = this.organizationApiService.updateSso(this.organizationId, request);
@@ -237,7 +263,7 @@ export class SsoComponent implements OnInit {
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
invalidUrl: { message: this.i18nService.t("keyConnectorTestFail") },
});
}
@@ -294,6 +320,7 @@ export class SsoComponent implements OnInit {
private populateForm(ssoSettings: OrganizationSsoResponse) {
this.enabled.setValue(ssoSettings.enabled);
this.ssoIdentifier.setValue(ssoSettings.identifier);
if (ssoSettings.data != null) {
const ssoConfigView = new SsoConfigView(ssoSettings.data);
this.ssoConfigForm.patchValue(ssoConfigView);

View File

@@ -6,7 +6,8 @@ import { Organization } from "@bitwarden/common/models/domain/organization";
import { OrganizationPermissionsGuard } from "src/app/organizations/guards/org-permissions.guard";
import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organization-layout.component";
import { ManageComponent } from "src/app/organizations/manage/manage.component";
import { canAccessSettingsTab } from "src/app/organizations/navigation-permissions";
import { SettingsComponent } from "src/app/organizations/settings/settings.component";
import { ScimComponent } from "./manage/scim.component";
import { SsoComponent } from "./manage/sso.component";
@@ -18,8 +19,12 @@ const routes: Routes = [
canActivate: [AuthGuard, OrganizationPermissionsGuard],
children: [
{
path: "manage",
component: ManageComponent,
path: "settings",
component: SettingsComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
organizationPermissions: canAccessSettingsTab,
},
children: [
{
path: "sso",

View File

@@ -3,24 +3,12 @@ import { NgModule } from "@angular/core";
import { SharedModule } from "src/app/shared/shared.module";
import { InputCheckboxComponent } from "./components/input-checkbox.component";
import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component";
import { InputTextComponent } from "./components/input-text.component";
import { SelectComponent } from "./components/select.component";
import { ScimComponent } from "./manage/scim.component";
import { SsoComponent } from "./manage/sso.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module";
// Form components are for use in the SSO Configuration Form only and should not be exported for use elsewhere.
// They will be deprecated by the Component Library.
@NgModule({
imports: [SharedModule, OrganizationsRoutingModule],
declarations: [
InputCheckboxComponent,
InputTextComponent,
InputTextReadOnlyComponent,
SelectComponent,
SsoComponent,
ScimComponent,
],
declarations: [InputCheckboxComponent, SsoComponent, ScimComponent],
})
export class OrganizationsModule {}

View File

@@ -2,10 +2,7 @@ import { Component } from "@angular/core";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
import { BasePolicy, BasePolicyComponent } from "src/app/organizations/policies";
export class DisablePersonalVaultExportPolicy extends BasePolicy {
name = "disablePersonalVaultExport";

View File

@@ -5,10 +5,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { PolicyRequest } from "@bitwarden/common/models/request/policyRequest";
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
import { BasePolicy, BasePolicyComponent } from "src/app/organizations/policies";
export class MaximumVaultTimeoutPolicy extends BasePolicy {
name = "maximumVaultTimeout";

View File

@@ -0,0 +1,16 @@
import { Directive, HostListener, Input } from "@angular/core";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Directive({
selector: "[appCopyClick]",
})
export class CopyClickDirective {
constructor(private platformUtilsService: PlatformUtilsService) {}
@Input("appCopyClick") valueToCopy = "";
@HostListener("click") onClick() {
this.platformUtilsService.copyToClipboard(this.valueToCopy);
}
}

View File

@@ -0,0 +1,19 @@
import { Directive, HostListener, Input } from "@angular/core";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Utils } from "@bitwarden/common/misc/utils";
@Directive({
selector: "[appLaunchClick]",
})
export class LaunchClickDirective {
constructor(private platformUtilsService: PlatformUtilsService) {}
@Input("appLaunchClick") uriToLaunch = "";
@HostListener("click") onClick() {
if (!Utils.isNullOrWhitespace(this.uriToLaunch)) {
this.platformUtilsService.launchUri(this.uriToLaunch);
}
}
}

View File

@@ -12,9 +12,11 @@ import { A11yTitleDirective } from "./directives/a11y-title.directive";
import { ApiActionDirective } from "./directives/api-action.directive";
import { AutofocusDirective } from "./directives/autofocus.directive";
import { BoxRowDirective } from "./directives/box-row.directive";
import { CopyClickDirective } from "./directives/copy-click.directive";
import { FallbackSrcDirective } from "./directives/fallback-src.directive";
import { InputStripSpacesDirective } from "./directives/input-strip-spaces.directive";
import { InputVerbatimDirective } from "./directives/input-verbatim.directive";
import { LaunchClickDirective } from "./directives/launch-click.directive";
import { NotPremiumDirective } from "./directives/not-premium.directive";
import { SelectCopyDirective } from "./directives/select-copy.directive";
import { StopClickDirective } from "./directives/stop-click.directive";
@@ -66,6 +68,8 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
StopClickDirective,
StopPropDirective,
TrueFalseValueDirective,
CopyClickDirective,
LaunchClickDirective,
UserNamePipe,
PasswordStrengthComponent,
],
@@ -95,6 +99,8 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
StopClickDirective,
StopPropDirective,
TrueFalseValueDirective,
CopyClickDirective,
LaunchClickDirective,
UserNamePipe,
PasswordStrengthComponent,
],

View File

@@ -0,0 +1,20 @@
import { FormControl, FormGroup } from "@angular/forms";
/**
* Helper type to map a model to a strictly typed from group. Allows specifying a formGroup's
* type explicity using an existing model/interface instead of a type being inferred
* indirectly by control structure.
* Source: https://netbasal.com/typed-reactive-forms-in-angular-no-longer-a-type-dream-bf6982b0af28
* @example
* interface UserData {
* name: string;
* age: number;
* }
* const strictlyTypedForm = this.formBuilder.group<Controls<UserData>>({
* name: new FormControl("John"),
* age: new FormControl(24),
* });
*/
export type ControlsOf<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends Record<any, any> ? FormGroup<ControlsOf<T[K]>> : FormControl<T[K]>;
};

View File

@@ -1,9 +0,0 @@
import { AbstractControl, ValidationErrors, Validators } from "@angular/forms";
/**
* Runs Validators.required on a field only if it's dirty. This prevents error messages from being displayed
* to the user prematurely.
*/
export function dirtyRequired(control: AbstractControl): ValidationErrors | null {
return control.dirty ? Validators.required(control) : null;
}

View File

@@ -74,7 +74,7 @@ export class SsoConfigApi extends BaseResponse {
spNameIdFormat: Saml2NameIdFormat;
spOutboundSigningAlgorithm: string;
spSigningBehavior: Saml2SigningBehavior;
spMinIncomingSigningAlgorithm: boolean;
spMinIncomingSigningAlgorithm: string;
spWantAssertionsSigned: boolean;
spValidateCertificates: boolean;

View File

@@ -2,5 +2,6 @@ import { SsoConfigApi } from "../../api/ssoConfigApi";
export class OrganizationSsoRequest {
enabled = false;
identifier: string;
data: SsoConfigApi;
}

View File

@@ -2,6 +2,10 @@ import { OrganizationKeysRequest } from "./organizationKeysRequest";
export class OrganizationUpdateRequest {
name: string;
/**
* @deprecated 2022-08-03 Moved to OrganizationSsoRequest, left for backwards compatability.
* https://bitwarden.atlassian.net/browse/EC-489
*/
identifier: string;
businessName: string;
billingEmail: string;

View File

@@ -3,12 +3,14 @@ import { BaseResponse } from "../baseResponse";
export class OrganizationSsoResponse extends BaseResponse {
enabled: boolean;
identifier: string;
data: SsoConfigApi;
urls: SsoUrls;
constructor(response: any) {
super(response);
this.enabled = this.getResponseProperty("Enabled");
this.identifier = this.getResponseProperty("Identifier");
this.data =
this.getResponseProperty("Data") != null
? new SsoConfigApi(this.getResponseProperty("Data"))

View File

@@ -34,7 +34,7 @@ export class SsoConfigView extends View {
spNameIdFormat: Saml2NameIdFormat;
spOutboundSigningAlgorithm: string;
spSigningBehavior: Saml2SigningBehavior;
spMinIncomingSigningAlgorithm: boolean;
spMinIncomingSigningAlgorithm: string;
spWantAssertionsSigned: boolean;
spValidateCertificates: boolean;

View File

@@ -33,6 +33,10 @@ export class BitErrorSummary {
return acc;
}
if (!control.dirty && control.untouched) {
return acc;
}
return acc + Object.keys(control.errors).length;
}, 0);
}

View File

@@ -28,6 +28,8 @@ export class BitErrorComponent {
return this.i18nService.t("inputEmail");
case "minlength":
return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength);
case "maxlength":
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
default:
// Attempt to show a custom error message.
if (this.error[1]?.message) {