1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 17:23:37 +00:00

Merge branch 'main' into autofill/pm-5189-fix-issues-present-with-inline-menu-rendering-in-iframes

This commit is contained in:
Cesar Gonzalez
2024-04-17 09:03:39 -05:00
committed by GitHub
43 changed files with 974 additions and 540 deletions

View File

@@ -189,12 +189,12 @@ jobs:
path: browser-source/apps/browser/dist/dist-chrome.zip path: browser-source/apps/browser/dist/dist-chrome.zip
if-no-files-found: error if-no-files-found: error
# - name: Upload Chrome MV3 artifact - name: Upload Chrome MV3 artifact (DO NOT USE FOR PROD)
# uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
# with: with:
# name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
# path: browser-source/apps/browser/dist/dist-chrome-mv3.zip path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
# if-no-files-found: error if-no-files-found: error
- name: Upload Firefox artifact - name: Upload Firefox artifact
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1

View File

@@ -18,11 +18,13 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
@@ -232,6 +234,7 @@ export class Main {
stateEventRunnerService: StateEventRunnerService; stateEventRunnerService: StateEventRunnerService;
biometricStateService: BiometricStateService; biometricStateService: BiometricStateService;
billingAccountProfileStateService: BillingAccountProfileStateService; billingAccountProfileStateService: BillingAccountProfileStateService;
providerApiService: ProviderApiServiceAbstraction;
constructor() { constructor() {
let p = null; let p = null;
@@ -692,6 +695,8 @@ export class Main {
this.eventUploadService, this.eventUploadService,
this.authService, this.authService,
); );
this.providerApiService = new ProviderApiService(this.apiService);
} }
async run() { async run() {

View File

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

View File

@@ -31,7 +31,12 @@
</bit-tab> </bit-tab>
<bit-tab label="{{ 'members' | i18n }}"> <bit-tab label="{{ 'members' | i18n }}">
<p>{{ "editGroupMembersDesc" | i18n }}</p> <p>
{{ "editGroupMembersDesc" | i18n }}
<span *ngIf="restrictGroupAccess$ | async">
{{ "restrictedGroupAccessDesc" | i18n }}
</span>
</p>
<bit-access-selector <bit-access-selector
formControlName="members" formControlName="members"
[items]="members" [items]="members"

View File

@@ -1,15 +1,31 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { catchError, combineLatest, from, map, of, Subject, switchMap, takeUntil } from "rxjs"; import {
catchError,
combineLatest,
concatMap,
from,
map,
Observable,
of,
shareReplay,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { Collection } from "@bitwarden/common/vault/models/domain/collection";
@@ -88,10 +104,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
tabIndex: GroupAddEditTabType; tabIndex: GroupAddEditTabType;
loading = true; loading = true;
editMode = false;
title: string; title: string;
collections: AccessItemView[] = []; collections: AccessItemView[] = [];
members: AccessItemView[] = []; members: Array<AccessItemView & { userId: UserId }> = [];
group: GroupView; group: GroupView;
groupForm = this.formBuilder.group({ groupForm = this.formBuilder.group({
@@ -110,6 +125,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
return this.params.organizationId; return this.params.organizationId;
} }
protected get editMode(): boolean {
return this.groupId != null;
}
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private get orgCollections$() { private get orgCollections$() {
@@ -134,7 +153,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
); );
} }
private get orgMembers$() { private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe( return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
map((response) => map((response) =>
response.data.map((m) => ({ response.data.map((m) => ({
@@ -145,13 +164,15 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email, listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email,
labelName: m.name || m.email, labelName: m.name || m.email,
status: m.status, status: m.status,
userId: m.userId as UserId,
})), })),
), ),
); );
} }
private get groupDetails$() { private groupDetails$: Observable<GroupView | undefined> = of(this.editMode).pipe(
if (!this.editMode) { concatMap((editMode) => {
if (!editMode) {
return of(undefined); return of(undefined);
} }
@@ -172,7 +193,26 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
return of(undefined); return of(undefined);
}), }),
); );
} }),
shareReplay({ refCount: false }),
);
restrictGroupAccess$ = combineLatest([
this.organizationService.get$(this.organizationId),
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
this.groupDetails$,
]).pipe(
map(
([organization, flexibleCollectionsV1Enabled, group]) =>
// Feature flag conditionals
flexibleCollectionsV1Enabled &&
organization.flexibleCollections &&
// Business logic conditionals
!organization.allowAdminAccessToAllCollectionItems &&
group !== undefined,
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
constructor( constructor(
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
@@ -188,17 +228,25 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dialogService: DialogService, private dialogService: DialogService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private configService: ConfigService,
private accountService: AccountService,
) { ) {
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
} }
ngOnInit() { ngOnInit() {
this.editMode = this.loading = this.groupId != null; this.loading = true;
this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup"); this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup");
combineLatest([this.orgCollections$, this.orgMembers$, this.groupDetails$]) combineLatest([
this.orgCollections$,
this.orgMembers$,
this.groupDetails$,
this.restrictGroupAccess$,
this.accountService.activeAccount$,
])
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(([collections, members, group]) => { .subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => {
this.collections = collections; this.collections = collections;
this.members = members; this.members = members;
this.group = group; this.group = group;
@@ -224,6 +272,18 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
}); });
} }
// If the current user is not already in the group and cannot add themselves, remove them from the list
if (restrictGroupAccess) {
const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id;
const isAlreadyInGroup = this.groupForm.value.members.some(
(m) => m.id === organizationUserId,
);
if (!isAlreadyInGroup) {
this.members = this.members.filter((m) => m.id !== organizationUserId);
}
}
this.loading = false; this.loading = false;
}); });
} }

View File

