Merge branch 'main' into auth/pm-7392/token-service-add-secure-storage-fallback

This commit is contained in:
Jared Snider 2024-05-14 13:12:53 -04:00 committed by GitHub
commit eaec61dd4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 442 additions and 103 deletions

View File

@ -3,7 +3,7 @@ import { mock } from "jest-mock-extended";
import {
AssertCredentialResult,
CreateCredentialResult,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
export function createCredentialCreationOptionsMock(
customFields: Partial<CredentialCreationOptions> = {},

View File

@ -79,6 +79,9 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -106,6 +109,8 @@ import { DefaultConfigService } from "@bitwarden/common/platform/services/config
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service";
import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service";
import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
@ -158,9 +163,6 @@ import { UserId } from "@bitwarden/common/types/guid";
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/common/vault/abstractions/collection.service";
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/vault/abstractions/fido2/fido2-authenticator.service.abstraction";
import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/vault/abstractions/fido2/fido2-user-interface.service.abstraction";
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@ -171,8 +173,6 @@ import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwar
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/services/collection.service";
import { Fido2AuthenticatorService } from "@bitwarden/common/vault/services/fido2/fido2-authenticator.service";
import { Fido2ClientService } from "@bitwarden/common/vault/services/fido2/fido2-client.service";
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";

View File

@ -3,7 +3,7 @@ import {
AssertCredentialResult,
CreateCredentialParams,
CreateCredentialResult,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
type SharedFido2ScriptInjectionDetails = {
runAt: browser.contentScripts.RegisteredContentScriptOptions["runAt"];

View File

@ -1,13 +1,13 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
AssertCredentialParams,
CreateCredentialParams,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Fido2ClientService } from "@bitwarden/common/platform/services/fido2/fido2-client.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { Fido2ClientService } from "@bitwarden/common/vault/services/fido2/fido2-client.service";
import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks";
import {

View File

@ -1,14 +1,14 @@
import { firstValueFrom, startWith } from "rxjs";
import { pairwise } from "rxjs/operators";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import {
AssertCredentialParams,
AssertCredentialResult,
CreateCredentialParams,
CreateCredentialResult,
Fido2ClientService,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { BrowserApi } from "../../../platform/browser/browser-api";

View File

@ -16,14 +16,14 @@ import {
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserRequestedFallbackAbortReason } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
import { UserRequestedFallbackAbortReason } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
Fido2UserInterfaceSession,
NewCredentialParams,
PickCredentialParams,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-user-interface.service.abstraction";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../../platform/browser/browser-api";
import { closeFido2Popout, openFido2Popout } from "../popup/utils/vault-popout-window";

View File

@ -1,6 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { CreateCredentialResult } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
import { CreateCredentialResult } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks";
import { triggerPortOnDisconnectEvent } from "../../../autofill/spec/testing-utils";

View File

@ -1,7 +1,7 @@
import {
AssertCredentialParams,
CreateCredentialParams,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { sendExtensionMessage } from "../../../autofill/utils";
import { Fido2PortName } from "../enums/fido2-port-name.enum";

View File

@ -3,7 +3,7 @@ import {
CreateCredentialResult,
AssertCredentialParams,
AssertCredentialResult,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
export enum MessageType {
CredentialCreationRequest,

View File

@ -1,4 +1,4 @@
import { FallbackRequestedError } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { Message, MessageType } from "./message";

View File

@ -1,4 +1,4 @@
import { FallbackRequestedError } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { WebauthnUtils } from "../webauthn-utils";

View File

@ -1,8 +1,8 @@
import {
CreateCredentialResult,
AssertCredentialResult,
} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction";
import { Fido2Utils } from "@bitwarden/common/vault/services/fido2/fido2-utils";
} from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction";
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
import {
InsecureAssertCredentialParams,

View File

@ -3,7 +3,6 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angula
import { AbstractControl, FormBuilder, Validators } from "@angular/forms";
import {
combineLatest,
from,
map,
Observable,
of,
@ -23,7 +22,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { BitValidators, DialogService } from "@bitwarden/components";
@ -56,7 +54,10 @@ export interface CollectionDialogParams {
initialTab?: CollectionDialogTabType;
parentCollectionId?: string;
showOrgSelector?: boolean;
collectionIds?: string[];
/**
* Flag to limit the nested collections to only those the user has explicit CanManage access too.
*/
limitNestedCollections?: boolean;
readonly?: boolean;
}
@ -85,7 +86,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
protected tabIndex: CollectionDialogTabType;
protected loading = true;
protected organization?: Organization;
protected collection?: CollectionView;
protected collection?: CollectionAdminView;
protected nestOptions: CollectionView[] = [];
protected accessItems: AccessItemView[] = [];
protected deletedParentName: string | undefined;
@ -107,7 +108,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService,
private groupService: GroupService,
private collectionAdminService: CollectionAdminService,
private collectionService: CollectionService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private organizationUserService: OrganizationUserService,
@ -124,7 +124,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
this.showOrgSelector = true;
this.formGroup.controls.selectedOrg.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((id) => this.loadOrg(id, this.params.collectionIds));
.subscribe((id) => this.loadOrg(id));
this.organizations$ = this.organizationService.organizations$.pipe(
first(),
map((orgs) =>
@ -138,11 +138,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
} else {
// Opened from the org vault
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
await this.loadOrg(this.params.organizationId, this.params.collectionIds);
await this.loadOrg(this.params.organizationId);
}
}
async loadOrg(orgId: string, collectionIds: string[]) {
async loadOrg(orgId: string) {
const organization$ = this.organizationService
.get$(orgId)
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
@ -158,28 +158,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
combineLatest({
organization: organization$,
collections: this.collectionAdminService.getAll(orgId),
collectionDetails: this.params.collectionId
? from(this.collectionAdminService.get(orgId, this.params.collectionId))
: of(null),
groups: groups$,
// Collection(s) needed to map readonlypermission for (potential) access selector disabled state
users: this.organizationUserService.getAllUsers(orgId, { includeCollections: true }),
collection: this.params.collectionId
? this.collectionService.get(this.params.collectionId)
: of(null),
flexibleCollectionsV1: this.flexibleCollectionsV1Enabled$,
})
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
.subscribe(
({
organization,
collections,
collectionDetails,
groups,
users,
collection,
flexibleCollectionsV1,
}) => {
({ organization, collections: allCollections, groups, users, flexibleCollectionsV1 }) => {
this.organization = organization;
this.accessItems = [].concat(
groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)),
@ -189,37 +175,48 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
// Force change detection to update the access selector's items
this.changeDetectorRef.detectChanges();
if (collectionIds) {
collections = collections.filter((c) => collectionIds.includes(c.id));
}
this.nestOptions = this.params.limitNestedCollections
? allCollections.filter((c) => c.manage)
: allCollections;
if (this.params.collectionId) {
this.collection = collections.find((c) => c.id === this.collectionId);
this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
this.collection = allCollections.find((c) => c.id === this.collectionId);
// Ensure we don't allow nesting the current collection within itself
this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId);
if (!this.collection) {
throw new Error("Could not find collection to edit.");
}
const { name, parent } = parseName(this.collection);
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
this.deletedParentName = parent;
// Parse the name to find its parent name
const { name, parent: parentName } = parseName(this.collection);
// Determine if the user can see/select the parent collection
if (parentName !== undefined) {
if (
this.organization.canViewAllCollections &&
!allCollections.find((c) => c.name === parentName)
) {
// The user can view all collections, but the parent was not found -> assume it has been deleted
this.deletedParentName = parentName;
} else if (!this.nestOptions.find((c) => c.name === parentName)) {
// We cannot find the current parent collection in our list of options, so add a placeholder
this.nestOptions.unshift({ name: parentName } as CollectionView);
}
}
const accessSelections = mapToAccessSelections(collectionDetails);
const accessSelections = mapToAccessSelections(this.collection);
this.formGroup.patchValue({
name,
externalId: this.collection.externalId,
parent,
parent: parentName,
access: accessSelections,
});
this.collection.manage = collection?.manage ?? false; // Get manage flag from sync data collection
this.showDeleteButton =
!this.dialogReadonly &&
this.collection.canDelete(organization, flexibleCollectionsV1);
} else {
this.nestOptions = collections;
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
const parent = this.nestOptions.find((c) => c.id === this.params.parentCollectionId);
const currentOrgUserId = users.data.find(
(u) => u.userId === this.organization?.userId,
)?.id;

View File

@ -650,7 +650,7 @@ export class VaultComponent implements OnInit, OnDestroy {
.sort(Utils.getSortFunction(this.i18nService, "name"))[0].id,
parentCollectionId: this.filter.collectionId,
showOrgSelector: true,
collectionIds: this.allCollections.map((c) => c.id),
limitNestedCollections: true,
},
});
const result = await lastValueFrom(dialog.closed);
@ -666,7 +666,12 @@ export class VaultComponent implements OnInit, OnDestroy {
async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise<void> {
const dialog = openCollectionDialog(this.dialogService, {
data: { collectionId: c?.id, organizationId: c.organizationId, initialTab: tab },
data: {
collectionId: c?.id,
organizationId: c.organizationId,
initialTab: tab,
limitNestedCollections: true,
},
});
const result = await lastValueFrom(dialog.closed);

View File

@ -3,6 +3,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
import { SharedModule } from "../../shared";
import { CollectionDialogTabType } from "../components/collection-dialog";
const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="10 -10 120 140" fill="none">
<rect class="tw-stroke-secondary-600" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/>
@ -16,24 +17,36 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height=
template: `<bit-no-items [icon]="icon" class="tw-mt-2 tw-block">
<span slot="title" class="tw-mt-4 tw-block">{{ "collectionAccessRestricted" | i18n }}</span>
<button
*ngIf="canEditCollection"
slot="button"
bitButton
(click)="viewCollectionClicked.emit()"
(click)="viewCollectionClicked.emit({ readonly: false, tab: collectionDialogTabType.Info })"
buttonType="secondary"
type="button"
>
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ buttonText | i18n }}
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "editCollection" | i18n }}
</button>
<button
*ngIf="!canEditCollection && canViewCollectionInfo"
slot="button"
bitButton
(click)="viewCollectionClicked.emit({ readonly: true, tab: collectionDialogTabType.Access })"
buttonType="secondary"
type="button"
>
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "viewAccess" | i18n }}
</button>
</bit-no-items>`,
})
export class CollectionAccessRestrictedComponent {
protected icon = icon;
protected collectionDialogTabType = CollectionDialogTabType;
@Input() canEditCollection = false;
@Input() canViewCollectionInfo = false;
@Output() viewCollectionClicked = new EventEmitter<void>();
get buttonText() {
return this.canEditCollection ? "editCollection" : "viewCollection";
}
@Output() viewCollectionClicked = new EventEmitter<{
readonly: boolean;
tab: CollectionDialogTabType;
}>();
}

View File

@ -116,14 +116,18 @@
</bit-no-items>
<collection-access-restricted
*ngIf="showCollectionAccessRestricted"
[canEditCollection]="organization.isProviderUser"
(viewCollectionClicked)="
editCollection(
selectedCollection.node,
CollectionDialogTabType.Info,
!organization.isProviderUser
[canEditCollection]="
selectedCollection?.node?.canEdit(organization, flexibleCollectionsV1Enabled)
"
[canViewCollectionInfo]="
selectedCollection?.node?.canViewCollectionInfo(
organization,
flexibleCollectionsV1Enabled
)
"
(viewCollectionClicked)="
editCollection(selectedCollection.node, $event.tab, $event.readonly)
"
>
</collection-access-restricted>
</ng-container>

View File

@ -1175,6 +1175,9 @@ export class VaultComponent implements OnInit, OnDestroy {
data: {
organizationId: this.organization?.id,
parentCollectionId: this.selectedCollection?.node.id,
limitNestedCollections: !this.organization.canEditAnyCollection(
this.flexibleCollectionsV1Enabled,
),
},
});
@ -1198,6 +1201,9 @@ export class VaultComponent implements OnInit, OnDestroy {
organizationId: this.organization?.id,
initialTab: tab,
readonly: readonly,
limitNestedCollections: !this.organization.canEditAnyCollection(
this.flexibleCollectionsV1Enabled,
),
},
});

View File

@ -7749,9 +7749,6 @@
"success": {
"message": "Success"
},
"viewCollection": {
"message": "View collection"
},
"restrictedGroupAccess": {
"message": "You cannot add yourself to groups."
},
@ -8198,5 +8195,26 @@
},
"viewAccess": {
"message": "View access"
},
"updateName": {
"message": "Update name"
},
"updatedOrganizationName": {
"message": "Updated organization name"
},
"providerPlan": {
"message": "Managed Service Provider"
},
"orgSeats": {
"message": "Organization Seats"
},
"providerDiscount": {
"message": "$AMOUNT$% Discount",
"placeholders": {
"amount": {
"content": "$1",
"example": "2"
}
}
}
}

View File

@ -12,8 +12,9 @@ import { OssModule } from "@bitwarden/web-vault/app/oss.module";
import { ProviderSubscriptionComponent } from "../../billing/providers";
import {
CreateClientOrganizationComponent,
ManageClientOrganizationSubscriptionComponent,
ManageClientOrganizationsComponent,
ManageClientOrganizationNameComponent,
ManageClientOrganizationSubscriptionComponent,
} from "../../billing/providers/clients";
import { AddOrganizationComponent } from "./clients/add-organization.component";
@ -62,6 +63,7 @@ import { SetupComponent } from "./setup/setup.component";
UserAddEditComponent,
CreateClientOrganizationComponent,
ManageClientOrganizationsComponent,
ManageClientOrganizationNameComponent,
ManageClientOrganizationSubscriptionComponent,
ProviderSubscriptionComponent,
],

View File

@ -1,3 +1,4 @@
export * from "./create-client-organization.component";
export * from "./manage-client-organizations.component";
export * from "./manage-client-organization-name.component";
export * from "./manage-client-organization-subscription.component";

View File

@ -0,0 +1,24 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog>
<span bitDialogTitle class="tw-font-semibold">
{{ "updateName" | i18n }}
<small class="tw-text-muted">{{ dialogParams.organization.name }}</small>
</span>
<div bitDialogContent>
<bit-form-field>
<bit-label>
{{ "organizationName" | i18n }}
</bit-label>
<input type="text" bitInput formControlName="name" />
</bit-form-field>
</div>
<ng-container bitDialogFooter>
<button bitButton bitFormButton buttonType="primary" type="submit">
{{ "save" | i18n }}
</button>
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@ -0,0 +1,77 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { UpdateClientOrganizationRequest } from "@bitwarden/common/billing/models/request/update-client-organization.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService } from "@bitwarden/components";
type ManageClientOrganizationNameParams = {
providerId: string;
organization: {
id: string;
name: string;
seats: number;
};
};
export enum ManageClientOrganizationNameResultType {
Closed = "closed",
Submitted = "submitted",
}
export const openManageClientOrganizationNameDialog = (
dialogService: DialogService,
dialogConfig: DialogConfig<ManageClientOrganizationNameParams>,
) =>
dialogService.open<ManageClientOrganizationNameResultType, ManageClientOrganizationNameParams>(
ManageClientOrganizationNameComponent,
dialogConfig,
);
@Component({
selector: "app-manage-client-organization-name",
templateUrl: "manage-client-organization-name.component.html",
})
export class ManageClientOrganizationNameComponent {
protected ResultType = ManageClientOrganizationNameResultType;
protected formGroup = this.formBuilder.group({
name: [this.dialogParams.organization.name, Validators.required],
});
constructor(
@Inject(DIALOG_DATA) protected dialogParams: ManageClientOrganizationNameParams,
private billingApiService: BillingApiServiceAbstraction,
private dialogRef: DialogRef<ManageClientOrganizationNameResultType>,
private formBuilder: FormBuilder,
private i18nService: I18nService,
private toastService: ToastService,
) {}
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
const request = new UpdateClientOrganizationRequest();
request.assignedSeats = this.dialogParams.organization.seats;
request.name = this.formGroup.value.name;
await this.billingApiService.updateClientOrganization(
this.dialogParams.providerId,
this.dialogParams.organization.id,
request,
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("updatedOrganizationName"),
});
this.dialogRef.close(this.ResultType.Submitted);
};
}

View File

@ -71,6 +71,7 @@ export class ManageClientOrganizationSubscriptionComponent implements OnInit {
const request = new UpdateClientOrganizationRequest();
request.assignedSeats = assignedSeats;
request.name = this.clientName;
await this.billingApiService.updateClientOrganization(
this.providerId,

View File

@ -78,8 +78,12 @@
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="manageName(client)">
<i aria-hidden="true" class="bwi bwi-pencil-square"></i>
{{ "updateName" | i18n }}
</button>
<button type="button" bitMenuItem (click)="manageSubscription(client)">
<i aria-hidden="true" class="bwi bwi-question-circle"></i>
<i aria-hidden="true" class="bwi bwi-family"></i>
{{ "manageSubscription" | i18n }}
</button>
<button type="button" bitMenuItem (click)="remove(client)">

View File

@ -24,6 +24,10 @@ import {
CreateClientOrganizationResultType,
openCreateClientOrganizationDialog,
} from "./create-client-organization.component";
import {
ManageClientOrganizationNameResultType,
openManageClientOrganizationNameDialog,
} from "./manage-client-organization-name.component";
import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component";
@Component({
@ -106,6 +110,25 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
this.loading = false;
}
async manageName(organization: ProviderOrganizationOrganizationDetailsResponse) {
const dialogRef = openManageClientOrganizationNameDialog(this.dialogService, {
data: {
providerId: this.providerId,
organization: {
id: organization.id,
name: organization.organizationName,
seats: organization.seats,
},
},
});
const result = await firstValueFrom(dialogRef.closed);
if (result === ManageClientOrganizationNameResultType.Submitted) {
await this.load();
}
}
async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) {
if (organization == null) {
return;
@ -135,4 +158,6 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
await this.load();
};
protected readonly openManageClientOrganizationNameDialog =
openManageClientOrganizationNameDialog;
}

View File

@ -1 +1,83 @@
<app-header></app-header>
<bit-container>
<ng-container *ngIf="!firstLoaded && loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="subscription && firstLoaded">
<bit-callout type="warning" title="{{ 'canceled' | i18n }}" *ngIf="false">
{{ "subscriptionCanceled" | i18n }}</bit-callout
>
<dl class="tw-grid tw-grid-flow-col tw-grid-rows-2">
<dt>{{ "billingPlan" | i18n }}</dt>
<dd>{{ "providerPlan" | i18n }}</dd>
<ng-container *ngIf="subscription">
<dt>{{ "status" | i18n }}</dt>
<dd>
<span class="tw-capitalize">{{ subscription.status }}</span>
</dd>
<dt [ngClass]="{ 'tw-text-danger': isExpired }">{{ "nextCharge" | i18n }}</dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ subscription.currentPeriodEndDate | date: "mediumDate" }}
</dd>
</ng-container>
</dl>
</ng-container>
<ng-container>
<div class="tw-flex-col">
<strong class="tw-block tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 pb-2"
>{{ "details" | i18n }} &#160;<span
bitBadge
variant="success"
*ngIf="subscription.discountPercentage"
>{{ "providerDiscount" | i18n: subscription.discountPercentage }}</span
>
</strong>
<bit-table>
<ng-template body>
<ng-container *ngIf="subscription">
<tr bitRow *ngFor="let i of subscription.plans">
<td bitCell class="tw-pl-0 tw-py-3">
{{ getFormattedPlanName(i.planName) }} {{ "orgSeats" | i18n }} ({{
i.cadence.toLowerCase()
}}) {{ "&times;" }}{{ getFormattedSeatCount(i.seatMinimum, i.purchasedSeats) }}
@
{{
getFormattedCost(
i.cost,
i.seatMinimum,
i.purchasedSeats,
subscription.discountPercentage
) | currency: "$"
}}
</td>
<td bitCell class="tw-text-right tw-py-3">
{{ ((100 - subscription.discountPercentage) / 100) * i.cost | currency: "$" }} /{{
"month" | i18n
}}
<div>
<bit-hint class="tw-text-sm tw-line-through">
{{ i.cost | currency: "$" }} /{{ "month" | i18n }}
</bit-hint>
</div>
</td>
</tr>
<tr bitRow>
<td bitCell class="tw-pl-0 tw-py-3"></td>
<td bitCell class="tw-text-right">
<span class="tw-font-bold">Total:</span> {{ totalCost | currency: "$" }} /{{
"month" | i18n
}}
</td>
</tr>
</ng-container>
</ng-template>
</bit-table>
</div>
</ng-container>
</bit-container>

View File

@ -1,7 +1,86 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, concatMap, takeUntil } from "rxjs";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import {
Plans,
ProviderSubscriptionResponse,
} from "@bitwarden/common/billing/models/response/provider-subscription-response";
@Component({
selector: "app-provider-subscription",
templateUrl: "./provider-subscription.component.html",
})
export class ProviderSubscriptionComponent {}
export class ProviderSubscriptionComponent {
subscription: ProviderSubscriptionResponse;
providerId: string;
firstLoaded = false;
loading: boolean;
private destroy$ = new Subject<void>();
totalCost: number;
currentDate = new Date();
constructor(
private billingApiService: BillingApiServiceAbstraction,
private route: ActivatedRoute,
) {}
async ngOnInit() {
this.route.params
.pipe(
concatMap(async (params) => {
this.providerId = params.providerId;
await this.load();
this.firstLoaded = true;
}),
takeUntil(this.destroy$),
)
.subscribe();
}
get isExpired() {
return this.subscription.status !== "active";
}
async load() {
if (this.loading) {
return;
}
this.loading = true;
this.subscription = await this.billingApiService.getProviderSubscription(this.providerId);
this.totalCost =
((100 - this.subscription.discountPercentage) / 100) * this.sumCost(this.subscription.plans);
this.loading = false;
}
getFormattedCost(
cost: number,
seatMinimum: number,
purchasedSeats: number,
discountPercentage: number,
): number {
const costPerSeat = cost / (seatMinimum + purchasedSeats);
const discountedCost = costPerSeat - (costPerSeat * discountPercentage) / 100;
return discountedCost;
}
getFormattedPlanName(planName: string): string {
const spaceIndex = planName.indexOf(" ");
return planName.substring(0, spaceIndex);
}
getFormattedSeatCount(seatMinimum: number, purchasedSeats: number): string {
const totalSeats = seatMinimum + purchasedSeats;
return totalSeats > 1 ? totalSeats.toString() : "";
}
sumCost(plans: Plans[]): number {
return plans.reduce((acc, plan) => acc + plan.cost, 0);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -1,3 +1,4 @@
export class UpdateClientOrganizationRequest {
assignedSeats: number;
name: string;
}

View File

@ -2,8 +2,14 @@ import { TextEncoder } from "util";
import { mock, MockProxy } from "jest-mock-extended";
import { Utils } from "../../../platform/misc/utils";
import { CipherService } from "../../abstractions/cipher.service";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type";
import { CipherType } from "../../../vault/enums/cipher-type";
import { Cipher } from "../../../vault/models/domain/cipher";
import { CipherView } from "../../../vault/models/view/cipher.view";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import { LoginView } from "../../../vault/models/view/login.view";
import {
Fido2AuthenticatorErrorCode,
Fido2AuthenticatorGetAssertionParams,
@ -14,13 +20,7 @@ import {
Fido2UserInterfaceSession,
NewCredentialParams,
} from "../../abstractions/fido2/fido2-user-interface.service.abstraction";
import { SyncService } from "../../abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { Cipher } from "../../models/domain/cipher";
import { CipherView } from "../../models/view/cipher.view";
import { Fido2CredentialView } from "../../models/view/fido2-credential.view";
import { LoginView } from "../../models/view/login.view";
import { Utils } from "../../misc/utils";
import { CBOR } from "./cbor";
import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service";

View File

@ -1,6 +1,9 @@
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
import { CipherService } from "../../abstractions/cipher.service";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type";
import { CipherType } from "../../../vault/enums/cipher-type";
import { CipherView } from "../../../vault/models/view/cipher.view";
import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view";
import {
Fido2AlgorithmIdentifier,
Fido2AuthenticatorError,
@ -13,11 +16,8 @@ import {
PublicKeyCredentialDescriptor,
} from "../../abstractions/fido2/fido2-authenticator.service.abstraction";
import { Fido2UserInterfaceService } from "../../abstractions/fido2/fido2-user-interface.service.abstraction";
import { SyncService } from "../../abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { CipherView } from "../../models/view/cipher.view";
import { Fido2CredentialView } from "../../models/view/fido2-credential.view";
import { LogService } from "../../abstractions/log.service";
import { Utils } from "../../misc/utils";
import { CBOR } from "./cbor";
import { p1363ToDer } from "./ecdsa-utils";

View File

@ -4,8 +4,8 @@ import { of } from "rxjs";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { Utils } from "../../../platform/misc/utils";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { ConfigService } from "../../abstractions/config/config.service";
import {
Fido2AuthenticatorError,
Fido2AuthenticatorErrorCode,
@ -17,7 +17,7 @@ import {
CreateCredentialParams,
FallbackRequestedError,
} from "../../abstractions/fido2/fido2-client.service.abstraction";
import { VaultSettingsService } from "../../abstractions/vault-settings/vault-settings.service";
import { Utils } from "../../misc/utils";
import { Fido2AuthenticatorService } from "./fido2-authenticator.service";
import { Fido2ClientService } from "./fido2-client.service";

View File

@ -4,9 +4,8 @@ import { parse } from "tldts";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { ConfigService } from "../../../platform/abstractions/config/config.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
import { VaultSettingsService } from "../../../vault/abstractions/vault-settings/vault-settings.service";
import { ConfigService } from "../../abstractions/config/config.service";
import {
Fido2AuthenticatorError,
Fido2AuthenticatorErrorCode,
@ -26,7 +25,8 @@ import {
UserRequestedFallbackAbortReason,
UserVerification,
} from "../../abstractions/fido2/fido2-client.service.abstraction";
import { VaultSettingsService } from "../../abstractions/vault-settings/vault-settings.service";
import { LogService } from "../../abstractions/log.service";
import { Utils } from "../../misc/utils";
import { isValidRpId } from "./domain-utils";
import { Fido2Utils } from "./fido2-utils";