diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8e52b1ba006..0e4c0da2452 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2877,6 +2877,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, diff --git a/apps/cli/package.json b/apps/cli/package.json index 55bcee689d0..fb9f682f961 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.52", + "tldts": "6.1.56", "zxcvbn": "4.4.2" } } diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index b87112508fe..13023ed5c3c 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -1965,9 +1965,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 2a6e402b445..3180604918e 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -40,7 +40,7 @@ scopeguard = "=1.2.0" sha2 = "=0.10.8" thiserror = "=1.0.61" tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] } -tokio-util = "=0.7.11" +tokio-util = "=0.7.12" typenum = "=1.17.0" [target.'cfg(windows)'.dependencies] diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index b243b51acc9..06c6cd1d2e5 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -14,11 +14,11 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "10.0.0", + "uuid": "11.0.1", "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.11", + "@types/node": "20.17.1", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -106,9 +106,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", - "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "version": "20.17.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz", + "integrity": "sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -421,16 +421,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.1.tgz", + "integrity": "sha512-wt9UB5EcLhnboy1UvA1mvGPXkIIrHSu+3FmUksARfdVw9tuPf3CH/CohxO0Su1ApoKAeT6BVzAJIvjTuQVSmuQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index ccd480e31b6..0c38902ea4c 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -19,11 +19,11 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "10.0.0", + "uuid": "11.0.1", "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "20.16.11", + "@types/node": "20.17.1", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index d24616edab5..414f254e7ad 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2393,6 +2393,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, diff --git a/apps/web/config/usdev.json b/apps/web/config/usdev.json index af96a38c6af..c19deba27b1 100644 --- a/apps/web/config/usdev.json +++ b/apps/web/config/usdev.json @@ -4,6 +4,15 @@ "notifications": "https://notifications.usdev.bitwarden.pw", "scim": "https://scim.usdev.bitwarden.pw" }, + "additionalRegions": [ + { + "key": "USDEV", + "domain": "usdev.bitwarden.pw", + "urls": { + "webVault": "https://vault.usdev.bitwarden.pw" + } + } + ], "flags": { "showPasswordless": true } diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 2c5daf93c6f..a8ecf255f33 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -23,14 +23,15 @@

{{ "inviteUserDesc" | i18n }}