@@ -31,6 +31,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service";
import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view";
import { import {
CollectionAccessSelectionView, CollectionAccessSelectionView,
GroupService, GroupService,
@@ -206,25 +207,52 @@ export class MemberDialogComponent implements OnDestroy {
collections: this.collectionAdminService.getAll(this.params.organizationId), collections: this.collectionAdminService.getAll(this.params.organizationId),
userDetails: userDetails$, userDetails: userDetails$,
groups: groups$, groups: groups$,
flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
false,
),
}) })
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(({ organization, collections, userDetails, groups }) => { .subscribe(
({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => {
this.setFormValidators(organization); this.setFormValidators(organization);
this.collectionAccessItems = [].concat( // Groups tab: populate available groups
collections.map((c) => mapCollectionToAccessItemView(c)),
);
this.groupAccessItems = [].concat( this.groupAccessItems = [].concat(
groups.map<AccessItemView>((g) => mapGroupToAccessItemView(g)), groups.map<AccessItemView>((g) => mapGroupToAccessItemView(g)),
); );
if (this.params.organizationUserId) { // Collections tab: Populate all available collections (including current user access where applicable)
this.loadOrganizationUser(userDetails, groups, collections); this.collectionAccessItems = collections
.map((c) =>
mapCollectionToAccessItemView(
c,
organization,
flexibleCollectionsV1Enabled,
userDetails == null
? undefined
: c.users.find((access) => access.id === userDetails.id),
),
)
// But remove collections that we can't assign access to, unless the user is already assigned
.filter(
(item) =>
!item.readonly || userDetails?.collections.some((access) => access.id == item.id),
);
if (userDetails != null) {
this.loadOrganizationUser(
userDetails,
groups,
collections,
organization,
flexibleCollectionsV1Enabled,
);
} }
this.loading = false; this.loading = false;
}); },
);
} }
private setFormValidators(organization: Organization) { private setFormValidators(organization: Organization) {
@@ -246,7 +274,9 @@ export class MemberDialogComponent implements OnDestroy {
private loadOrganizationUser( private loadOrganizationUser(
userDetails: OrganizationUserAdminView, userDetails: OrganizationUserAdminView,
groups: GroupView[], groups: GroupView[],
collections: CollectionView[], collections: CollectionAdminView[],
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
) { ) {
if (!userDetails) { if (!userDetails) {
throw new Error("Could not find user to edit."); throw new Error("Could not find user to edit.");
@@ -295,13 +325,22 @@ export class MemberDialogComponent implements OnDestroy {
}), }),
); );
// Populate additional collection access via groups (rendered as separate rows from user access)
this.collectionAccessItems = this.collectionAccessItems.concat( this.collectionAccessItems = this.collectionAccessItems.concat(
collectionsFromGroups.map(({ collection, accessSelection, group }) => collectionsFromGroups.map(({ collection, accessSelection, group }) =>
mapCollectionToAccessItemView(collection, accessSelection, group), mapCollectionToAccessItemView(
collection,
organization,
flexibleCollectionsV1Enabled,
accessSelection,
group,
),
), ),
); );
const accessSelections = mapToAccessSelections(userDetails); // Set current collections and groups the user has access to (excluding collections the current user doesn't have
// permissions to change - they are included as readonly via the CollectionAccessItems)
const accessSelections = mapToAccessSelections(userDetails, this.collectionAccessItems);
const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups);
this.formGroup.removeControl("emails"); this.formGroup.removeControl("emails");
@@ -573,6 +612,8 @@ export class MemberDialogComponent implements OnDestroy {
function mapCollectionToAccessItemView( function mapCollectionToAccessItemView(
collection: CollectionView, collection: CollectionView,
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
accessSelection?: CollectionAccessSelectionView, accessSelection?: CollectionAccessSelectionView,
group?: GroupView, group?: GroupView,
): AccessItemView { ): AccessItemView {
@@ -581,7 +622,8 @@ function mapCollectionToAccessItemView(
id: group ? `${collection.id}-${group.id}` : collection.id, id: group ? `${collection.id}-${group.id}` : collection.id,
labelName: collection.name, labelName: collection.name,
listName: collection.name, listName: collection.name,
readonly: group !== undefined, readonly:
group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled),
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
viaGroupName: group?.name, viaGroupName: group?.name,
}; };
@@ -596,16 +638,23 @@ function mapGroupToAccessItemView(group: GroupView): AccessItemView {
}; };
} }
function mapToAccessSelections(user: OrganizationUserAdminView): AccessItemValue[] { function mapToAccessSelections(
user: OrganizationUserAdminView,
items: AccessItemView[],
): AccessItemValue[] {
if (user == undefined) { if (user == undefined) {
return []; return [];
} }
return [].concat(
user.collections.map<AccessItemValue>((selection) => ({ return (
user.collections
// The FormControl value only represents editable collection access - exclude readonly access selections
.filter((selection) => !items.find((item) => item.id == selection.id).readonly)
.map<AccessItemValue>((selection) => ({
id: selection.id, id: selection.id,
type: AccessItemType.Collection, type: AccessItemType.Collection,
permission: convertToPermission(selection), permission: convertToPermission(selection),
})), }))
); );
} }

View File

