diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 7e2b802d346..8de3014fb2c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -8,7 +8,6 @@ import { AuthRequestServiceAbstraction, AuthRequestService, LoginEmailServiceAbstraction, - LoginEmailService, LogoutReason, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; @@ -708,8 +707,6 @@ export default class MainBackground { this.stateProvider, ); - this.loginEmailService = new LoginEmailService(this.stateProvider); - this.ssoLoginService = new SsoLoginService(this.stateProvider); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -1255,9 +1252,6 @@ export default class MainBackground { clearCaches(); if (userId == null) { - this.loginEmailService.setRememberEmail(false); - await this.loginEmailService.saveEmailSettings(); - await this.refreshBadge(); await this.refreshMenu(); await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts diff --git a/apps/desktop/desktop_native/napi/build.js b/apps/desktop/desktop_native/napi/build.js index a6fc633afd4..6c92dbad1b6 100644 --- a/apps/desktop/desktop_native/napi/build.js +++ b/apps/desktop/desktop_native/napi/build.js @@ -14,6 +14,8 @@ switch (process.platform) { default: targets = ['x86_64-unknown-linux-musl']; + process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; + process.env["PKG_CONFIG_ALL_STATIC"] = "1"; break; } diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 661d47a5a27..a0dde225233 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -24,7 +24,7 @@ "**/node_modules/argon2/package.json", "**/node_modules/argon2/build/Release/argon2.node" ], - "electronVersion": "30.1.2", + "electronVersion": "31.1.0", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 92cfebfd605..ff27dacd963 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -165,13 +165,10 @@ export class AccountSwitcherComponent { async addAccount() { this.close(); - this.loginEmailService.setRememberEmail(false); - await this.loginEmailService.saveEmailSettings(); - - await this.router.navigate(["/login"]); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); await this.stateService.clearDecryptedData(activeAccount?.id as UserId); await this.accountService.switchAccount(null); + await this.router.navigate(["/login"]); } private async createInactiveAccounts(baseAccounts: { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 80dbf40cb87..f2295f2cdd8 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -291,8 +291,7 @@ export class Main { }); }, (e: any) => { - // eslint-disable-next-line - console.error(e); + this.logService.error("Error while running migrations:", e); }, ); } diff --git a/apps/desktop/src/platform/services/electron-storage.service.ts b/apps/desktop/src/platform/services/electron-storage.service.ts index dc145fc2f7e..3fa9b2220c5 100644 --- a/apps/desktop/src/platform/services/electron-storage.service.ts +++ b/apps/desktop/src/platform/services/electron-storage.service.ts @@ -82,9 +82,14 @@ export class ElectronStorageService implements AbstractStorageService { } save(key: string, obj: unknown): Promise { + if (obj === undefined) { + return this.remove(key); + } + if (obj instanceof Set) { obj = Array.from(obj); } + this.store.set(key, obj); this.updatesSubject.next({ key, updateType: "save" }); return Promise.resolve(); diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index 9ff596181e3..7c86ac28498 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -71,6 +71,11 @@ type GroupDetailsRow = { collectionNames?: string[]; }; +/** + * @deprecated To be replaced with NewGroupsComponent which significantly refactors this component. + * The GroupsComponentRefactor flag switches between the old and new components; this component will be removed when + * the feature flag is removed. + */ @Component({ selector: "app-org-groups", templateUrl: "groups.component.html", diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html new file mode 100644 index 00000000000..3e659e5b6a8 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html @@ -0,0 +1,109 @@ + + + + + + + + {{ "loading" | i18n }} + + +

{{ "noGroupsInList" | i18n }}

+ + + + + + + + + + {{ "name" | i18n }} + {{ "collections" | i18n }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts new file mode 100644 index 00000000000..e5e99333e64 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts @@ -0,0 +1,255 @@ +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { + BehaviorSubject, + combineLatest, + concatMap, + from, + lastValueFrom, + map, + switchMap, + tap, +} from "rxjs"; +import { debounceTime, first } from "rxjs/operators"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; +import { Collection } from "@bitwarden/common/vault/models/domain/collection"; +import { + CollectionDetailsResponse, + CollectionResponse, +} from "@bitwarden/common/vault/models/response/collection.response"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; + +import { InternalGroupService as GroupService, GroupView } from "../core"; + +import { + GroupAddEditDialogResultType, + GroupAddEditTabType, + openGroupAddEditDialog, +} from "./group-add-edit.component"; + +type GroupDetailsRow = { + /** + * Details used for displaying group information + */ + details: GroupView; + + /** + * True if the group is selected in the table + */ + checked?: boolean; + + /** + * A list of collection names the group has access to + */ + collectionNames?: string[]; +}; + +/** + * Custom filter predicate that filters the groups table by id and name only. + * This is required because the default implementation searches by all properties, which can unintentionally match + * with members' names (who are assigned to the group) or collection names (which the group has access to). + */ +const groupsFilter = (filter: string) => { + const transformedFilter = filter.trim().toLowerCase(); + return (data: GroupDetailsRow) => { + const group = data.details; + + return ( + group.id.toLowerCase().indexOf(transformedFilter) != -1 || + group.name.toLowerCase().indexOf(transformedFilter) != -1 + ); + }; +}; + +@Component({ + templateUrl: "new-groups.component.html", +}) +export class NewGroupsComponent { + loading = true; + organizationId: string; + + protected dataSource = new TableDataSource(); + protected searchControl = new FormControl(""); + + // Fixed sizes used for cdkVirtualScroll + protected rowHeight = 46; + protected rowHeightClass = `tw-h-[46px]`; + + protected ModalTabType = GroupAddEditTabType; + private refreshGroups$ = new BehaviorSubject(null); + + constructor( + private apiService: ApiService, + private groupService: GroupService, + private route: ActivatedRoute, + private i18nService: I18nService, + private dialogService: DialogService, + private logService: LogService, + private collectionService: CollectionService, + private toastService: ToastService, + ) { + this.route.params + .pipe( + tap((params) => (this.organizationId = params.organizationId)), + switchMap(() => + combineLatest([ + // collectionMap + from(this.apiService.getCollections(this.organizationId)).pipe( + concatMap((response) => this.toCollectionMap(response)), + ), + // groups + this.refreshGroups$.pipe( + switchMap(() => this.groupService.getAll(this.organizationId)), + ), + ]), + ), + map(([collectionMap, groups]) => { + return groups.map((g) => ({ + id: g.id, + name: g.name, + details: g, + checked: false, + collectionNames: g.collections + .map((c) => collectionMap[c.id]?.name) + .sort(this.i18nService.collator?.compare), + })); + }), + takeUntilDestroyed(), + ) + .subscribe((groups) => { + this.dataSource.data = groups; + this.loading = false; + }); + + // Connect the search input to the table dataSource filter input + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((v) => (this.dataSource.filter = groupsFilter(v))); + + this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => { + this.searchControl.setValue(qParams.search); + }); + } + + async edit( + group: GroupDetailsRow, + startingTabIndex: GroupAddEditTabType = GroupAddEditTabType.Info, + ) { + const dialogRef = openGroupAddEditDialog(this.dialogService, { + data: { + initialTab: startingTabIndex, + organizationId: this.organizationId, + groupId: group != null ? group.details.id : null, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result == GroupAddEditDialogResultType.Saved) { + this.refreshGroups$.next(); + } else if (result == GroupAddEditDialogResultType.Deleted) { + this.removeGroup(group); + } + } + + async add() { + await this.edit(null); + } + + async delete(groupRow: GroupDetailsRow) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: groupRow.details.name, + content: { key: "deleteGroupConfirmation" }, + type: "warning", + }); + if (!confirmed) { + return false; + } + + try { + await this.groupService.delete(this.organizationId, groupRow.details.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedGroupId", groupRow.details.name), + }); + this.removeGroup(groupRow); + } catch (e) { + this.logService.error(e); + } + } + + async deleteAllSelected() { + const groupsToDelete = this.dataSource.data.filter((g) => g.checked); + + if (groupsToDelete.length == 0) { + return; + } + + const deleteMessage = groupsToDelete.map((g) => g.details.name).join(", "); + const confirmed = await this.dialogService.openSimpleDialog({ + title: { + key: "deleteMultipleGroupsConfirmation", + placeholders: [groupsToDelete.length.toString()], + }, + content: deleteMessage, + type: "warning", + }); + if (!confirmed) { + return false; + } + + try { + await this.groupService.deleteMany( + this.organizationId, + groupsToDelete.map((g) => g.details.id), + ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedManyGroups", groupsToDelete.length.toString()), + }); + + groupsToDelete.forEach((g) => this.removeGroup(g)); + } catch (e) { + this.logService.error(e); + } + } + + check(groupRow: GroupDetailsRow) { + groupRow.checked = !groupRow.checked; + } + + toggleAllVisible(event: Event) { + this.dataSource.filteredData.forEach( + (g) => (g.checked = (event.target as HTMLInputElement).checked), + ); + } + + private removeGroup(groupRow: GroupDetailsRow) { + // Assign a new array to dataSource.data to trigger the setters and update the table + this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow); + } + + private async toCollectionMap(response: ListResponse) { + const collections = response.data.map( + (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)), + ); + const decryptedCollections = await this.collectionService.decryptMany(collections); + + // Convert to an object using collection Ids as keys for faster name lookups + const collectionMap: Record = {}; + decryptedCollections.forEach((c) => (collectionMap[c.id] = c)); + + return collectionMap; + } +} diff --git a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts index 2959601c10a..7427bbb481d 100644 --- a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts @@ -2,6 +2,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { AuthGuard } from "@bitwarden/angular/auth/guards"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessOrgAdmin, canAccessGroupsTab, @@ -11,11 +12,13 @@ import { canAccessSettingsTab, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard"; import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component"; import { GroupsComponent } from "../../admin-console/organizations/manage/groups.component"; +import { NewGroupsComponent } from "../../admin-console/organizations/manage/new-groups.component"; import { deepLinkGuard } from "../../auth/guards/deep-link.guard"; import { VaultModule } from "../../vault/org-vault/vault.module"; @@ -46,14 +49,18 @@ const routes: Routes = [ path: "members", loadChildren: () => import("./members").then((m) => m.MembersModule), }, - { - path: "groups", - component: GroupsComponent, - canActivate: [organizationPermissionsGuard(canAccessGroupsTab)], - data: { - titleId: "groups", + ...featureFlaggedRoute({ + defaultComponent: GroupsComponent, + flaggedComponent: NewGroupsComponent, + featureFlag: FeatureFlag.GroupsComponentRefactor, + routeOptions: { + path: "groups", + canActivate: [organizationPermissionsGuard(canAccessGroupsTab)], + data: { + titleId: "groups", + }, }, - }, + }), { path: "reporting", loadChildren: () => diff --git a/apps/web/src/app/admin-console/organizations/organization.module.ts b/apps/web/src/app/admin-console/organizations/organization.module.ts index 29a7139231e..79f3a8e5f7d 100644 --- a/apps/web/src/app/admin-console/organizations/organization.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization.module.ts @@ -1,3 +1,4 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; import { NgModule } from "@angular/core"; import { LooseComponentsModule } from "../../shared"; @@ -5,6 +6,7 @@ import { LooseComponentsModule } from "../../shared"; import { CoreOrganizationModule } from "./core"; import { GroupAddEditComponent } from "./manage/group-add-edit.component"; import { GroupsComponent } from "./manage/groups.component"; +import { NewGroupsComponent } from "./manage/new-groups.component"; import { OrganizationsRoutingModule } from "./organization-routing.module"; import { SharedOrganizationModule } from "./shared"; import { AccessSelectorModule } from "./shared/components/access-selector"; @@ -16,7 +18,8 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; CoreOrganizationModule, OrganizationsRoutingModule, LooseComponentsModule, + ScrollingModule, ], - declarations: [GroupsComponent, GroupAddEditComponent], + declarations: [GroupsComponent, NewGroupsComponent, GroupAddEditComponent], }) export class OrganizationModule {} diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index d18f5125fda..4e9180ef123 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -22,11 +22,21 @@ const routes: Routes = [ canActivate: [organizationRedirectGuard(getSettingsRoute)], children: [], // This is required to make the auto redirect work, }, - { path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } }, + { + path: "account", + component: AccountComponent, + canActivate: [organizationPermissionsGuard((o) => o.isOwner)], + data: { + titleId: "organizationInfo", + }, + }, { path: "two-factor", component: TwoFactorSetupComponent, - data: { titleId: "twoStepLogin" }, + canActivate: [organizationPermissionsGuard((o) => o.use2fa && o.isOwner)], + data: { + titleId: "twoStepLogin", + }, }, { path: "policies", diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8462697973c..60b5b313ac0 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8511,14 +8511,18 @@ "downloadCSV": { "message": "Download CSV" }, - "billingHistoryDescription": { - "message": "Download a CSV to obtain client details for each billing date. Prorated charges are not included in the CSV and may vary from the linked invoice. For the most accurate billing details, refer to your monthly invoices.", - "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." - }, "monthlySubscriptionUserSeatsMessage": { "message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. " }, "annualSubscriptionUserSeatsMessage": { "message": "Adjustments to your subscription will result in prorated charges on a monthly billing cycle. " + }, + "billingHistoryDescription": { + "message": "Download a CSV to obtain client details for each billing date. Prorated charges are not included in the CSV and may vary from the linked invoice. For the most accurate billing details, refer to your monthly invoices.", + "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." + }, + "noInvoicesToList": { + "message": "There are no invoices to list", + "description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations." } } 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 46adf44d5a3..d17c973181f 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 @@ -19,8 +19,8 @@ import { ProviderPaymentMethodComponent, ProviderSelectPaymentMethodDialogComponent, ProviderSubscriptionComponent, + ProviderSubscriptionStatusComponent, } from "../../billing/providers"; -import { ProviderSubscriptionStatusComponent } from "../../billing/providers/subscription/provider-subscription-status.component"; import { AddOrganizationComponent } from "./clients/add-organization.component"; import { ClientsComponent } from "./clients/clients.component"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html index 8a22e964d43..c4b5ec40462 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/create-client-dialog.component.html @@ -17,6 +17,7 @@ *ngFor="let planCard of planCards" [ngClass]="getPlanCardContainerClasses(planCard.selected)" (click)="selectPlan(planCard.name)" + tabindex="0" >
{{ "selected" | i18n }}
-
+

{{ planCard.name }}

{{ planCard.cost | currency: "$" }} - /{{ "monthPerMember" | i18n }} + / {{ "monthPerMember" | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html index 2468f9df1af..caf07e49734 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html @@ -28,8 +28,8 @@ {{ "client" | i18n }} {{ "assigned" | i18n }} - {{ "used" | i18n }} - {{ "remaining" | i18n }} + {{ "used" | i18n }} + {{ "remaining" | i18n }} {{ "billingPlan" | i18n }} @@ -47,18 +47,18 @@ - {{ client.seats }} + {{ client.seats }} - {{ client.userCount }} + {{ client.assignedSeats }} - {{ client.seats - client.userCount }} + {{ client.remainingSeats }} - - {{ client.plan }} + + {{ removeMonthly(client.plan) }} - +