- + {{ "email" | i18n }} - {{ - "inviteMultipleEmailDesc" - | i18n - : (organization.productTierType === ProductTierType.TeamsStarter ? "10" : "20") + {{ + "inviteMultipleEmailDesc" | i18n: remainingSeats }} + + {{ "inviteSingleEmailDesc" | i18n: remainingSeats }} +
diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 8df40e35fef..4a95c9cb9cb 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -89,6 +89,7 @@ export class MemberDialogComponent implements OnDestroy { PermissionMode = PermissionMode; showNoMasterPasswordWarning = false; isOnSecretsManagerStandalone: boolean; + remainingSeats$: Observable; protected organization$: Observable; protected collectionAccessItems: AccessItemView[] = []; @@ -250,6 +251,10 @@ export class MemberDialogComponent implements OnDestroy { this.loading = false; }); + + this.remainingSeats$ = this.organization$.pipe( + map((organization) => organization.seats - this.params.numConfirmedMembers), + ); } private setFormValidators(organization: Organization) { diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html index 20f62c1be0b..f656d488e06 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.html @@ -15,7 +15,7 @@ - - - {{ "cancel" | i18n }} - - - - - - - diff --git a/apps/web/src/app/auth/settings/account/account.component.html b/apps/web/src/app/auth/settings/account/account.component.html index 71508f7ae97..a5e5329fce7 100644 --- a/apps/web/src/app/auth/settings/account/account.component.html +++ b/apps/web/src/app/auth/settings/account/account.component.html @@ -3,7 +3,7 @@ -
+

{{ "changeEmail" | i18n }}

diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index dd8dc881f6e..51bf4276960 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; -import { lastValueFrom, map, Observable, of, switchMap } from "rxjs"; +import { combineLatest, from, lastValueFrom, map, Observable } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -21,7 +21,7 @@ export class AccountComponent implements OnInit { @ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef; - showChangeEmail = true; + showChangeEmail$: Observable; showPurgeVault$: Observable; constructor( @@ -33,21 +33,36 @@ export class AccountComponent implements OnInit { ) {} async ngOnInit() { - this.showChangeEmail = await this.userVerificationService.hasMasterPassword(); - this.showPurgeVault$ = this.configService - .getFeatureFlag$(FeatureFlag.AccountDeprovisioning) - .pipe( - switchMap((isAccountDeprovisioningEnabled) => - isAccountDeprovisioningEnabled - ? this.organizationService.organizations$.pipe( - map( - (organizations) => - !organizations.some((o) => o.userIsManagedByOrganization === true), - ), - ) - : of(true), - ), - ); + const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.AccountDeprovisioning, + ); + + const userIsManagedByOrganization$ = this.organizationService.organizations$.pipe( + map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), + ); + + const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword()); + + this.showChangeEmail$ = combineLatest([ + hasMasterPassword$, + isAccountDeprovisioningEnabled$, + userIsManagedByOrganization$, + ]).pipe( + map( + ([hasMasterPassword, isAccountDeprovisioningEnabled, userIsManagedByOrganization]) => + hasMasterPassword && (!isAccountDeprovisioningEnabled || !userIsManagedByOrganization), + ), + ); + + this.showPurgeVault$ = combineLatest([ + isAccountDeprovisioningEnabled$, + userIsManagedByOrganization$, + ]).pipe( + map( + ([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) => + !isAccountDeprovisioningEnabled || !userIsManagedByOrganization, + ), + ); } async deauthorizeSessions() { diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index b3a8db20028..71d26030b03 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -40,7 +40,6 @@ import { flagEnabled, Flags } from "../utils/flags"; import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component"; import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; -import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { deepLinkGuard } from "./auth/guards/deep-link.guard"; import { HintComponent } from "./auth/hint.component"; @@ -156,12 +155,6 @@ const routes: Routes = [ canActivate: [unauthGuardFn()], data: { titleId: "deleteOrganization" }, }, - { - path: "verify-recover-delete-provider", - component: VerifyRecoverDeleteProviderComponent, - canActivate: [unauthGuardFn()], - data: { titleId: "deleteAccount" } satisfies RouteDataProperties, - }, { path: "update-temp-password", component: UpdateTempPasswordComponent, diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 638a523cd4f..a238f2110ce 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -17,8 +17,6 @@ import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../admin-console/organizations/tools/reused-passwords-report.component"; import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component"; import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component"; -import { ProvidersComponent } from "../admin-console/providers/providers.component"; -import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component"; import { HintComponent } from "../auth/hint.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; @@ -149,7 +147,6 @@ import { SharedModule } from "./shared.module"; PremiumBadgeComponent, ProfileComponent, ChangeAvatarDialogComponent, - ProvidersComponent, PurgeVaultComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, @@ -176,7 +173,6 @@ import { SharedModule } from "./shared.module"; UpdateTempPasswordComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, - VerifyRecoverDeleteProviderComponent, ], exports: [ UserVerificationModule, @@ -218,7 +214,6 @@ import { SharedModule } from "./shared.module"; PremiumBadgeComponent, ProfileComponent, ChangeAvatarDialogComponent, - ProvidersComponent, PurgeVaultComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, @@ -246,7 +241,6 @@ import { SharedModule } from "./shared.module"; UserLayoutComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, - VerifyRecoverDeleteProviderComponent, HeaderModule, DangerZoneComponent, ], diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9ea33149de8..eead4fd80d6 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -3214,6 +3214,9 @@ } } }, + "inviteSingleEmailDesc": { + "message": "You have 1 invite remaining." + }, "userUsingTwoStep": { "message": "This user is using two-step login to protect their account." }, @@ -4681,12 +4684,18 @@ "singleOrgDesc": { "message": "Restrict members from joining other organizations." }, + "singleOrgPolicyDesc": { + "message": "Restrict members from joining other organizations. This policy is required for organizations that have enabled domain verification." + }, "singleOrgBlockCreateMessage": { "message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account." }, "singleOrgPolicyWarning": { "message": "Organization members who are not owners or admins and are already a member of another organization will be removed from your organization." }, + "singleOrgPolicyMemberWarning": { + "message": "Non-compliant members will be placed in revoked status until they leave all other organizations. Administrators are exempt and can restore members once compliance is met." + }, "requireSso": { "message": "Require single sign-on authentication" }, @@ -6403,6 +6412,20 @@ "generateEmail": { "message": "Generate email" }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -9528,5 +9551,11 @@ }, "selfHostingTitleProper": { "message": "Self-Hosting" + }, + "verified-domain-single-org-warning" : { + "message": "Verifying a domain will turn on the single organization policy." + }, + "single-org-revoked-user-warning": { + "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts index 703808900c9..987888741a4 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/domain-verification/domain-verification.component.ts @@ -1,14 +1,24 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Params } from "@angular/router"; -import { concatMap, Observable, Subject, take, takeUntil } from "rxjs"; +import { + concatMap, + firstValueFrom, + map, + Observable, + Subject, + take, + takeUntil, + withLatestFrom, +} from "rxjs"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrgDomainServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction"; import { OrganizationDomainResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain.response"; import { HttpStatusCode } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -31,13 +41,13 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { constructor( private route: ActivatedRoute, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private orgDomainApiService: OrgDomainApiServiceAbstraction, private orgDomainService: OrgDomainServiceAbstraction, private dialogService: DialogService, private validationService: ValidationService, private toastService: ToastService, + private configService: ConfigService, ) {} // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -64,13 +74,38 @@ export class DomainVerificationComponent implements OnInit, OnDestroy { this.loading = false; } - addDomain() { + async addDomain() { const domainAddEditDialogData: DomainAddEditDialogData = { organizationId: this.organizationId, orgDomain: null, existingDomainNames: this.getExistingDomainNames(), }; + await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning).pipe( + withLatestFrom(this.orgDomains$), + map(async ([accountDeprovisioningEnabled, organizationDomains]) => { + if ( + accountDeprovisioningEnabled && + organizationDomains.every((domain) => domain.verifiedDate === null) + ) { + await this.dialogService.openSimpleDialog({ + title: { key: "verified-domain-single-org-warning" }, + content: { key: "single-org-revoked-user-warning" }, + cancelButtonText: { key: "cancel" }, + acceptButtonText: { key: "confirm" }, + acceptAction: () => this.openAddDomainDialog(domainAddEditDialogData), + type: "info", + }); + } else { + await this.openAddDomainDialog(domainAddEditDialogData); + } + }), + ), + ); + } + + private async openAddDomainDialog(domainAddEditDialogData: DomainAddEditDialogData) { this.dialogService.open(DomainAddEditDialogComponent, { data: domainAddEditDialogData, }); diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 55c22ec4cea..00c944e69bb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -4,7 +4,6 @@ import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/providers/providers.component"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -22,6 +21,7 @@ import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { EventsComponent } from "./manage/events.component"; import { MembersComponent } from "./manage/members.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; +import { ProvidersComponent } from "./providers.component"; import { AccountComponent } from "./settings/account.component"; import { SetupProviderComponent } from "./setup/setup-provider.component"; import { SetupComponent } from "./setup/setup.component"; diff --git a/apps/web/src/app/admin-console/providers/providers.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.html similarity index 100% rename from apps/web/src/app/admin-console/providers/providers.component.html rename to bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.html diff --git a/apps/web/src/app/admin-console/providers/providers.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts similarity index 100% rename from apps/web/src/app/admin-console/providers/providers.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/providers.component.ts diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index b6c7125c48c..80108e66eda 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -32,10 +32,12 @@ import { MembersComponent } from "./manage/members.component"; import { UserAddEditComponent } from "./manage/user-add-edit.component"; import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersRoutingModule } from "./providers-routing.module"; +import { ProvidersComponent } from "./providers.component"; import { WebProviderService } from "./services/web-provider.service"; import { AccountComponent } from "./settings/account.component"; import { SetupProviderComponent } from "./setup/setup-provider.component"; import { SetupComponent } from "./setup/setup.component"; +import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-provider.component"; @NgModule({ imports: [ @@ -73,6 +75,8 @@ import { SetupComponent } from "./setup/setup.component"; ProviderBillingHistoryComponent, ProviderSubscriptionComponent, ProviderSubscriptionStatusComponent, + ProvidersComponent, + VerifyRecoverDeleteProviderComponent, ], providers: [WebProviderService], }) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.html new file mode 100644 index 00000000000..116e1660d7a --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.html @@ -0,0 +1,15 @@ +

{{ "deleteProvider" | i18n }}

+{{ "deleteProviderWarning" | i18n }} +

+ {{ name }} +

+

{{ "deleteProviderRecoverConfirmDesc" | i18n }}

+
+
+ + + {{ "cancel" | i18n }} + +
diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts similarity index 63% rename from apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts rename to bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts index dc6fa099610..a4461b3e11a 100644 --- a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -5,8 +5,6 @@ import { firstValueFrom } from "rxjs"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-verify-recover-delete.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; @Component({ @@ -16,7 +14,6 @@ import { ToastService } from "@bitwarden/components"; // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class VerifyRecoverDeleteProviderComponent implements OnInit { name: string; - formPromise: Promise; private providerId: string; private token: string; @@ -24,10 +21,8 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { constructor( private router: Router, private providerApiService: ProviderApiServiceAbstraction, - private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private route: ActivatedRoute, - private logService: LogService, private toastService: ToastService, ) {} @@ -42,22 +37,14 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { } } - async submit() { - try { - const request = new ProviderVerifyRecoverDeleteRequest(this.token); - this.formPromise = this.providerApiService.providerRecoverDeleteToken( - this.providerId, - request, - ); - await this.formPromise; - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("providerDeleted"), - message: this.i18nService.t("providerDeletedDesc"), - }); - await this.router.navigate(["/"]); - } catch (e) { - this.logService.error(e); - } - } + submit = async () => { + const request = new ProviderVerifyRecoverDeleteRequest(this.token); + await this.providerApiService.providerRecoverDeleteToken(this.providerId, request); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("providerDeleted"), + message: this.i18nService.t("providerDeletedDesc"), + }); + await this.router.navigate(["/"]); + }; } diff --git a/bitwarden_license/bit-web/src/app/app-routing.module.ts b/bitwarden_license/bit-web/src/app/app-routing.module.ts index f3f3c158708..6aed12511c1 100644 --- a/bitwarden_license/bit-web/src/app/app-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/app-routing.module.ts @@ -1,9 +1,13 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { unauthGuardFn } from "@bitwarden/angular/auth/guards"; +import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link.guard"; +import { RouteDataProperties } from "@bitwarden/web-vault/app/core"; import { ProvidersModule } from "./admin-console/providers/providers.module"; +import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component"; const routes: Routes = [ { @@ -17,6 +21,18 @@ const routes: Routes = [ loadChildren: async () => (await import("./secrets-manager/secrets-manager.module")).SecretsManagerModule, }, + { + path: "verify-recover-delete-provider", + component: AnonLayoutWrapperComponent, + canActivate: [unauthGuardFn()], + children: [ + { + path: "", + component: VerifyRecoverDeleteProviderComponent, + data: { titleId: "deleteAccount" } satisfies RouteDataProperties, + }, + ], + }, ]; @NgModule({ diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index ad17a0a97a3..239383ddd00 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -18,7 +18,8 @@ import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstraction import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { ClientType } from "@bitwarden/common/enums"; +import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -26,6 +27,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; @@ -136,6 +138,7 @@ export class LoginComponent implements OnInit, OnDestroy { private syncService: SyncService, private toastService: ToastService, private logService: LogService, + private validationService: ValidationService, ) { this.clientType = this.platformUtilsService.getClientType(); this.loginViaAuthRequestSupported = this.loginComponentService.isLoginViaAuthRequestSupported(); @@ -182,19 +185,54 @@ export class LoginComponent implements OnInit, OnDestroy { null, ); - const authResult = await this.loginStrategyService.logIn(credentials); + try { + const authResult = await this.loginStrategyService.logIn(credentials); - await this.saveEmailSettings(); - await this.handleAuthResult(authResult); + await this.saveEmailSettings(); + await this.handleAuthResult(authResult); - if (this.clientType === ClientType.Desktop) { - if (this.captchaSiteKey) { - const content = document.getElementById("content") as HTMLDivElement; - content.setAttribute("style", "width:335px"); + if (this.clientType === ClientType.Desktop) { + if (this.captchaSiteKey) { + const content = document.getElementById("content") as HTMLDivElement; + content.setAttribute("style", "width:335px"); + } } + } catch (error) { + this.logService.error(error); + this.handleSubmitError(error); } }; + /** + * Handles the error from the submit function. + * + * @param error The error object. + */ + private handleSubmitError(error: unknown) { + // Handle error responses + if (error instanceof ErrorResponse) { + switch (error.statusCode) { + case HttpStatusCode.BadRequest: { + if (error.message.toLowerCase().includes("username or password is incorrect")) { + this.formGroup.controls.masterPassword.setErrors({ + error: { + message: this.i18nService.t("invalidMasterPassword"), + }, + }); + } + break; + } + default: { + // Allow all other errors to be handled by toast + this.validationService.showError(error); + } + } + } else { + // Allow all other errors to be handled by toast + this.validationService.showError(error); + } + } + /** * Handles the result of the authentication process. * diff --git a/libs/common/src/admin-console/models/response/policy.response.ts b/libs/common/src/admin-console/models/response/policy.response.ts index 25a1f208a0b..0544cd996f4 100644 --- a/libs/common/src/admin-console/models/response/policy.response.ts +++ b/libs/common/src/admin-console/models/response/policy.response.ts @@ -8,6 +8,7 @@ export class PolicyResponse extends BaseResponse { type: PolicyType; data: any; enabled: boolean; + canToggleState: boolean; constructor(response: any) { super(response); @@ -16,5 +17,6 @@ export class PolicyResponse extends BaseResponse { this.type = this.getResponseProperty("Type"); this.data = this.getResponseProperty("Data"); this.enabled = this.getResponseProperty("Enabled"); + this.canToggleState = this.getResponseProperty("CanToggleState") ?? true; } } diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index adea07becc7..a1617315448 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -96,7 +96,7 @@ export class DefaultSdkService implements SdkService { let client: BitwardenClient; const createAndInitializeClient = async () => { - if (privateKey == null || userKey == null || orgKeys == null) { + if (privateKey == null || userKey == null) { return undefined; } @@ -150,7 +150,7 @@ export class DefaultSdkService implements SdkService { kdfParams: KdfConfig, privateKey: EncryptedString, userKey: UserKey, - orgKeys: Record, + orgKeys?: Record, ) { await client.crypto().initialize_user_crypto({ email: account.email, @@ -169,9 +169,12 @@ export class DefaultSdkService implements SdkService { }, privateKey, }); + + // We initialize the org crypto even if the org_keys are + // null to make sure any existing org keys are cleared. await client.crypto().initialize_org_crypto({ organizationKeys: new Map( - Object.entries(orgKeys) + Object.entries(orgKeys ?? {}) .filter(([_, v]) => v.type === "organization") .map(([k, v]) => [k, v.key]), ), diff --git a/libs/tools/generator/components/src/credential-generator-history.component.html b/libs/tools/generator/components/src/credential-generator-history.component.html index 2b8802b9327..c42d6a12729 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.html +++ b/libs/tools/generator/components/src/credential-generator-history.component.html @@ -1,7 +1,10 @@
-

{{ credential.credential }}

+ {{ credential.generationDate | date: "medium" }} diff --git a/libs/tools/generator/components/src/credential-generator-history.component.ts b/libs/tools/generator/components/src/credential-generator-history.component.ts index 2f76027a941..bcedd91babf 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history.component.ts @@ -9,6 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { UserId } from "@bitwarden/common/types/guid"; import { CardComponent, + ColorPasswordModule, IconButtonModule, NoItemsModule, SectionComponent, @@ -21,6 +22,7 @@ import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generat selector: "bit-credential-generator-history", templateUrl: "credential-generator-history.component.html", imports: [ + ColorPasswordModule, CommonModule, IconButtonModule, NoItemsModule, diff --git a/libs/tools/generator/components/src/passphrase-settings.component.html b/libs/tools/generator/components/src/passphrase-settings.component.html index 25e9684e864..d089de7a07b 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.html +++ b/libs/tools/generator/components/src/passphrase-settings.component.html @@ -8,6 +8,7 @@ {{ "numWords" | i18n }} + {{ numWordsBoundariesHint$ | async }}
diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index 4c171e0c205..d65e897f4e1 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -1,9 +1,10 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, takeUntil, Subject } from "rxjs"; +import { BehaviorSubject, skip, takeUntil, Subject, ReplaySubject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { Generators, @@ -29,11 +30,13 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component * @param accountService queries user availability * @param generatorService settings and policy logic + * @param i18nService localize hints * @param formBuilder reactive form controls */ constructor( private formBuilder: FormBuilder, private generatorService: CredentialGeneratorService, + private i18nService: I18nService, private accountService: AccountService, ) {} @@ -97,6 +100,13 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly); this.toggleEnabled(Controls.includeNumber, !constraints.includeNumber?.readonly); + + const boundariesHint = this.i18nService.t( + "generatorBoundariesHint", + constraints.numWords.min, + constraints.numWords.max, + ); + this.numWordsBoundariesHint.next(boundariesHint); }); // now that outputs are set up, connect inputs @@ -106,6 +116,11 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { /** display binding for enterprise policy notice */ protected policyInEffect: boolean; + private numWordsBoundariesHint = new ReplaySubject(1); + + /** display binding for min/max constraints of `numWords` */ + protected numWordsBoundariesHint$ = this.numWordsBoundariesHint.asObservable(); + private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) { if (enabled) { this.settings.get(setting).enable({ emitEvent: false }); diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index 5e1d1941e74..aa12a3247c3 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -8,6 +8,7 @@ {{ "length" | i18n }} + {{ lengthBoundariesHint$ | async }}
diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index 7832818d678..6e9d106b71a 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -1,9 +1,10 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, takeUntil, Subject, map, filter, tap, skip } from "rxjs"; +import { BehaviorSubject, takeUntil, Subject, map, filter, tap, skip, ReplaySubject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { Generators, @@ -33,11 +34,13 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component * @param accountService queries user availability * @param generatorService settings and policy logic + * @param i18nService localize hints * @param formBuilder reactive form controls */ constructor( private formBuilder: FormBuilder, private generatorService: CredentialGeneratorService, + private i18nService: I18nService, private accountService: AccountService, ) {} @@ -147,6 +150,13 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { for (const [control, enabled] of toggles) { this.toggleEnabled(control, enabled); } + + const boundariesHint = this.i18nService.t( + "generatorBoundariesHint", + constraints.length.min, + constraints.length.max, + ); + this.lengthBoundariesHint.next(boundariesHint); }); // cascade selections between checkboxes and spinboxes @@ -208,6 +218,11 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { /** display binding for enterprise policy notice */ protected policyInEffect: boolean; + private lengthBoundariesHint = new ReplaySubject(1); + + /** display binding for min/max constraints of `length` */ + protected lengthBoundariesHint$ = this.lengthBoundariesHint.asObservable(); + private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) { if (enabled) { this.settings.get(setting).enable({ emitEvent: false }); diff --git a/libs/tools/generator/components/src/subaddress-settings.component.ts b/libs/tools/generator/components/src/subaddress-settings.component.ts index 30db8dc657d..bd6ca899db7 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.ts +++ b/libs/tools/generator/components/src/subaddress-settings.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs"; +import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -53,9 +53,23 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy { const singleUserId$ = this.singleUserId$(); const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ }); - settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => { - this.settings.patchValue(s, { emitEvent: false }); - }); + settings + .pipe( + withLatestFrom(this.accountService.activeAccount$), + map(([settings, activeAccount]) => { + // if the subaddress isn't specified, copy it from + // the user's settings + if ((settings.subaddressEmail ?? "").length < 1) { + settings.subaddressEmail = activeAccount.email; + } + + return settings; + }), + takeUntil(this.destroyed$), + ) + .subscribe((s) => { + this.settings.patchValue(s, { emitEvent: false }); + }); // the first emission is the current value; subsequent emissions are updates settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated); diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index 022f248aa1c..322fea94e3a 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -46,6 +46,7 @@