@@ -0,0 +1,34 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "deleteProvider" | i18n }}</p>
<div class="card">
<div class="card-body">
<app-callout type="warning">{{ "deleteProviderWarning" | i18n }}</app-callout>
<p class="text-center">
<strong>{{ name }}</strong>
</p>
<p>{{ "deleteProviderRecoverConfirmDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-danger btn-block btn-submit"
[disabled]="form.loading"
>
<span>{{ "deleteProvider" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,61 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
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";
@Component({
selector: "app-verify-recover-delete-provider",
templateUrl: "verify-recover-delete-provider.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class VerifyRecoverDeleteProviderComponent implements OnInit {
name: string;
formPromise: Promise<any>;
private providerId: string;
private token: string;
constructor(
private router: Router,
private providerApiService: ProviderApiServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private route: ActivatedRoute,
private logService: LogService,
) {}
async ngOnInit() {
const qParams = await firstValueFrom(this.route.queryParams);
if (qParams.providerId != null && qParams.token != null && qParams.name != null) {
this.providerId = qParams.providerId;
this.token = qParams.token;
this.name = qParams.name;
} else {
await this.router.navigate(["/"]);
}
}
async submit() {
try {
const request = new ProviderVerifyRecoverDeleteRequest(this.token);
this.formPromise = this.providerApiService.providerRecoverDeleteToken(
this.providerId,
request,
);
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("providerDeleted"),
this.i18nService.t("providerDeletedDesc"),
);
await this.router.navigate(["/"]);
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,13 +1,12 @@
<div class="d-flex tabbed-header"> <div class="tw-flex tw-justify-between tw-mb-2 tw-pb-2 tw-mt-6">
<h1> <h2 bitTypography="h2">
{{ "billingHistory" | i18n }} {{ "billingHistory" | i18n }}
</h1> </h2>
<button <button
type="button" type="button"
bitButton bitButton
buttonType="secondary" buttonType="secondary"
(click)="load()" (click)="load()"
class="tw-ml-auto"
*ngIf="firstLoaded" *ngIf="firstLoaded"
[disabled]="loading" [disabled]="loading"
> >
@@ -17,11 +16,11 @@
</div> </div>
<ng-container *ngIf="!firstLoaded && loading"> <ng-container *ngIf="!firstLoaded && loading">
<i <i
class="bwi bwi-spinner bwi-spin text-muted" class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<ng-container *ngIf="billing"> <ng-container *ngIf="billing">
<app-billing-history [billing]="billing"></app-billing-history> <app-billing-history [billing]="billing"></app-billing-history>

View File

@@ -170,8 +170,8 @@
</div> </div>
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"> <ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<div class="mt-3"> <div class="mt-3">
<div class="d-flex" *ngIf="!showAdjustStorage"> <div class="d-flex">
<button bitButton type="button" buttonType="secondary" (click)="adjustStorage(true)"> <button bitButton type="button" buttonType="secondary" [bitAction]="adjustStorage(true)">
{{ "addStorage" | i18n }} {{ "addStorage" | i18n }}
</button> </button>
<button <button
@@ -179,18 +179,11 @@
type="button" type="button"
buttonType="secondary" buttonType="secondary"
class="tw-ml-1" class="tw-ml-1"
(click)="adjustStorage(false)" [bitAction]="adjustStorage(false)"
> >
{{ "removeStorage" | i18n }} {{ "removeStorage" | i18n }}
</button> </button>
</div> </div>
<app-adjust-storage
[storageGbPrice]="4"
[add]="adjustStorageAdd"
(onAdjusted)="closeStorage(true)"
(onCanceled)="closeStorage(false)"
*ngIf="showAdjustStorage"
></app-adjust-storage>
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>

View File

@@ -12,6 +12,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import {
AdjustStorageDialogResult,
openAdjustStorageDialog,
} from "../shared/adjust-storage.component";
import { import {
OffboardingSurveyDialogResultType, OffboardingSurveyDialogResultType,
openOffboardingSurvey, openOffboardingSurvey,
@@ -24,7 +28,6 @@ export class UserSubscriptionComponent implements OnInit {
loading = false; loading = false;
firstLoaded = false; firstLoaded = false;
adjustStorageAdd = true; adjustStorageAdd = true;
showAdjustStorage = false;
showUpdateLicense = false; showUpdateLicense = false;
sub: SubscriptionResponse; sub: SubscriptionResponse;
selfHosted = false; selfHosted = false;
@@ -144,19 +147,20 @@ export class UserSubscriptionComponent implements OnInit {
} }
} }
adjustStorage(add: boolean) { adjustStorage = (add: boolean) => {
this.adjustStorageAdd = add; return async () => {
this.showAdjustStorage = true; const dialogRef = openAdjustStorageDialog(this.dialogService, {
} data: {
storageGbPrice: 4,
closeStorage(load: boolean) { add: add,
this.showAdjustStorage = false; },
if (load) { });
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. const result = await lastValueFrom(dialogRef.closed);
// eslint-disable-next-line @typescript-eslint/no-floating-promises if (result === AdjustStorageDialogResult.Adjusted) {
this.load(); await this.load();
}
} }
};
};
get subscriptionMarkedForCancel() { get subscriptionMarkedForCancel() {
return ( return (

View File

@@ -1,10 +1,18 @@
<div class="card card-org-plans"> <div
<div class="card-body"> class="tw-relative tw-flex tw-flex-col tw-min-w-0 tw-rounded tw-border tw-border-solid tw-border-secondary-300"
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()"> >
<span aria-hidden="true">&times;</span> <div class="tw-flex-auto tw-p-5">
</button> <button
<h2 class="card-body-header">{{ "changeBillingPlan" | i18n }}</h2> bitIconButton="bwi-close"
<p class="mb-0">{{ "changeBillingPlanUpgrade" | i18n }}</p> buttonType="main"
type="button"
size="small"
class="tw-float-right"
appA11yTitle="{{ 'cancel' | i18n }}"
(click)="cancel()"
></button>
<h2 bitTypography="h2">{{ "changeBillingPlan" | i18n }}</h2>
<p bitTypography="body1" class="tw-mb-0">{{ "changeBillingPlanUpgrade" | i18n }}</p>
<app-organization-plans <app-organization-plans
[showFree]="false" [showFree]="false"
[showCancel]="true" [showCancel]="true"

View File

@@ -16,11 +16,11 @@
<bit-container> <bit-container>
<ng-container *ngIf="!firstLoaded && loading"> <ng-container *ngIf="!firstLoaded && loading">
<i <i
class="bwi bwi-spinner bwi-spin text-muted" class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<ng-container *ngIf="billing"> <ng-container *ngIf="billing">
<app-billing-history [billing]="billing"></app-billing-history> <app-billing-history [billing]="billing"></app-billing-history>

View File

@@ -15,6 +15,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request"; import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request";
@@ -147,6 +148,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private messagingService: MessagingService, private messagingService: MessagingService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private providerApiService: ProviderApiServiceAbstraction,
) { ) {
this.selfHosted = platformUtilsService.isSelfHost(); this.selfHosted = platformUtilsService.isSelfHost();
} }
@@ -182,7 +184,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
if (this.hasProvider) { if (this.hasProvider) {
this.formGroup.controls.businessOwned.setValue(true); this.formGroup.controls.businessOwned.setValue(true);
this.changedOwnedBusiness(); this.changedOwnedBusiness();
this.provider = await this.apiService.getProvider(this.providerId); this.provider = await this.providerApiService.getProvider(this.providerId);
const providerDefaultPlan = this.passwordManagerPlans.find( const providerDefaultPlan = this.passwordManagerPlans.find(
(plan) => plan.type === PlanType.TeamsAnnually, (plan) => plan.type === PlanType.TeamsAnnually,
); );

View File

@@ -175,23 +175,24 @@
<bit-progress [barWidth]="storagePercentage" bgColor="success"></bit-progress> <bit-progress [barWidth]="storagePercentage" bgColor="success"></bit-progress>
<ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"> <ng-container *ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel">
<div class="tw-mt-3"> <div class="tw-mt-3">
<div class="tw-flex tw-space-x-2" *ngIf="!showAdjustStorage"> <div class="tw-flex tw-space-x-2">
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(true)"> <button
bitButton
buttonType="secondary"
type="button"
[bitAction]="adjustStorage(true)"
>
{{ "addStorage" | i18n }} {{ "addStorage" | i18n }}
</button> </button>
<button bitButton buttonType="secondary" type="button" (click)="adjustStorage(false)"> <button
bitButton
buttonType="secondary"
type="button"
[bitAction]="adjustStorage(false)"
>
{{ "removeStorage" | i18n }} {{ "removeStorage" | i18n }}
</button> </button>
</div> </div>
<app-adjust-storage
[storageGbPrice]="storageGbPrice"
[add]="adjustStorageAdd"
[organizationId]="organizationId"
[interval]="billingInterval"
(onAdjusted)="closeStorage(true)"
(onCanceled)="closeStorage(false)"
*ngIf="showAdjustStorage"
></app-adjust-storage>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="showAdjustSecretsManager"> <ng-container *ngIf="showAdjustSecretsManager">

View File

@@ -18,6 +18,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import {
AdjustStorageDialogResult,
openAdjustStorageDialog,
} from "../shared/adjust-storage.component";
import { import {
OffboardingSurveyDialogResultType, OffboardingSurveyDialogResultType,
openOffboardingSurvey, openOffboardingSurvey,
@@ -36,8 +40,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
userOrg: Organization; userOrg: Organization;
showChangePlan = false; showChangePlan = false;
showDownloadLicense = false; showDownloadLicense = false;
adjustStorageAdd = true;
showAdjustStorage = false;
hasBillingSyncToken: boolean; hasBillingSyncToken: boolean;
showAdjustSecretsManager = false; showAdjustSecretsManager = false;
showSecretsManagerSubscribe = false; showSecretsManagerSubscribe = false;
@@ -361,19 +363,22 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.load(); this.load();
} }
adjustStorage(add: boolean) { adjustStorage = (add: boolean) => {
this.adjustStorageAdd = add; return async () => {
this.showAdjustStorage = true; const dialogRef = openAdjustStorageDialog(this.dialogService, {
} data: {
storageGbPrice: this.storageGbPrice,
closeStorage(load: boolean) { add: add,
this.showAdjustStorage = false; organizationId: this.organizationId,
if (load) { interval: this.billingInterval,
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. },
// eslint-disable-next-line @typescript-eslint/no-floating-promises });
this.load(); const result = await lastValueFrom(dialogRef.closed);
} if (result === AdjustStorageDialogResult.Adjusted) {
await this.load();
} }
};
};
removeSponsorship = async () => { removeSponsorship = async () => {
const confirmed = await this.dialogService.openSimpleDialog({ const confirmed = await this.dialogService.openSimpleDialog({

View File

@@ -0,0 +1,25 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog
dialogSize="large"
[title]="(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n"
>
<ng-container bitDialogContent>
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
</ng-container>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
[bitDialogClose]="DialogResult.Cancelled"
>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,110 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, ViewChild } from "@angular/core";
import { FormGroup } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.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 { DialogService } from "@bitwarden/components";
import { PaymentComponent } from "./payment.component";
import { TaxInfoComponent } from "./tax-info.component";
export interface AdjustPaymentDialogData {
organizationId: string;
currentType: PaymentMethodType;
}
export enum AdjustPaymentDialogResult {
Adjusted = "adjusted",
Cancelled = "cancelled",
}
@Component({
templateUrl: "adjust-payment-dialog.component.html",
})
export class AdjustPaymentDialogComponent {
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
organizationId: string;
currentType: PaymentMethodType;
paymentMethodType = PaymentMethodType;
protected DialogResult = AdjustPaymentDialogResult;
protected formGroup = new FormGroup({});
constructor(
private dialogRef: DialogRef,
@Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData,
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction,
private paymentMethodWarningService: PaymentMethodWarningService,
) {
this.organizationId = data.organizationId;
this.currentType = data.currentType;
}
submit = async () => {
const request = new PaymentRequest();
const response = this.paymentComponent.createPaymentToken().then((result) => {
request.paymentToken = result[0];
request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
request.country = this.taxInfoComponent.taxInfo.country;
if (this.organizationId == null) {
return this.apiService.postAccountPayment(request);
} else {
request.taxId = this.taxInfoComponent.taxInfo.taxId;
request.state = this.taxInfoComponent.taxInfo.state;
request.line1 = this.taxInfoComponent.taxInfo.line1;
request.line2 = this.taxInfoComponent.taxInfo.line2;
request.city = this.taxInfoComponent.taxInfo.city;
request.state = this.taxInfoComponent.taxInfo.state;
return this.organizationApiService.updatePayment(this.organizationId, request);
}
});
await response;
if (this.organizationId) {
await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
}
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("updatedPaymentMethod"),
);
this.dialogRef.close(AdjustPaymentDialogResult.Adjusted);
};
changeCountry() {
if (this.taxInfoComponent.taxInfo.country === "US") {
this.paymentComponent.hideBank = !this.organizationId;
} else {
this.paymentComponent.hideBank = true;
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
}
}
/**
* Strongly typed helper to open a AdjustPaymentDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export function openAdjustPaymentDialog(
dialogService: DialogService,
config: DialogConfig<AdjustPaymentDialogData>,
) {
return dialogService.open<AdjustPaymentDialogResult>(AdjustPaymentDialogComponent, config);
}

View File

@@ -1,19 +0,0 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="card-body">
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
<h3 class="card-body-header">
{{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
</h3>
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</div>
</form>

View File

@@ -1,90 +0,0 @@
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.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 { PaymentComponent } from "./payment.component";
import { TaxInfoComponent } from "./tax-info.component";
@Component({
selector: "app-adjust-payment",
templateUrl: "adjust-payment.component.html",
})
export class AdjustPaymentComponent {
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
@Input() currentType?: PaymentMethodType;
@Input() organizationId: string;
@Output() onAdjusted = new EventEmitter();
@Output() onCanceled = new EventEmitter();
paymentMethodType = PaymentMethodType;
formPromise: Promise<void>;
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction,
private paymentMethodWarningService: PaymentMethodWarningService,
) {}
async submit() {
try {
const request = new PaymentRequest();
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
request.paymentToken = result[0];
request.paymentMethodType = result[1];
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
request.country = this.taxInfoComponent.taxInfo.country;
if (this.organizationId == null) {
return this.apiService.postAccountPayment(request);
} else {
request.taxId = this.taxInfoComponent.taxInfo.taxId;
request.state = this.taxInfoComponent.taxInfo.state;
request.line1 = this.taxInfoComponent.taxInfo.line1;
request.line2 = this.taxInfoComponent.taxInfo.line2;
request.city = this.taxInfoComponent.taxInfo.city;
request.state = this.taxInfoComponent.taxInfo.state;
return this.organizationApiService.updatePayment(this.organizationId, request);
}
});
await this.formPromise;
if (this.organizationId) {
await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId);
}
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("updatedPaymentMethod"),
);
this.onAdjusted.emit();
} catch (e) {
this.logService.error(e);
}
}
cancel() {
this.onCanceled.emit();
}
changeCountry() {
if (this.taxInfoComponent.taxInfo.country === "US") {
this.paymentComponent.hideBank = !this.organizationId;
} else {
this.paymentComponent.hideBank = true;
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
this.paymentComponent.method = PaymentMethodType.Card;
this.paymentComponent.changeMethod();
}
}
}
}

View File

@@ -1,43 +1,35 @@
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> <form [formGroup]="formGroup" [bitSubmit]="submit">
<div class="card-body"> <bit-dialog dialogSize="default" [title]="(add ? 'addStorage' : 'removeStorage') | i18n">
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()"> <ng-container bitDialogContent>
<span aria-hidden="true">&times;</span> <p bitTypography="body1">{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}</p>
</button> <div class="tw-grid tw-grid-cols-12">
<h3 class="card-body-header">{{ (add ? "addStorage" : "removeStorage") | i18n }}</h3> <bit-form-field class="tw-col-span-7">
<div class="row"> <bit-label>{{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }}</bit-label>
<div class="form-group col-6"> <input bitInput type="number" formControlName="storageAdjustment" />
<label for="storageAdjustment">{{ <bit-hint *ngIf="add">
(add ? "gbStorageAdd" : "gbStorageRemove") | i18n <strong>{{ "total" | i18n }}:</strong>
}}</label> {{ formGroup.get("storageAdjustment").value || 0 }} GB &times;
<input
id="storageAdjustment"
class="form-control"
type="number"
name="StorageGbAdjustment"
[(ngModel)]="storageAdjustment"
min="0"
max="99"
step="1"
required
/>
</div>
</div>
<div *ngIf="add" class="mb-3">
<strong>{{ "total" | i18n }}:</strong> {{ storageAdjustment || 0 }} GB &times;
{{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{
interval | i18n interval | i18n
}} }}
</bit-hint>
</bit-form-field>
</div> </div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> </ng-container>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <ng-container bitDialogFooter>
<span>{{ "submit" | i18n }}</span> <button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button> </button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()"> <button
type="button"
bitButton
bitFormButton
buttonType="secondary"
[bitDialogClose]="DialogResult.Cancelled"
>
{{ "cancel" | i18n }} {{ "cancel" | i18n }}
</button> </button>
<small class="d-block text-muted mt-3"> </ng-container>
{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }} </bit-dialog>
</small>
</div>
</form> </form>
<app-payment [showMethods]="false"></app-payment> <app-payment [showMethods]="false"></app-payment>

View File

@@ -1,4 +1,6 @@
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, ViewChild } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -8,27 +10,45 @@ import { StorageRequest } from "@bitwarden/common/models/request/storage.request
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
import { PaymentComponent } from "./payment.component"; import { PaymentComponent } from "./payment.component";
export interface AdjustStorageDialogData {
storageGbPrice: number;
add: boolean;
organizationId?: string;
interval?: string;
}
export enum AdjustStorageDialogResult {
Adjusted = "adjusted",
Cancelled = "cancelled",
}
@Component({ @Component({
selector: "app-adjust-storage",
templateUrl: "adjust-storage.component.html", templateUrl: "adjust-storage.component.html",
}) })
export class AdjustStorageComponent { export class AdjustStorageComponent {
@Input() storageGbPrice = 0; storageGbPrice: number;
@Input() add = true; add: boolean;
@Input() organizationId: string; organizationId: string;
@Input() interval = "year"; interval: string;
@Output() onAdjusted = new EventEmitter<number>();
@Output() onCanceled = new EventEmitter();
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
storageAdjustment = 0; protected DialogResult = AdjustStorageDialogResult;
formPromise: Promise<PaymentResponse | void>; protected formGroup = new FormGroup({
storageAdjustment: new FormControl(0, [
Validators.required,
Validators.min(0),
Validators.max(99),
]),
});
constructor( constructor(
private dialogRef: DialogRef,
@Inject(DIALOG_DATA) protected data: AdjustStorageDialogData,
private apiService: ApiService, private apiService: ApiService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@@ -36,12 +56,16 @@ export class AdjustStorageComponent {
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private logService: LogService, private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
) {} ) {
this.storageGbPrice = data.storageGbPrice;
this.add = data.add;
this.organizationId = data.organizationId;
this.interval = data.interval || "year";
}
async submit() { submit = async () => {
try {
const request = new StorageRequest(); const request = new StorageRequest();
request.storageGbAdjustment = this.storageAdjustment; request.storageGbAdjustment = this.formGroup.value.storageAdjustment;
if (!this.add) { if (!this.add) {
request.storageGbAdjustment *= -1; request.storageGbAdjustment *= -1;
} }
@@ -50,12 +74,9 @@ export class AdjustStorageComponent {
const action = async () => { const action = async () => {
let response: Promise<PaymentResponse>; let response: Promise<PaymentResponse>;
if (this.organizationId == null) { if (this.organizationId == null) {
response = this.formPromise = this.apiService.postAccountStorage(request); response = this.apiService.postAccountStorage(request);
} else { } else {
response = this.formPromise = this.organizationApiService.updateStorage( response = this.organizationApiService.updateStorage(this.organizationId, request);
this.organizationId,
request,
);
} }
const result = await response; const result = await response;
if (result != null && result.paymentIntentClientSecret != null) { if (result != null && result.paymentIntentClientSecret != null) {
@@ -69,9 +90,8 @@ export class AdjustStorageComponent {
} }
} }
}; };
this.formPromise = action(); await action();
await this.formPromise; this.dialogRef.close(AdjustStorageDialogResult.Adjusted);
this.onAdjusted.emit(this.storageAdjustment);
if (paymentFailed) { if (paymentFailed) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"warning", "warning",
@@ -89,16 +109,21 @@ export class AdjustStorageComponent {
this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()),
); );
} }
} catch (e) { };
this.logService.error(e);
}
}
cancel() {
this.onCanceled.emit();
}
get adjustedStorageTotal(): number { get adjustedStorageTotal(): number {
return this.storageGbPrice * this.storageAdjustment; return this.storageGbPrice * this.formGroup.value.storageAdjustment;
} }
} }
/**
* Strongly typed helper to open an AdjustStorageDialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Configuration for the dialog
*/
export function openAdjustStorageDialog(
dialogService: DialogService,
config: DialogConfig<AdjustStorageDialogData>,
) {
return dialogService.open<AdjustStorageDialogResult>(AdjustStorageComponent, config);
}

View File

@@ -1,15 +1,16 @@
<h2 class="mt-3">{{ "invoices" | i18n }}</h2> <bit-section>
<p *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p> <h3 bitTypography="h3">{{ "invoices" | i18n }}</h3>
<table class="table mb-2" *ngIf="invoices && invoices.length"> <p bitTypography="body1" *ngIf="!invoices || !invoices.length">{{ "noInvoices" | i18n }}</p>
<tbody> <bit-table>
<tr *ngFor="let i of invoices"> <ng-template body>
<td>{{ i.date | date: "mediumDate" }}</td> <tr bitRow *ngFor="let i of invoices">
<td> <td bitCell>{{ i.date | date: "mediumDate" }}</td>
<td bitCell>
<a <a
href="{{ i.pdfUrl }}" href="{{ i.pdfUrl }}"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
class="mr-2" class="tw-mr-2"
appA11yTitle="{{ 'downloadInvoice' | i18n }}" appA11yTitle="{{ 'downloadInvoice' | i18n }}"
> >
<i class="bwi bwi-file-pdf" aria-hidden="true"></i <i class="bwi bwi-file-pdf" aria-hidden="true"></i
@@ -18,33 +19,37 @@
{{ "invoiceNumber" | i18n: i.number }}</a {{ "invoiceNumber" | i18n: i.number }}</a
> >
</td> </td>
<td>{{ i.amount | currency: "$" }}</td> <td bitCell>{{ i.amount | currency: "$" }}</td>
<td> <td bitCell>
<span *ngIf="i.paid"> <span *ngIf="i.paid">
<i class="bwi bwi-check text-success" aria-hidden="true"></i> <i class="bwi bwi-check tw-text-success" aria-hidden="true"></i>
{{ "paid" | i18n }} {{ "paid" | i18n }}
</span> </span>
<span *ngIf="!i.paid"> <span *ngIf="!i.paid">
<i class="bwi bwi-exclamation-circle text-muted" aria-hidden="true"></i> <i class="bwi bwi-exclamation-circle tw-text-muted" aria-hidden="true"></i>
{{ "unpaid" | i18n }} {{ "unpaid" | i18n }}
</span> </span>
</td> </td>
</tr> </tr>
</tbody> </ng-template>
</table> </bit-table>
<h2 class="spaced-header">{{ "transactions" | i18n }}</h2> </bit-section>
<p *ngIf="!transactions || !transactions.length">{{ "noTransactions" | i18n }}</p> <bit-section>
<table class="table mb-2" *ngIf="transactions && transactions.length"> <h3 bitTypography="h3">{{ "transactions" | i18n }}</h3>
<tbody> <p bitTypography="body1" *ngIf="!transactions || !transactions.length">
<tr *ngFor="let t of transactions"> {{ "noTransactions" | i18n }}
<td>{{ t.createdDate | date: "mediumDate" }}</td> </p>
<td> <bit-table *ngIf="transactions && transactions.length">
<ng-template body>
<tr bitRow *ngFor="let t of transactions">
<td bitCell>{{ t.createdDate | date: "mediumDate" }}</td>
<td bitCell>
<span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit"> <span *ngIf="t.type === transactionType.Charge || t.type === transactionType.Credit">
{{ "chargeNoun" | i18n }} {{ "chargeNoun" | i18n }}
</span> </span>
<span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span> <span *ngIf="t.type === transactionType.Refund">{{ "refundNoun" | i18n }}</span>
</td> </td>
<td> <td bitCell>
<i <i
class="bwi bwi-fw" class="bwi bwi-fw"
*ngIf="t.paymentMethodType" *ngIf="t.paymentMethodType"
@@ -56,10 +61,12 @@
<td <td
[ngClass]="{ 'text-strike': t.refunded }" [ngClass]="{ 'text-strike': t.refunded }"
title="{{ (t.refunded ? 'refunded' : '') | i18n }}" title="{{ (t.refunded ? 'refunded' : '') | i18n }}"
bitCell
> >
{{ t.amount | currency: "$" }} {{ t.amount | currency: "$" }}
</td> </td>
</tr> </tr>
</tbody> </ng-template>
</table> </bit-table>
<small class="text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small> <small class="tw-text-muted">* {{ "chargesStatement" | i18n: "BITWARDEN" }}</small>
</bit-section>

View File

@@ -4,7 +4,7 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared"; import { SharedModule } from "../../shared";
import { AddCreditComponent } from "./add-credit.component"; import { AddCreditComponent } from "./add-credit.component";
import { AdjustPaymentComponent } from "./adjust-payment.component"; import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component";
import { AdjustStorageComponent } from "./adjust-storage.component"; import { AdjustStorageComponent } from "./adjust-storage.component";
import { BillingHistoryComponent } from "./billing-history.component"; import { BillingHistoryComponent } from "./billing-history.component";
import { OffboardingSurveyComponent } from "./offboarding-survey.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component";
@@ -18,7 +18,7 @@ import { UpdateLicenseComponent } from "./update-license.component";
imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule], imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule],
declarations: [ declarations: [
AddCreditComponent, AddCreditComponent,
AdjustPaymentComponent, AdjustPaymentDialogComponent,
AdjustStorageComponent, AdjustStorageComponent,
BillingHistoryComponent, BillingHistoryComponent,
PaymentMethodComponent, PaymentMethodComponent,

View File

@@ -15,7 +15,7 @@
<bit-container> <bit-container>
<div class="tabbed-header" *ngIf="!organizationId"> <div class="tabbed-header" *ngIf="!organizationId">
<!-- TODO: Organization and individual should use different "page" components --> <!--TODO: Organization and individual should use different "page" components -->
<h1>{{ "paymentMethod" | i18n }}</h1> <h1>{{ "paymentMethod" | i18n }}</h1>
</div> </div>
@@ -102,23 +102,9 @@
{{ paymentSource.description }} {{ paymentSource.description }}
</p> </p>
</ng-container> </ng-container>
<button <button type="button" bitButton buttonType="secondary" [bitAction]="changePayment">
type="button"
bitButton
buttonType="secondary"
(click)="changePayment()"
*ngIf="!showAdjustPayment"
>
{{ (paymentSource ? "changePaymentMethod" : "addPaymentMethod") | i18n }} {{ (paymentSource ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
</button> </button>
<app-adjust-payment
[organizationId]="organizationId"
[currentType]="paymentSource != null ? paymentSource.type : null"
(onAdjusted)="closePayment(true)"
(onCanceled)="closePayment(false)"
*ngIf="showAdjustPayment"
>
</app-adjust-payment>
<p *ngIf="isUnpaid">{{ "paymentChargedWithUnpaidSubscription" | i18n }}</p> <p *ngIf="isUnpaid">{{ "paymentChargedWithUnpaidSubscription" | i18n }}</p>
<ng-container *ngIf="forOrganization"> <ng-container *ngIf="forOrganization">
<h2 class="spaced-header">{{ "taxInformation" | i18n }}</h2> <h2 class="spaced-header">{{ "taxInformation" | i18n }}</h2>

View File

@@ -1,6 +1,7 @@
import { Component, OnInit, ViewChild } from "@angular/core"; import { Component, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { FormBuilder, FormControl, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { lastValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
@@ -14,6 +15,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import {
AdjustPaymentDialogResult,
openAdjustPaymentDialog,
} from "./adjust-payment-dialog.component";
import { TaxInfoComponent } from "./tax-info.component"; import { TaxInfoComponent } from "./tax-info.component";
@Component({ @Component({
@@ -25,7 +30,6 @@ export class PaymentMethodComponent implements OnInit {
loading = false; loading = false;
firstLoaded = false; firstLoaded = false;
showAdjustPayment = false;
showAddCredit = false; showAddCredit = false;
billing: BillingPaymentResponse; billing: BillingPaymentResponse;
org: OrganizationSubscriptionResponse; org: OrganizationSubscriptionResponse;
@@ -120,18 +124,18 @@ export class PaymentMethodComponent implements OnInit {
} }
} }
changePayment() { changePayment = async () => {
this.showAdjustPayment = true; const dialogRef = openAdjustPaymentDialog(this.dialogService, {
} data: {
organizationId: this.organizationId,
closePayment(load: boolean) { currentType: this.paymentSource !== null ? this.paymentSource.type : null,
this.showAdjustPayment = false; },
if (load) { });
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. const result = await lastValueFrom(dialogRef.closed);
// eslint-disable-next-line @typescript-eslint/no-floating-promises if (result === AdjustPaymentDialogResult.Adjusted) {
this.load(); await this.load();
}
} }
};
async verifyBank() { async verifyBank() {
if (this.loading || !this.forOrganization) { if (this.loading || !this.forOrganization) {

View File

@@ -13,6 +13,7 @@ import { flagEnabled, Flags } from "../utils/flags";
import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.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 { 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 { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component";
import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component"; import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component";
import { AcceptOrganizationComponent } from "./auth/accept-organization.component"; import { AcceptOrganizationComponent } from "./auth/accept-organization.component";
@@ -156,6 +157,12 @@ const routes: Routes = [
canActivate: [UnauthGuard], canActivate: [UnauthGuard],
data: { titleId: "deleteAccount" }, data: { titleId: "deleteAccount" },
}, },
{
path: "verify-recover-delete-provider",
component: VerifyRecoverDeleteProviderComponent,
canActivate: [UnauthGuard],
data: { titleId: "deleteAccount" },
},
{ {
path: "send/:sendId/:key", path: "send/:sendId/:key",
component: AccessComponent, component: AccessComponent,

View File

@@ -13,6 +13,7 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f
import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-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 { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component";
import { ProvidersComponent } from "../admin-console/providers/providers.component"; import { ProvidersComponent } from "../admin-console/providers/providers.component";
import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component";
import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component"; import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component";
import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component"; import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component";
import { AcceptOrganizationComponent } from "../auth/accept-organization.component"; import { AcceptOrganizationComponent } from "../auth/accept-organization.component";
@@ -184,6 +185,7 @@ import { SharedModule } from "./shared.module";
VerifyEmailComponent, VerifyEmailComponent,
VerifyEmailTokenComponent, VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent, VerifyRecoverDeleteComponent,
VerifyRecoverDeleteProviderComponent,
LowKdfComponent, LowKdfComponent,
], ],
exports: [ exports: [
@@ -261,6 +263,7 @@ import { SharedModule } from "./shared.module";
VerifyEmailComponent, VerifyEmailComponent,
VerifyEmailTokenComponent, VerifyEmailTokenComponent,
VerifyRecoverDeleteComponent, VerifyRecoverDeleteComponent,
VerifyRecoverDeleteProviderComponent,
LowKdfComponent, LowKdfComponent,
HeaderModule, HeaderModule,
DangerZoneComponent, DangerZoneComponent,

View File

@@ -124,6 +124,9 @@ export class CollectionAdminService {
view.groups = c.groups; view.groups = c.groups;
view.users = c.users; view.users = c.users;
view.assigned = c.assigned; view.assigned = c.assigned;
view.readOnly = c.readOnly;
view.hidePasswords = c.hidePasswords;
view.manage = c.manage;
} }
return view; return view;

View File

@@ -7905,5 +7905,44 @@
}, },
"unassignedItemsBannerSelfHost": { "unassignedItemsBannerSelfHost": {
"message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible."
},
"restrictedGroupAccessDesc": {
"message": "You cannot add yourself to a group."
},
"deleteProvider": {
"message": "Delete provider"
},
"deleteProviderConfirmation": {
"message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data."
},
"deleteProviderName": {
"message": "Cannot delete $ID$",
"placeholders": {
"id": {
"content": "$1",
"example": "John Smith"
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"placeholders": {
"id": {
"content": "$1",
"example": "John Smith"
}
}
},
"providerDeleted": {
"message": "Provider deleted"
},
"providerDeletedDesc": {
"message": "The Provider and all associated data has been deleted."
},
"deleteProviderRecoverConfirmDesc": {
"message": "You have requested to delete this Provider. Use the button below to confirm."
},
"deleteProviderWarning": {
"message": "Deleting your provider is permanent. It cannot be undone."
} }
} }

View File

@@ -8,6 +8,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared"; import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { OssModule } from "@bitwarden/web-vault/app/oss.module";
import { DangerZoneComponent } from "../../../../../../apps/web/src/app/auth/settings/account/danger-zone.component";
import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component"; import { ManageClientOrganizationSubscriptionComponent } from "../../billing/providers/clients/manage-client-organization-subscription.component";
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component"; import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
@@ -40,6 +41,7 @@ import { SetupComponent } from "./setup/setup.component";
ProvidersLayoutComponent, ProvidersLayoutComponent,
PaymentMethodWarningsModule, PaymentMethodWarningsModule,
TaxInfoComponent, TaxInfoComponent,
DangerZoneComponent,
], ],
declarations: [ declarations: [
AcceptProviderComponent, AcceptProviderComponent,

View File

@@ -1,20 +1,20 @@
<app-header></app-header> <app-header></app-header>
<bit-container>
<div *ngIf="loading"> <div *ngIf="loading">
<i <i
class="bwi bwi-spinner bwi-spin text-muted" class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="sr-only">{{ "loading" | i18n }}</span>
</div> </div>
<form <form
*ngIf="provider && !loading" *ngIf="provider && !loading"
#form #form
(ngSubmit)="submit()" (ngSubmit)="submit()"
[appApiAction]="formPromise" [appApiAction]="formPromise"
ngNativeValidate ngNativeValidate
> >
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<div class="form-group"> <div class="form-group">
@@ -48,4 +48,11 @@
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span> <span>{{ "save" | i18n }}</span>
</button> </button>
</form> </form>
<app-danger-zone *ngIf="enableDeleteProvider$ | async">
<button type="button" bitButton buttonType="danger" (click)="deleteProvider()">
{{ "deleteProvider" | i18n }}
</button>
</app-danger-zone>
</bit-container>

View File

@@ -1,13 +1,18 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request"; import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request";
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
@Component({ @Component({
selector: "provider-account", selector: "provider-account",
@@ -23,6 +28,11 @@ export class AccountComponent {
private providerId: string; private providerId: string;
protected enableDeleteProvider$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableDeleteProvider,
false,
);
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private i18nService: I18nService, private i18nService: I18nService,
@@ -30,6 +40,9 @@ export class AccountComponent {
private syncService: SyncService, private syncService: SyncService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private logService: LogService, private logService: LogService,
private dialogService: DialogService,
private configService: ConfigService,
private providerApiService: ProviderApiServiceAbstraction,
) {} ) {}
async ngOnInit() { async ngOnInit() {
@@ -38,7 +51,7 @@ export class AccountComponent {
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
this.providerId = params.providerId; this.providerId = params.providerId;
try { try {
this.provider = await this.apiService.getProvider(this.providerId); this.provider = await this.providerApiService.getProvider(this.providerId);
} catch (e) { } catch (e) {
this.logService.error(`Handled exception: ${e}`); this.logService.error(`Handled exception: ${e}`);
} }
@@ -53,7 +66,7 @@ export class AccountComponent {
request.businessName = this.provider.businessName; request.businessName = this.provider.businessName;
request.billingEmail = this.provider.billingEmail; request.billingEmail = this.provider.billingEmail;
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => { this.formPromise = this.providerApiService.putProvider(this.providerId, request).then(() => {
return this.syncService.fullSync(true); return this.syncService.fullSync(true);
}); });
await this.formPromise; await this.formPromise;
@@ -62,4 +75,60 @@ export class AccountComponent {
this.logService.error(`Handled exception: ${e}`); this.logService.error(`Handled exception: ${e}`);
} }
} }
async deleteProvider() {
const providerClients = await this.apiService.getProviderClients(this.providerId);
if (providerClients.data != null && providerClients.data.length > 0) {
await this.dialogService.openSimpleDialog({
title: { key: "deleteProviderName", placeholders: [this.provider.name] },
content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] },
acceptButtonText: { key: "ok" },
type: "danger",
});
return false;
}
const userVerified = await this.verifyUser();
if (!userVerified) {
return;
}
this.formPromise = this.providerApiService.deleteProvider(this.providerId);
try {
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("providerDeleted"),
this.i18nService.t("providerDeletedDesc"),
);
} catch (e) {
this.logService.error(e);
}
this.formPromise = null;
}
private async verifyUser(): Promise<boolean> {
const confirmDescription = "deleteProviderConfirmation";
const result = await UserVerificationDialogComponent.open(this.dialogService, {
title: "deleteProvider",
bodyText: confirmDescription,
confirmButtonOptions: {
text: "deleteProvider",
type: "danger",
},
});
// Handle the result of the dialog based on user action and verification success
if (result.userAction === "cancel") {
// User cancelled the dialog
return false;
}
// User confirmed the dialog so check verification success
if (!result.verificationSuccess) {
return false;
}
return true;
}
} }

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators"; import { first } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request";
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -50,10 +50,10 @@ export class SetupComponent implements OnInit {
private i18nService: I18nService, private i18nService: I18nService,
private route: ActivatedRoute, private route: ActivatedRoute,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private apiService: ApiService,
private syncService: SyncService, private syncService: SyncService,
private validationService: ValidationService, private validationService: ValidationService,
private configService: ConfigService, private configService: ConfigService,
private providerApiService: ProviderApiServiceAbstraction,
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -80,7 +80,7 @@ export class SetupComponent implements OnInit {
// Check if provider exists, redirect if it does // Check if provider exists, redirect if it does
try { try {
const provider = await this.apiService.getProvider(this.providerId); const provider = await this.providerApiService.getProvider(this.providerId);
if (provider.name != null) { if (provider.name != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -128,7 +128,7 @@ export class SetupComponent implements OnInit {
} }
} }
const provider = await this.apiService.postProviderSetup(this.providerId, request); const provider = await this.providerApiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup")); this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
await this.syncService.fullSync(true); await this.syncService.fullSync(true);

View File

@@ -38,6 +38,7 @@ import {
InternalPolicyService, InternalPolicyService,
PolicyService as PolicyServiceAbstraction, PolicyService as PolicyServiceAbstraction,
} from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
@@ -47,6 +48,7 @@ import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/comm
import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
import { import {
@@ -1115,6 +1117,11 @@ const safeProviders: SafeProvider[] = [
useClass: LoggingErrorHandler, useClass: LoggingErrorHandler,
deps: [], deps: [],
}), }),
safeProvider({
provide: ProviderApiServiceAbstraction,
useClass: ProviderApiService,
deps: [ApiServiceAbstraction],
}),
]; ];
function encryptServiceFactory( function encryptServiceFactory(

View File

@@ -4,8 +4,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request";
import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request";
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
@@ -29,7 +27,6 @@ import {
ProviderUserResponse, ProviderUserResponse,
ProviderUserUserDetailsResponse, ProviderUserUserDetailsResponse,
} from "../admin-console/models/response/provider/provider-user.response"; } from "../admin-console/models/response/provider/provider-user.response";
import { ProviderResponse } from "../admin-console/models/response/provider/provider.response";
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
@@ -297,7 +294,6 @@ export abstract class ApiService {
) => Promise<any>; ) => Promise<any>;
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>; getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
getSync: () => Promise<SyncResponse>; getSync: () => Promise<SyncResponse>;
@@ -373,10 +369,6 @@ export abstract class ApiService {
getPlans: () => Promise<ListResponse<PlanResponse>>; getPlans: () => Promise<ListResponse<PlanResponse>>;
getTaxRates: () => Promise<ListResponse<TaxRateResponse>>; getTaxRates: () => Promise<ListResponse<TaxRateResponse>>;
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
getProvider: (id: string) => Promise<ProviderResponse>;
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>; getProviderUsers: (providerId: string) => Promise<ListResponse<ProviderUserUserDetailsResponse>>;
getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>; getProviderUser: (providerId: string, id: string) => Promise<ProviderUserResponse>;
postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>; postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise<any>;

View File

@@ -0,0 +1,15 @@
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request";
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
import { ProviderResponse } from "../../models/response/provider/provider.response";
export class ProviderApiServiceAbstraction {
postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise<ProviderResponse>;
getProvider: (id: string) => Promise<ProviderResponse>;
putProvider: (id: string, request: ProviderUpdateRequest) => Promise<ProviderResponse>;
providerRecoverDeleteToken: (
organizationId: string,
request: ProviderVerifyRecoverDeleteRequest,
) => Promise<any>;
deleteProvider: (id: string) => Promise<void>;
}

View File

@@ -0,0 +1,7 @@
export class ProviderVerifyRecoverDeleteRequest {
token: string;
constructor(token: string) {
this.token = token;
}
}

View File

@@ -0,0 +1,47 @@
import { ApiService } from "../../../abstractions/api.service";
import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction";
import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request";
import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request";
import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request";
import { ProviderResponse } from "../../models/response/provider/provider.response";
export class ProviderApiService implements ProviderApiServiceAbstraction {
constructor(private apiService: ApiService) {}
async postProviderSetup(id: string, request: ProviderSetupRequest) {
const r = await this.apiService.send(
"POST",
"/providers/" + id + "/setup",
request,
true,
true,
);
return new ProviderResponse(r);
}
async getProvider(id: string) {
const r = await this.apiService.send("GET", "/providers/" + id, null, true, true);
return new ProviderResponse(r);
}
async putProvider(id: string, request: ProviderUpdateRequest) {
const r = await this.apiService.send("PUT", "/providers/" + id, request, true, true);
return new ProviderResponse(r);
}
providerRecoverDeleteToken(
providerId: string,
request: ProviderVerifyRecoverDeleteRequest,
): Promise<any> {
return this.apiService.send(
"POST",
"/providers/" + providerId + "/delete-recover-token",
request,
false,
false,
);
}
async deleteProvider(id: string): Promise<void> {
await this.apiService.send("DELETE", "/providers/" + id, null, true, false);
}
}

View File

@@ -10,6 +10,7 @@ export enum FeatureFlag {
EnableConsolidatedBilling = "enable-consolidated-billing", EnableConsolidatedBilling = "enable-consolidated-billing",
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
UnassignedItemsBanner = "unassigned-items-banner", UnassignedItemsBanner = "unassigned-items-banner",
EnableDeleteProvider = "AC-1218-delete-provider",
} }
// Replace this with a type safe lookup of the feature flag values in PM-2282 // Replace this with a type safe lookup of the feature flag values in PM-2282

View File

@@ -7,8 +7,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request";
import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request";
import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request";
import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request";
import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request";
import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request";
import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request";
@@ -32,7 +30,6 @@ import {
ProviderUserResponse, ProviderUserResponse,
ProviderUserUserDetailsResponse, ProviderUserUserDetailsResponse,
} from "../admin-console/models/response/provider/provider-user.response"; } from "../admin-console/models/response/provider/provider-user.response";
import { ProviderResponse } from "../admin-console/models/response/provider/provider.response";
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
import { TokenService } from "../auth/abstractions/token.service"; import { TokenService } from "../auth/abstractions/token.service";
import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
@@ -866,16 +863,6 @@ export class ApiService implements ApiServiceAbstraction {
return r; return r;
} }
async putGroupUsers(organizationId: string, id: string, request: string[]): Promise<any> {
await this.send(
"PUT",
"/organizations/" + organizationId + "/groups/" + id + "/users",
request,
true,
false,
);
}
deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> { deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> {
return this.send( return this.send(
"DELETE", "DELETE",
@@ -1161,23 +1148,6 @@ export class ApiService implements ApiServiceAbstraction {
return this.send("DELETE", "/organizations/connections/" + id, null, true, false); return this.send("DELETE", "/organizations/connections/" + id, null, true, false);
} }
// Provider APIs
async postProviderSetup(id: string, request: ProviderSetupRequest) {
const r = await this.send("POST", "/providers/" + id + "/setup", request, true, true);
return new ProviderResponse(r);
}
async getProvider(id: string) {
const r = await this.send("GET", "/providers/" + id, null, true, true);
return new ProviderResponse(r);
}
async putProvider(id: string, request: ProviderUpdateRequest) {
const r = await this.send("PUT", "/providers/" + id, request, true, true);
return new ProviderResponse(r);
}
// Provider User APIs // Provider User APIs
async getProviderUsers( async getProviderUsers(

View File

@@ -21,6 +21,10 @@ export class CollectionDetailsResponse extends CollectionResponse {
readOnly: boolean; readOnly: boolean;
manage: boolean; manage: boolean;
hidePasswords: boolean; hidePasswords: boolean;
/**
* Flag indicating the user has been explicitly assigned to this Collection
*/
assigned: boolean; assigned: boolean;
constructor(response: any) { constructor(response: any) {
@@ -35,15 +39,10 @@ export class CollectionDetailsResponse extends CollectionResponse {
} }
} }
export class CollectionAccessDetailsResponse extends CollectionResponse { export class CollectionAccessDetailsResponse extends CollectionDetailsResponse {
groups: SelectionReadOnlyResponse[] = []; groups: SelectionReadOnlyResponse[] = [];
users: SelectionReadOnlyResponse[] = []; users: SelectionReadOnlyResponse[] = [];
/**
* Flag indicating the user has been explicitly assigned to this Collection
*/
assigned: boolean;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.assigned = this.getResponseProperty("Assigned") || false; this.assigned = this.getResponseProperty("Assigned") || false;

2
package-lock.json generated
View File

@@ -247,7 +247,7 @@
}, },
"apps/web": { "apps/web": {
"name": "@bitwarden/web-vault", "name": "@bitwarden/web-vault",
"version": "2024.4.0" "version": "2024.4.1"
}, },
"libs/admin-console": { "libs/admin-console": {
"name": "@bitwarden/admin-console", "name": "@bitwarden/admin-console",