1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-23 03:33:54 +00:00

merge main, fix conflicts

This commit is contained in:
rr-bw
2024-09-05 16:53:32 -07:00
300 changed files with 5977 additions and 2162 deletions

View File

@@ -1,11 +1,11 @@
import { Injectable } from "@angular/core";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import {
OrganizationUserApiService,
OrganizationUserInviteRequest,
OrganizationUserUpdateRequest,
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
OrganizationUserDetailsResponse,
} from "@bitwarden/admin-console/common";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CoreOrganizationModule } from "../core-organization.module";
@@ -15,14 +15,14 @@ import { OrganizationUserAdminView } from "../views/organization-user-admin-view
export class UserAdminService {
constructor(
private configService: ConfigService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
) {}
async get(
organizationId: string,
organizationUserId: string,
): Promise<OrganizationUserAdminView | undefined> {
const userResponse = await this.organizationUserService.getOrganizationUser(
const userResponse = await this.organizationUserApiService.getOrganizationUser(
organizationId,
organizationUserId,
{
@@ -47,7 +47,11 @@ export class UserAdminService {
request.groups = user.groups;
request.accessSecretsManager = user.accessSecretsManager;
await this.organizationUserService.putOrganizationUser(user.organizationId, user.id, request);
await this.organizationUserApiService.putOrganizationUser(
user.organizationId,
user.id,
request,
);
}
async invite(emails: string[], user: OrganizationUserAdminView): Promise<void> {
@@ -59,7 +63,7 @@ export class UserAdminService {
request.groups = user.groups;
request.accessSecretsManager = user.accessSecretsManager;
await this.organizationUserService.postOrganizationUserInvite(user.organizationId, request);
await this.organizationUserApiService.postOrganizationUserInvite(user.organizationId, request);
}
private async decryptMany(

View File

@@ -1,4 +1,4 @@
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
OrganizationUserType,

View File

@@ -6,31 +6,27 @@
<div bitDialogContent>
<form [formGroup]="filterFormGroup" [bitSubmit]="refreshEvents">
<div class="tw-flex tw-items-center tw-space-x-2">
<div>
<label class="tw-sr-only" for="start">{{ "startDate" | i18n }}</label>
<span>
<input
bitInput
type="datetime-local"
id="start"
placeholder="{{ 'startDate' | i18n }}"
formControlName="start"
/>
</span>
</div>
<bit-form-field>
<bit-label>{{ "from" | i18n }}</bit-label>
<input
bitInput
type="datetime-local"
id="start"
placeholder="{{ 'startDate' | i18n }}"
formControlName="start"
/>
</bit-form-field>
<span class="tw-mx-2">-</span>
<div>
<label class="tw-sr-only" for="end">{{ "endDate" | i18n }}</label>
<span>
<input
bitInput
type="datetime-local"
id="end"
placeholder="{{ 'endDate' | i18n }}"
formControlName="end"
/>
</span>
</div>
<bit-form-field>
<bit-label>{{ "to" | i18n }}</bit-label>
<input
bitInput
type="datetime-local"
id="end"
placeholder="{{ 'endDate' | i18n }}"
formControlName="end"
/>
</bit-form-field>
<button type="submit" bitButton buttonType="primary" bitFormButton>
<i class="bwi bwi-refresh bwi-fw" aria-hidden="true"></i>
{{ "refresh" | i18n }}

View File

@@ -2,9 +2,9 @@ import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { EventResponse } from "@bitwarden/common/models/response/event.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EventView } from "@bitwarden/common/models/view/event.view";
@@ -60,7 +60,7 @@ export class EntityEventsComponent implements OnInit {
private platformUtilsService: PlatformUtilsService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private formBuilder: FormBuilder,
private validationService: ValidationService,
private toastService: ToastService,
@@ -78,7 +78,9 @@ export class EntityEventsComponent implements OnInit {
async load() {
try {
if (this.showUser) {
const response = await this.organizationUserService.getAllUsers(this.params.organizationId);
const response = await this.organizationUserApiService.getAllUsers(
this.params.organizationId,
);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.orgUsersIdMap.set(u.id, { name: name, email: u.email });

View File

@@ -2,10 +2,10 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { EventSystemUser } from "@bitwarden/common/enums";
@@ -49,7 +49,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
logService: LogService,
private userNamePipe: UserNamePipe,
private organizationService: OrganizationService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private providerService: ProviderService,
fileDownloadService: FileDownloadService,
toastService: ToastService,
@@ -83,7 +83,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
}
async load() {
const response = await this.organizationUserService.getAllUsers(this.organizationId);
const response = await this.organizationUserApiService.getAllUsers(this.organizationId);
response.data.forEach((u) => {
const name = this.userNamePipe.transform(u);
this.orgUsersUserIdMap.set(u.userId, { name: name, email: u.email });

View File

@@ -14,9 +14,9 @@ import {
takeUntil,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -131,7 +131,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
);
private get orgMembers$(): Observable<Array<AccessItemView & { userId: UserId }>> {
return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe(
return from(this.organizationUserApiService.getAllUsers(this.organizationId)).pipe(
map((response) =>
response.data.map((m) => ({
id: m.id,
@@ -202,7 +202,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
private apiService: ApiService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private groupService: GroupService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,

View File

@@ -3,7 +3,7 @@ import { Directive, OnInit } from "@angular/core";
import {
OrganizationUserBulkPublicKeyResponse,
OrganizationUserBulkResponse,
} from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
} from "@bitwarden/admin-console/common";
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";

View File

@@ -1,6 +1,6 @@
import { Directive } from "@angular/core";
import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common";
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -1,9 +1,11 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import {
OrganizationUserApiService,
OrganizationUserBulkConfirmRequest,
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserBulkConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -40,7 +42,7 @@ export class BulkConfirmComponent implements OnInit {
@Inject(DIALOG_DATA) protected data: BulkConfirmDialogData,
protected cryptoService: CryptoService,
protected apiService: ApiService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private i18nService: I18nService,
) {
this.organizationId = data.organizationId;
@@ -104,7 +106,7 @@ export class BulkConfirmComponent implements OnInit {
}
protected async getPublicKeys() {
return await this.organizationUserService.postOrganizationUsersPublicKey(
return await this.organizationUserApiService.postOrganizationUsersPublicKey(
this.organizationId,
this.filteredUsers.map((user) => user.id),
);
@@ -116,7 +118,7 @@ export class BulkConfirmComponent implements OnInit {
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
return await this.organizationUserService.postOrganizationUserBulkConfirm(
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
this.organizationId,
request,
);

View File

@@ -1,7 +1,7 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
@@ -21,7 +21,7 @@ export class BulkEnableSecretsManagerDialogComponent implements OnInit {
constructor(
public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: BulkEnableSecretsManagerDialogData,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private toastService: ToastService,
@@ -32,7 +32,7 @@ export class BulkEnableSecretsManagerDialogComponent implements OnInit {
}
submit = async () => {
await this.organizationUserService.putOrganizationUserBulkEnableSecretsManager(
await this.organizationUserApiService.putOrganizationUserBulkEnableSecretsManager(
this.data.orgId,
this.dataSource.data.map((u) => u.id),
);

View File

@@ -1,8 +1,8 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
@@ -33,7 +33,7 @@ export class BulkRemoveComponent {
@Inject(DIALOG_DATA) protected data: BulkRemoveDialogData,
protected apiService: ApiService,
protected i18nService: I18nService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
) {
this.organizationId = data.organizationId;
this.users = data.users;
@@ -60,7 +60,7 @@ export class BulkRemoveComponent {
};
protected async removeUsers() {
return await this.organizationUserService.removeManyOrganizationUsers(
return await this.organizationUserApiService.removeManyOrganizationUsers(
this.organizationId,
this.users.map((user) => user.id),
);

View File

@@ -1,7 +1,7 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService } from "@bitwarden/components";
@@ -32,7 +32,7 @@ export class BulkRestoreRevokeComponent {
constructor(
protected i18nService: I18nService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
) {
this.isRevoking = data.isRevoking;
@@ -66,12 +66,12 @@ export class BulkRestoreRevokeComponent {
protected async performBulkUserAction() {
const userIds = this.users.map((user) => user.id);
if (this.isRevoking) {
return await this.organizationUserService.revokeManyOrganizationUsers(
return await this.organizationUserApiService.revokeManyOrganizationUsers(
this.organizationId,
userIds,
);
} else {
return await this.organizationUserService.restoreManyOrganizationUsers(
return await this.organizationUserApiService.restoreManyOrganizationUsers(
this.organizationId,
userIds,
);

View File

@@ -1,7 +1,7 @@
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
ProviderUserStatusType,

View File

@@ -13,8 +13,8 @@ import {
takeUntil,
} from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
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 {
OrganizationUserStatusType,
OrganizationUserType,
@@ -139,7 +139,7 @@ export class MemberDialogComponent implements OnDestroy {
private collectionAdminService: CollectionAdminService,
private groupService: GroupService,
private userService: UserAdminService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private dialogService: DialogService,
private accountService: AccountService,
organizationService: OrganizationService,
@@ -491,7 +491,7 @@ export class MemberDialogComponent implements OnDestroy {
}
}
await this.organizationUserService.removeOrganizationUser(
await this.organizationUserApiService.removeOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
@@ -528,7 +528,7 @@ export class MemberDialogComponent implements OnDestroy {
}
}
await this.organizationUserService.revokeOrganizationUser(
await this.organizationUserApiService.revokeOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
@@ -547,7 +547,7 @@ export class MemberDialogComponent implements OnDestroy {
return;
}
await this.organizationUserService.restoreOrganizationUser(
await this.organizationUserApiService.restoreOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);

View File

@@ -13,15 +13,17 @@ import {
switchMap,
} from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserConfirmRequest,
OrganizationUserUserDetailsResponse,
} from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { PolicyApiServiceAbstraction as PolicyApiService } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import {
@@ -116,7 +118,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
private syncService: SyncService,
private organizationService: OrganizationService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private router: Router,
private groupService: GroupService,
private collectionService: CollectionService,
@@ -213,7 +215,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
let collectionsPromise: Promise<Map<string, string>>;
// We don't need both groups and collections for the table, so only load one
const userPromise = this.organizationUserService.getAllUsers(this.organization.id, {
const userPromise = this.organizationUserApiService.getAllUsers(this.organization.id, {
includeGroups: this.organization.useGroups,
includeCollections: !this.organization.useGroups,
});
@@ -270,19 +272,19 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
removeUser(id: string): Promise<void> {
return this.organizationUserService.removeOrganizationUser(this.organization.id, id);
return this.organizationUserApiService.removeOrganizationUser(this.organization.id, id);
}
revokeUser(id: string): Promise<void> {
return this.organizationUserService.revokeOrganizationUser(this.organization.id, id);
return this.organizationUserApiService.revokeOrganizationUser(this.organization.id, id);
}
restoreUser(id: string): Promise<void> {
return this.organizationUserService.restoreOrganizationUser(this.organization.id, id);
return this.organizationUserApiService.restoreOrganizationUser(this.organization.id, id);
}
reinviteUser(id: string): Promise<void> {
return this.organizationUserService.postOrganizationUserReinvite(this.organization.id, id);
return this.organizationUserApiService.postOrganizationUserReinvite(this.organization.id, id);
}
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
@@ -290,7 +292,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey);
const request = new OrganizationUserConfirmRequest();
request.key = key.encryptedString;
await this.organizationUserService.postOrganizationUserConfirm(
await this.organizationUserApiService.postOrganizationUserConfirm(
this.organization.id,
user.id,
request,
@@ -585,7 +587,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}
try {
const response = this.organizationUserService.postManyOrganizationUserReinvite(
const response = this.organizationUserApiService.postManyOrganizationUserReinvite(
this.organization.id,
filteredUsers.map((user) => user.id),
);

View File

@@ -1,8 +1,10 @@
import { mock, MockProxy } from "jest-mock-extended";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordDetailsResponse,
} from "@bitwarden/admin-console/common";
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 { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
@@ -24,7 +26,7 @@ describe("OrganizationUserResetPasswordService", () => {
let cryptoService: MockProxy<CryptoService>;
let encryptService: MockProxy<EncryptService>;
let organizationService: MockProxy<OrganizationService>;
let organizationUserService: MockProxy<OrganizationUserService>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let organizationApiService: MockProxy<OrganizationApiService>;
let i18nService: MockProxy<I18nService>;
@@ -32,7 +34,7 @@ describe("OrganizationUserResetPasswordService", () => {
cryptoService = mock<CryptoService>();
encryptService = mock<EncryptService>();
organizationService = mock<OrganizationService>();
organizationUserService = mock<OrganizationUserService>();
organizationUserApiService = mock<OrganizationUserApiService>();
organizationApiService = mock<OrganizationApiService>();
i18nService = mock<I18nService>();
@@ -40,7 +42,7 @@ describe("OrganizationUserResetPasswordService", () => {
cryptoService,
encryptService,
organizationService,
organizationUserService,
organizationUserApiService,
organizationApiService,
i18nService,
);
@@ -112,7 +114,7 @@ describe("OrganizationUserResetPasswordService", () => {
const mockOrgId = "test-org-id";
beforeEach(() => {
organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue(
organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(
new OrganizationUserResetPasswordDetailsResponse({
kdf: KdfType.PBKDF2_SHA256,
kdfIterations: 5000,
@@ -140,11 +142,11 @@ describe("OrganizationUserResetPasswordService", () => {
it("should reset the user's master password", async () => {
await sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId);
expect(organizationUserService.putOrganizationUserResetPassword).toHaveBeenCalled();
expect(organizationUserApiService.putOrganizationUserResetPassword).toHaveBeenCalled();
});
it("should throw an error if the user details are null", async () => {
organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null);
organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null);
await expect(
sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId),
).rejects.toThrow();

View File

@@ -1,13 +1,13 @@
import { Injectable } from "@angular/core";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordRequest,
OrganizationUserResetPasswordWithIdRequest,
} from "@bitwarden/admin-console/common";
import { UserKeyRotationDataProvider } from "@bitwarden/auth/common";
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 { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import {
OrganizationUserResetPasswordRequest,
OrganizationUserResetPasswordWithIdRequest,
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import {
Argon2KdfConfig,
KdfConfig,
@@ -33,7 +33,7 @@ export class OrganizationUserResetPasswordService
private cryptoService: CryptoService,
private encryptService: EncryptService,
private organizationService: OrganizationService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private organizationApiService: OrganizationApiServiceAbstraction,
private i18nService: I18nService,
) {}
@@ -76,7 +76,7 @@ export class OrganizationUserResetPasswordService
orgUserId: string,
orgId: string,
): Promise<void> {
const response = await this.organizationUserService.getOrganizationUserResetPasswordDetails(
const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails(
orgId,
orgUserId,
);
@@ -128,7 +128,11 @@ export class OrganizationUserResetPasswordService
request.newMasterPasswordHash = newMasterKeyHash;
// Change user's password
await this.organizationUserService.putOrganizationUserResetPassword(orgId, orgUserId, request);
await this.organizationUserApiService.putOrganizationUserResetPassword(
orgId,
orgUserId,
request,
);
}
/**

View File

@@ -0,0 +1,74 @@
import { Meta, StoryObj } from "@storybook/angular";
import { AccessSelectorComponent, PermissionMode } from "./access-selector.component";
import { AccessItemType, AccessItemValue } from "./access-selector.models";
import { default as baseComponentDefinition } from "./access-selector.stories";
import { actionsData, itemsFactory } from "./storybook-utils";
/**
* Displays the Access Selector in a dialog.
*/
export default {
title: "Web/Organizations/Access Selector/Dialog",
decorators: baseComponentDefinition.decorators,
} as Meta;
type Story = StoryObj<AccessSelectorComponent & { initialValue: AccessItemValue[] }>;
const render: Story["render"] = (args) => ({
props: {
items: [],
valueChanged: actionsData.onValueChanged,
initialValue: [],
...args,
},
template: `
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>Access selector</span>
<span bitDialogContent>
<bit-access-selector
(ngModelChange)="valueChanged($event)"
[ngModel]="initialValue"
[items]="items"
[disabled]="disabled"
[columnHeader]="columnHeader"
[showGroupColumn]="showGroupColumn"
[selectorLabelText]="selectorLabelText"
[selectorHelpText]="selectorHelpText"
[emptySelectionText]="emptySelectionText"
[permissionMode]="permissionMode"
[showMemberRoles]="showMemberRoles"
></bit-access-selector>
</span>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
<button
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"></button>
</ng-container>
</bit-dialog>
`,
});
const dialogAccessItems = itemsFactory(10, AccessItemType.Collection);
export const Dialog: Story = {
args: {
permissionMode: PermissionMode.Edit,
showMemberRoles: false,
showGroupColumn: true,
columnHeader: "Collection",
selectorLabelText: "Select Collections",
selectorHelpText: "Some helper text describing what this does",
emptySelectionText: "No collections added",
disabled: false,
initialValue: [] as any[],
items: dialogAccessItems,
},
render,
};

View File

@@ -0,0 +1,64 @@
import { FormBuilder, FormControl, FormGroup } from "@angular/forms";
import { Meta, StoryObj } from "@storybook/angular";
import { AccessSelectorComponent, PermissionMode } from "./access-selector.component";
import { AccessItemType, AccessItemValue } from "./access-selector.models";
import { default as baseComponentDefinition } from "./access-selector.stories";
import { actionsData, itemsFactory } from "./storybook-utils";
/**
* Displays the Access Selector embedded in a reactive form.
*/
export default {
title: "Web/Organizations/Access Selector/Reactive form",
decorators: baseComponentDefinition.decorators,
argTypes: {
formObj: { table: { disable: true } },
},
} as Meta;
type FormObj = { formObj: FormGroup<{ formItems: FormControl<AccessItemValue[]> }> };
type Story = StoryObj<AccessSelectorComponent & FormObj>;
const fb = new FormBuilder();
const render: Story["render"] = (args) => ({
props: {
items: [],
onSubmit: actionsData.onSubmit,
...args,
},
template: `
<form [formGroup]="formObj" (ngSubmit)="onSubmit(formObj.controls.formItems.value)">
<bit-access-selector
formControlName="formItems"
[items]="items"
[columnHeader]="columnHeader"
[selectorLabelText]="selectorLabelText"
[selectorHelpText]="selectorHelpText"
[emptySelectionText]="emptySelectionText"
[permissionMode]="permissionMode"
[showMemberRoles]="showMemberRoles"
></bit-access-selector>
<button type="submit" bitButton buttonType="primary" class="tw-mt-5">Submit</button>
</form>
`,
});
const sampleMembers = itemsFactory(10, AccessItemType.Member);
const sampleGroups = itemsFactory(6, AccessItemType.Group);
export const ReactiveForm: Story = {
args: {
formObj: fb.group({ formItems: [[{ id: "1g", type: AccessItemType.Group }]] }),
permissionMode: PermissionMode.Edit,
showMemberRoles: false,
columnHeader: "Groups/Members",
selectorLabelText: "Select groups and members",
selectorHelpText:
"Permissions set for a member will replace permissions set by that member's group",
emptySelectionText: "No members or groups added",
items: sampleGroups.concat(sampleMembers),
},
render,
};

View File

@@ -1,4 +1,4 @@
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common";
import {
OrganizationUserStatusType,
OrganizationUserType,

View File

@@ -1,13 +1,8 @@
import { importProvidersFrom } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { action } from "@storybook/addon-actions";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
OrganizationUserStatusType,
OrganizationUserType,
} from "@bitwarden/common/admin-console/enums";
import {
AvatarModule,
BadgeModule,
@@ -21,10 +16,20 @@ import {
import { PreloadedEnglishI18nModule } from "../../../../../core/tests";
import { AccessSelectorComponent } from "./access-selector.component";
import { AccessItemType, AccessItemView, CollectionPermission } from "./access-selector.models";
import { AccessSelectorComponent, PermissionMode } from "./access-selector.component";
import { AccessItemType, AccessItemValue, CollectionPermission } from "./access-selector.models";
import { actionsData, itemsFactory } from "./storybook-utils";
import { UserTypePipe } from "./user-type.pipe";
/**
* The Access Selector is used to view and edit:
* - member and group access to collections
* - members assigned to groups
*
* It is highly configurable in order to display these relationships from each perspective. For example, you can
* manage member-group relationships from the perspective of a particular member (showing all their groups) or a
* particular group (showing all its members).
*/
export default {
title: "Web/Organizations/Access Selector",
decorators: [
@@ -49,65 +54,16 @@ export default {
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
}),
],
parameters: {},
argTypes: {
formObj: { table: { disable: true } },
},
} as Meta;
// TODO: This is a workaround since this story does weird things.
type Story = StoryObj<any>;
const actionsData = {
onValueChanged: action("onValueChanged"),
onSubmit: action("onSubmit"),
};
/**
* Factory to help build semi-realistic looking items
* @param n - The number of items to build
* @param type - Which type to build
*/
const itemsFactory = (n: number, type: AccessItemType) => {
return [...Array(n)].map((_: unknown, id: number) => {
const item: AccessItemView = {
id: id.toString(),
type: type,
} as AccessItemView;
switch (item.type) {
case AccessItemType.Collection:
item.labelName = item.listName = `Collection ${id}`;
item.id = item.id + "c";
item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1);
break;
case AccessItemType.Group:
item.labelName = item.listName = `Group ${id}`;
item.id = item.id + "g";
break;
case AccessItemType.Member:
item.id = item.id + "m";
item.email = `member${id}@email.com`;
item.status = id % 3 == 0 ? 0 : 2;
item.labelName = item.status == 2 ? `Member ${id}` : item.email;
item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email;
item.role = id % 5;
break;
}
return item;
});
};
type Story = StoryObj<AccessSelectorComponent & { initialValue: AccessItemValue[] }>;
const sampleMembers = itemsFactory(10, AccessItemType.Member);
const sampleGroups = itemsFactory(6, AccessItemType.Group);
// TODO: These renders are badly handled but storybook has made it more difficult to use multiple renders in a single story.
const StandaloneAccessSelectorRender = (args: any) => ({
const render: Story["render"] = (args) => ({
props: {
items: [],
valueChanged: actionsData.onValueChanged,
initialValue: [],
...args,
},
template: `
@@ -127,49 +83,8 @@ const StandaloneAccessSelectorRender = (args: any) => ({
`,
});
const DialogAccessSelectorRender = (args: any) => ({
props: {
items: [],
valueChanged: actionsData.onValueChanged,
initialValue: [],
...args,
},
template: `
<bit-dialog [dialogSize]="dialogSize" [disablePadding]="disablePadding">
<span bitDialogTitle>Access selector</span>
<span bitDialogContent>
<bit-access-selector
(ngModelChange)="valueChanged($event)"
[ngModel]="initialValue"
[items]="items"
[disabled]="disabled"
[columnHeader]="columnHeader"
[showGroupColumn]="showGroupColumn"
[selectorLabelText]="selectorLabelText"
[selectorHelpText]="selectorHelpText"
[emptySelectionText]="emptySelectionText"
[permissionMode]="permissionMode"
[showMemberRoles]="showMemberRoles"
></bit-access-selector>
</span>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
<button
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"></button>
</ng-container>
</bit-dialog>
`,
});
const dialogAccessItems = itemsFactory(10, AccessItemType.Collection);
const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).concat([
const memberCollectionAccessItems = itemsFactory(5, AccessItemType.Collection).concat([
// These represent collection access via a group
{
id: "c1-group1",
type: AccessItemType.Collection,
@@ -190,25 +105,25 @@ const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).c
},
]);
export const Dialog: Story = {
args: {
permissionMode: "edit",
showMemberRoles: false,
showGroupColumn: true,
columnHeader: "Collection",
selectorLabelText: "Select Collections",
selectorHelpText: "Some helper text describing what this does",
emptySelectionText: "No collections added",
disabled: false,
initialValue: [] as any[],
items: dialogAccessItems,
},
render: DialogAccessSelectorRender,
};
// Simulate the current user not having permission to change access to this collection
// TODO: currently the member dialog duplicates the AccessItemValue.permission on the
// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication:
// https://bitwarden.atlassian.net/browse/PM-11590
memberCollectionAccessItems[4].readonly = true;
memberCollectionAccessItems[4].readonlyPermission = CollectionPermission.Manage;
/**
* Displays a member's collection access.
*
* This is currently used in the **Member dialog -> Collections tab**. Note that it includes collection access that the
* member has via a group.
*
* This is also used in the **Groups dialog -> Collections tab** to show a group's collection access and in this
* case the Group column is hidden.
*/
export const MemberCollectionAccess: Story = {
args: {
permissionMode: "edit",
permissionMode: PermissionMode.Edit,
showMemberRoles: false,
showGroupColumn: true,
columnHeader: "Collection",
@@ -216,22 +131,41 @@ export const MemberCollectionAccess: Story = {
selectorHelpText: "Some helper text describing what this does",
emptySelectionText: "No collections added",
disabled: false,
initialValue: [],
initialValue: [
{
id: "4c",
type: AccessItemType.Collection,
permission: CollectionPermission.Manage,
},
{
id: "2c",
type: AccessItemType.Collection,
permission: CollectionPermission.Edit,
},
],
items: memberCollectionAccessItems,
},
render: StandaloneAccessSelectorRender,
render,
};
/**
* Displays the groups a member is assigned to.
*
* This is currently used in the **Member dialog -> Groups tab**.
*/
export const MemberGroupAccess: Story = {
args: {
permissionMode: "readonly",
permissionMode: PermissionMode.Hidden,
showMemberRoles: false,
columnHeader: "Groups",
selectorLabelText: "Select Groups",
selectorHelpText: "Some helper text describing what this does",
emptySelectionText: "No groups added",
disabled: false,
initialValue: [{ id: "3g" }, { id: "0g" }],
initialValue: [
{ id: "3g", type: AccessItemType.Group },
{ id: "0g", type: AccessItemType.Group },
],
items: itemsFactory(4, AccessItemType.Group).concat([
{
id: "admin",
@@ -241,27 +175,40 @@ export const MemberGroupAccess: Story = {
},
]),
},
render: StandaloneAccessSelectorRender,
render,
};
/**
* Displays the members assigned to a group.
*
* This is currently used in the **Group dialog -> Members tab**.
*/
export const GroupMembersAccess: Story = {
args: {
permissionMode: "hidden",
permissionMode: PermissionMode.Hidden,
showMemberRoles: true,
columnHeader: "Members",
selectorLabelText: "Select Members",
selectorHelpText: "Some helper text describing what this does",
emptySelectionText: "No members added",
disabled: false,
initialValue: [{ id: "2m" }, { id: "0m" }],
initialValue: [
{ id: "2m", type: AccessItemType.Member },
{ id: "0m", type: AccessItemType.Member },
],
items: sampleMembers,
},
render: StandaloneAccessSelectorRender,
render,
};
/**
* Displays the members and groups assigned to a collection.
*
* This is currently used in the **Collection dialog -> Access tab**.
*/
export const CollectionAccess: Story = {
args: {
permissionMode: "edit",
permissionMode: PermissionMode.Edit,
showMemberRoles: false,
columnHeader: "Groups/Members",
selectorLabelText: "Select groups and members",
@@ -270,68 +217,38 @@ export const CollectionAccess: Story = {
emptySelectionText: "No members or groups added",
disabled: false,
initialValue: [
{ id: "3g", permission: CollectionPermission.EditExceptPass },
{ id: "0m", permission: CollectionPermission.View },
{ id: "3g", type: AccessItemType.Group, permission: CollectionPermission.EditExceptPass },
{ id: "0m", type: AccessItemType.Member, permission: CollectionPermission.View },
{ id: "7m", type: AccessItemType.Member, permission: CollectionPermission.Manage },
],
items: sampleGroups.concat(sampleMembers).concat([
{
id: "admin-group",
type: AccessItemType.Group,
listName: "Admin Group",
labelName: "Admin Group",
readonly: true,
},
{
id: "admin-member",
type: AccessItemType.Member,
listName: "Admin Member (admin@email.com)",
labelName: "Admin Member",
status: OrganizationUserStatusType.Confirmed,
role: OrganizationUserType.Admin,
email: "admin@email.com",
readonly: true,
},
]),
},
render: StandaloneAccessSelectorRender,
};
const fb = new FormBuilder();
const ReactiveFormAccessSelectorRender = (args: any) => ({
props: {
items: [],
onSubmit: actionsData.onSubmit,
...args,
},
template: `
<form [formGroup]="formObj" (ngSubmit)="onSubmit(formObj.controls.formItems.value)">
<bit-access-selector
formControlName="formItems"
[items]="items"
[columnHeader]="columnHeader"
[selectorLabelText]="selectorLabelText"
[selectorHelpText]="selectorHelpText"
[emptySelectionText]="emptySelectionText"
[permissionMode]="permissionMode"
[showMemberRoles]="showMemberRoles"
></bit-access-selector>
<button type="submit" bitButton buttonType="primary" class="tw-mt-5">Submit</button>
</form>
`,
});
export const ReactiveForm: Story = {
args: {
formObj: fb.group({ formItems: [[{ id: "1g" }]] }),
permissionMode: "edit",
showMemberRoles: false,
columnHeader: "Groups/Members",
selectorLabelText: "Select groups and members",
selectorHelpText:
"Permissions set for a member will replace permissions set by that member's group",
emptySelectionText: "No members or groups added",
items: sampleGroups.concat(sampleMembers),
},
render: ReactiveFormAccessSelectorRender,
render,
};
// TODO: currently the collection dialog duplicates the AccessItemValue.permission on the
// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication:
// https://bitwarden.atlassian.net/browse/PM-11590
const disabledMembers = itemsFactory(3, AccessItemType.Member);
disabledMembers[1].readonlyPermission = CollectionPermission.Manage;
disabledMembers[2].readonlyPermission = CollectionPermission.View;
const disabledGroups = itemsFactory(2, AccessItemType.Group);
disabledGroups[0].readonlyPermission = CollectionPermission.ViewExceptPass;
/**
* Displays the members and groups assigned to a collection when the control is in a disabled state.
*/
export const DisabledCollectionAccess: Story = {
args: {
...CollectionAccess.args,
disabled: true,
items: disabledGroups.concat(disabledMembers),
initialValue: [
{ id: "1m", type: AccessItemType.Member, permission: CollectionPermission.Manage },
{ id: "2m", type: AccessItemType.Member, permission: CollectionPermission.View },
{ id: "0g", type: AccessItemType.Group, permission: CollectionPermission.ViewExceptPass },
],
},
render,
};

View File

@@ -0,0 +1,44 @@
import { action } from "@storybook/addon-actions";
import { AccessItemType, AccessItemView } from "./access-selector.models";
export const actionsData = {
onValueChanged: action("onValueChanged"),
onSubmit: action("onSubmit"),
};
/**
* Factory to help build semi-realistic looking items
* @param n - The number of items to build
* @param type - Which type to build
*/
export const itemsFactory = (n: number, type: AccessItemType) => {
return [...Array(n)].map((_: unknown, id: number) => {
const item: AccessItemView = {
id: id.toString(),
type: type,
} as AccessItemView;
switch (item.type) {
case AccessItemType.Collection:
item.labelName = item.listName = `Collection ${id}`;
item.id = item.id + "c";
item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1);
break;
case AccessItemType.Group:
item.labelName = item.listName = `Group ${id}`;
item.id = item.id + "g";
break;
case AccessItemType.Member:
item.id = item.id + "m";
item.email = `member${id}@email.com`;
item.status = id % 3 == 0 ? 0 : 2;
item.labelName = item.status == 2 ? `Member ${id}` : item.email;
item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email;
item.role = id % 5;
break;
}
return item;
});
};

View File

@@ -1,6 +1,8 @@
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification";
@@ -23,7 +25,7 @@ export class EnrollMasterPasswordReset {
dialogService: DialogService,
data: EnrollMasterPasswordResetData,
resetPasswordService: OrganizationUserResetPasswordService,
organizationUserService: OrganizationUserService,
organizationUserApiService: OrganizationUserApiService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
syncService: SyncService,
@@ -50,7 +52,7 @@ export class EnrollMasterPasswordReset {
// Process the enrollment request, which is an endpoint that is
// gated by a server-side check of the master password hash
await organizationUserService.putOrganizationUserResetPasswordEnrollment(
await organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
data.organization.id,
data.organization.userId,
request,

View File

@@ -44,8 +44,8 @@ export class HintComponent extends BaseHintComponent implements OnInit {
);
}
ngOnInit(): void {
super.ngOnInit();
async ngOnInit(): Promise<void> {
await super.ngOnInit();
this.emailFormControl.setValue(this.email);
}

View File

@@ -1,4 +1,4 @@
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";
import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request";
import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request";

View File

@@ -1,7 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common";
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request";

View File

@@ -1,9 +1,9 @@
import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { MockProxy, mock } from "jest-mock-extended";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -35,7 +35,7 @@ describe("AcceptOrganizationInviteService", () => {
let policyService: MockProxy<PolicyService>;
let logService: MockProxy<LogService>;
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
let organizationUserService: MockProxy<OrganizationUserService>;
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
let i18nService: MockProxy<I18nService>;
let globalStateProvider: FakeGlobalStateProvider;
let globalState: FakeGlobalState<OrganizationInvite>;
@@ -49,7 +49,7 @@ describe("AcceptOrganizationInviteService", () => {
policyService = mock();
logService = mock();
organizationApiService = mock();
organizationUserService = mock();
organizationUserApiService = mock();
i18nService = mock();
globalStateProvider = new FakeGlobalStateProvider();
globalState = globalStateProvider.getFake(ORGANIZATION_INVITE);
@@ -63,7 +63,7 @@ describe("AcceptOrganizationInviteService", () => {
policyService,
logService,
organizationApiService,
organizationUserService,
organizationUserApiService,
i18nService,
globalStateProvider,
);
@@ -85,10 +85,10 @@ describe("AcceptOrganizationInviteService", () => {
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(organizationUserService.postOrganizationUserAcceptInit).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled();
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(null);
expect(organizationUserService.postOrganizationUserAccept).not.toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
@@ -133,10 +133,10 @@ describe("AcceptOrganizationInviteService", () => {
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
expect(globalState.nextMock).toHaveBeenCalledWith(null);
expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
@@ -161,8 +161,8 @@ describe("AcceptOrganizationInviteService", () => {
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled();
expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
expect(authService.logOut).not.toHaveBeenCalled();
});
});

View File

@@ -1,13 +1,13 @@
import { Injectable } from "@angular/core";
import { BehaviorSubject, firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 {
OrganizationUserApiService,
OrganizationUserAcceptRequest,
OrganizationUserAcceptInitRequest,
} from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
} from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
@@ -58,7 +58,7 @@ export class AcceptOrganizationInviteService {
private readonly policyService: PolicyService,
private readonly logService: LogService,
private readonly organizationApiService: OrganizationApiServiceAbstraction,
private readonly organizationUserService: OrganizationUserService,
private readonly organizationUserApiService: OrganizationUserApiService,
private readonly i18nService: I18nService,
private readonly globalStateProvider: GlobalStateProvider,
) {
@@ -121,7 +121,7 @@ export class AcceptOrganizationInviteService {
private async acceptAndInitOrganization(invite: OrganizationInvite): Promise<void> {
await this.prepareAcceptAndInitRequest(invite).then((request) =>
this.organizationUserService.postOrganizationUserAcceptInit(
this.organizationUserApiService.postOrganizationUserAcceptInit(
invite.organizationId,
invite.organizationUserId,
request,
@@ -156,7 +156,7 @@ export class AcceptOrganizationInviteService {
private async accept(invite: OrganizationInvite): Promise<void> {
await this.prepareAcceptRequest(invite).then((request) =>
this.organizationUserService.postOrganizationUserAccept(
this.organizationUserApiService.postOrganizationUserAccept(
invite.organizationId,
invite.organizationUserId,
request,

View File

@@ -220,7 +220,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
this.dialogService,
{ data: result },
);
this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onChangeStatus
this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onUpdated
.pipe(first(), takeUntil(this.destroy$))
.subscribe((enabled: boolean) => {
webAuthnComp.close();

View File

@@ -1,5 +1,5 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, EventEmitter, Inject, NgZone, Output } from "@angular/core";
import { Component, Inject, NgZone } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -33,7 +33,6 @@ interface Key {
templateUrl: "two-factor-webauthn.component.html",
})
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
@Output() onChangeStatus = new EventEmitter<boolean>();
type = TwoFactorProviderType.WebAuthn;
name: string;
keys: Key[];
@@ -85,34 +84,33 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
// Should never happen.
return Promise.reject();
}
return this.enable();
};
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
request.deviceResponse = this.webAuthnResponse;
request.id = this.keyIdAvailable;
request.name = this.formGroup.value.name;
return this.enableWebAuth(request);
};
private enableWebAuth(request: any) {
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
const response = await this.formPromise;
this.processResponse(response);
const response = await this.apiService.putTwoFactorWebAuthn(request);
this.processResponse(response);
this.toastService.showToast({
title: this.i18nService.t("success"),
message: this.i18nService.t("twoFactorProviderEnabled"),
variant: "success",
});
this.onUpdated.emit(response.enabled);
}
disable = async () => {
await this.disableWebAuth();
await this.disableMethod();
if (!this.enabled) {
this.onChangeStatus.emit(this.enabled);
this.onUpdated.emit(this.enabled);
this.dialogRef.close();
}
};
private async disableWebAuth() {
return super.disable(this.formPromise);
}
async remove(key: Key) {
if (this.keysConfiguredCount <= 1 || key.removePromise != null) {
return;
@@ -208,7 +206,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
}
}
this.enabled = response.enabled;
this.onChangeStatus.emit(this.enabled);
this.onUpdated.emit(this.enabled);
}
static open(

View File

@@ -36,7 +36,7 @@
<img src="../../images/yubikey.jpg" class="tw-rounded img-fluid tw-mb-3" alt="" />
</picture>
<bit-form-field>
<bit-label class="tw-sr-only">{{ "verificationCode" | i18n }}</bit-label>
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
<input type="password" bitInput formControlName="token" appAutofocus appInputVerbatim />
</bit-form-field>
</ng-container>

View File

@@ -1,10 +1,14 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { PaymentMethodComponent } from "../shared";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
import { PremiumComponent } from "./premium.component";
import { PremiumV2Component } from "./premium/premium-v2.component";
import { PremiumComponent } from "./premium/premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -20,11 +24,15 @@ const routes: Routes = [
component: UserSubscriptionComponent,
data: { titleId: "premiumMembership" },
},
{
path: "premium",
component: PremiumComponent,
data: { titleId: "goPremium" },
},
...featureFlaggedRoute({
defaultComponent: PremiumComponent,
flaggedComponent: PremiumV2Component,
featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
routeOptions: {
path: "premium",
data: { titleId: "goPremium" },
},
}),
{
path: "payment-method",
component: PaymentMethodComponent,

View File

@@ -5,7 +5,8 @@ import { BillingSharedModule } from "../shared";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
import { PremiumComponent } from "./premium.component";
import { PremiumV2Component } from "./premium/premium-v2.component";
import { PremiumComponent } from "./premium/premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -16,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
BillingHistoryViewComponent,
UserSubscriptionComponent,
PremiumComponent,
PremiumV2Component,
],
})
export class IndividualBillingModule {}

View File

@@ -0,0 +1,144 @@
<bit-section>
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
<bit-callout
type="info"
*ngIf="hasPremiumFromAnyOrganization$ | async"
title="{{ 'youHavePremiumAccess' | i18n }}"
icon="bwi bwi-star-f"
>
{{ "alreadyPremiumFromOrg" | i18n }}
</bit-callout>
<bit-callout type="success">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<ul class="bwi-ul">
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpStorage" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpTwoStepOptions" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpEmergency" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpReports" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpTotp" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpSupport" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpFuture" | i18n }}
</li>
</ul>
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
{{
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
}}
<a
bitLink
linkType="primary"
routerLink="/create-organization"
[queryParams]="{ plan: 'families' }"
>
{{ "bitwardenFamiliesPlan" | i18n }}
</a>
</p>
<a
bitButton
href="{{ premiumURL }}}"
target="_blank"
rel="noreferrer"
buttonType="secondary"
*ngIf="isSelfHost"
>
{{ "purchasePremium" | i18n }}
</a>
</bit-callout>
</bit-section>
<bit-section *ngIf="isSelfHost">
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
<bit-form-field>
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
<div>
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
}}
</div>
<input
bitInput
#fileSelector
type="file"
formControlName="file"
(change)="onLicenseFileSelected($event)"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" buttonType="primary" bitButton bitFormButton>
{{ "submit" | i18n }}
</button>
</form>
</bit-section>
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [bitSubmit]="submitPayment">
<bit-section>
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
<bit-form-field class="tw-col-span-6">
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
<input
bitInput
formControlName="additionalStorage"
type="number"
step="1"
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
/>
<bit-hint>{{
"additionalStorageIntervalDesc"
| i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
}}</bit-hint>
</bit-form-field>
</div>
</bit-section>
<bit-section>
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
{{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB &times;
{{ storageGBPrice | currency: "$" }} =
{{ additionalStorageCost | currency: "$" }}
<hr class="tw-my-3" />
</bit-section>
<bit-section>
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
<app-payment-v2 [showBankAccount]="false"></app-payment-v2>
<app-tax-info></app-tax-info>
<div class="tw-mb-4">
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
<!-- TODO: Currently incorrect - https://bitwarden.atlassian.net/browse/PM-11525 -->
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
</div>
</div>
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
<p bitTypography="body1">
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
</p>
<button type="submit" buttonType="primary" bitButton bitFormButton>
{{ "submit" | i18n }}
</button>
</bit-section>
</form>

View File

@@ -0,0 +1,164 @@
import { Component, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { combineLatest, concatMap, from, Observable, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { ToastService } from "@bitwarden/components";
import { PaymentV2Component } from "../../shared/payment/payment-v2.component";
import { TaxInfoComponent } from "../../shared/tax-info.component";
@Component({
templateUrl: "./premium-v2.component.html",
})
export class PremiumV2Component {
@ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
protected addOnFormGroup = new FormGroup({
additionalStorage: new FormControl<number>(0, [Validators.min(0), Validators.max(99)]),
});
protected licenseFormGroup = new FormGroup({
file: new FormControl<File>(null, [Validators.required]),
});
protected cloudWebVaultURL: string;
protected isSelfHost = false;
protected readonly familyPlanMaxUserCount = 6;
protected readonly premiumPrice = 10;
protected readonly storageGBPrice = 4;
constructor(
private activatedRoute: ActivatedRoute,
private apiService: ApiService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private environmentService: EnvironmentService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private router: Router,
private syncService: SyncService,
private toastService: ToastService,
private tokenService: TokenService,
) {
this.isSelfHost = this.platformUtilsService.isSelfHost();
this.hasPremiumFromAnyOrganization$ =
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$;
combineLatest([
this.billingAccountProfileStateService.hasPremiumPersonally$,
this.environmentService.cloudWebVaultUrl$,
])
.pipe(
takeUntilDestroyed(),
concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => {
if (hasPremiumPersonally) {
return from(this.navigateToSubscriptionPage());
}
this.cloudWebVaultURL = cloudWebVaultURL;
return of(true);
}),
)
.subscribe();
}
finalizeUpgrade = async () => {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("premiumUpdated"),
});
await this.navigateToSubscriptionPage();
};
navigateToSubscriptionPage = (): Promise<boolean> =>
this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
onLicenseFileSelected = (event: Event): void => {
const element = event.target as HTMLInputElement;
this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null;
};
submitPremiumLicense = async (): Promise<void> => {
this.licenseFormGroup.markAllAsTouched();
if (this.licenseFormGroup.invalid) {
return this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("selectFile"),
});
}
const emailVerified = await this.tokenService.getEmailVerified();
if (!emailVerified) {
return this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("verifyEmailFirst"),
});
}
const formData = new FormData();
formData.append("license", this.licenseFormGroup.value.file);
await this.apiService.postAccountLicense(formData);
await this.finalizeUpgrade();
};
submitPayment = async (): Promise<void> => {
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
if (this.taxInfoComponent.taxFormGroup.invalid) {
return;
}
const { type, token } = await this.paymentComponent.tokenize();
const formData = new FormData();
formData.append("paymentMethodType", type.toString());
formData.append("paymentToken", token);
formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString());
formData.append("country", this.taxInfoComponent.country);
formData.append("postalCode", this.taxInfoComponent.postalCode);
await this.apiService.postPremium(formData);
await this.finalizeUpgrade();
};
protected get additionalStorageCost(): number {
return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage;
}
protected get estimatedTax(): number {
return this.taxInfoComponent?.taxRate != null
? (this.taxInfoComponent.taxRate / 100) * this.subtotal
: 0;
}
protected get premiumURL(): string {
return `${this.cloudWebVaultURL}/#/settings/subscription/premium`;
}
protected get subtotal(): number {
return this.premiumPrice + this.additionalStorageCost;
}
protected get total(): number {
return this.subtotal + this.estimatedTax;
}
}

View File

@@ -69,7 +69,7 @@
<form [formGroup]="licenseForm" [bitSubmit]="submit">
<bit-form-field>
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
<div>
<div class="tw-pt-2 tw-pb-1">
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>

View File

@@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ToastService } from "@bitwarden/components";
import { PaymentComponent, TaxInfoComponent } from "../shared";
import { PaymentComponent, TaxInfoComponent } from "../../shared";
@Component({
templateUrl: "premium.component.html",

View File

@@ -48,7 +48,7 @@
}}</span>
</dd>
<dt>{{ "nextCharge" | i18n }}</dt>
<dd>
<dd *ngIf="!enableTimeThreshold">
{{
nextInvoice
? (nextInvoice.date | date: "mediumDate") +
@@ -57,6 +57,15 @@
: "-"
}}
</dd>
<dd *ngIf="enableTimeThreshold">
{{
nextInvoice
? (sub.subscription.periodEndDate | date: "mediumDate") +
", " +
(nextInvoice.amount | currency: "$")
: "-"
}}
</dd>
</dl>
</div>
<div class="tw-w-2/3" *ngIf="subscription">

View File

@@ -5,6 +5,8 @@ import { firstValueFrom, lastValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -12,10 +14,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
import {
AdjustStorageDialogV2Component,
AdjustStorageDialogV2ResultType,
} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component";
import {
AdjustStorageDialogResult,
openAdjustStorageDialog,
} from "../shared/adjust-storage.component";
} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component";
import {
OffboardingSurveyDialogResultType,
openOffboardingSurvey,
@@ -34,9 +40,17 @@ export class UserSubscriptionComponent implements OnInit {
sub: SubscriptionResponse;
selfHosted = false;
cloudWebVaultUrl: string;
enableTimeThreshold: boolean;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
);
constructor(
private apiService: ApiService,
@@ -49,6 +63,7 @@ export class UserSubscriptionComponent implements OnInit {
private environmentService: EnvironmentService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
private configService: ConfigService,
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -56,6 +71,7 @@ export class UserSubscriptionComponent implements OnInit {
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
await this.load();
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
this.firstLoaded = true;
}
@@ -150,15 +166,33 @@ export class UserSubscriptionComponent implements OnInit {
};
adjustStorage = async (add: boolean) => {
const dialogRef = openAdjustStorageDialog(this.dialogService, {
data: {
storageGbPrice: 4,
add: add,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AdjustStorageDialogResult.Adjusted) {
await this.load();
const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$);
if (deprecateStripeSourcesAPI) {
const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, {
data: {
price: 4,
cadence: "year",
type: add ? "Add" : "Remove",
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AdjustStorageDialogV2ResultType.Submitted) {
await this.load();
}
} else {
const dialogRef = openAdjustStorageDialog(this.dialogService, {
data: {
storageGbPrice: 4,
add: add,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AdjustStorageDialogResult.Adjusted) {
await this.load();
}
}
};

View File

@@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
@@ -11,7 +11,7 @@ import { ToastService } from "@bitwarden/components";
selector: "app-adjust-subscription",
templateUrl: "adjust-subscription.component.html",
})
export class AdjustSubscription {
export class AdjustSubscription implements OnInit, OnDestroy {
@Input() organizationId: string;
@Input() maxAutoscaleSeats: number;
@Input() currentSeatCount: number;
@@ -19,6 +19,8 @@ export class AdjustSubscription {
@Input() interval = "year";
@Output() onAdjusted = new EventEmitter();
private destroy$ = new Subject<void>();
adjustSubscriptionForm = this.formBuilder.group({
newSeatCount: [0, [Validators.min(0)]],
limitSubscription: [false],
@@ -30,30 +32,25 @@ export class AdjustSubscription {
private organizationApiService: OrganizationApiServiceAbstraction,
private formBuilder: FormBuilder,
private toastService: ToastService,
) {
) {}
ngOnInit() {
this.adjustSubscriptionForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
const maxAutoscaleSeatsControl = this.adjustSubscriptionForm.controls.newMaxSeats;
if (value.limitSubscription) {
maxAutoscaleSeatsControl.setValidators([Validators.min(value.newSeatCount)]);
maxAutoscaleSeatsControl.enable({ emitEvent: false });
} else {
maxAutoscaleSeatsControl.disable({ emitEvent: false });
}
});
this.adjustSubscriptionForm.patchValue({
newSeatCount: this.currentSeatCount,
limitSubscription: this.maxAutoscaleSeats != null,
newMaxSeats: this.maxAutoscaleSeats,
limitSubscription: this.maxAutoscaleSeats != null,
});
this.adjustSubscriptionForm
.get("limitSubscription")
.valueChanges.pipe(takeUntilDestroyed())
.subscribe((value: boolean) => {
if (value) {
this.adjustSubscriptionForm
.get("newMaxSeats")
.addValidators([
Validators.min(
this.adjustSubscriptionForm.value.newSeatCount == null
? 1
: this.adjustSubscriptionForm.value.newSeatCount,
),
Validators.required,
]);
}
this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity();
});
}
submit = async () => {
@@ -99,4 +96,9 @@ export class AdjustSubscription {
get limitSubscription(): boolean {
return this.adjustSubscriptionForm.value.limitSubscription;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -53,9 +53,12 @@
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
{{ "subscriptionExpiration" | i18n }}
</dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="!enableTimeThreshold">
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
</dd>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="enableTimeThreshold">
{{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }}
</dd>
</ng-container>
</dl>
</ng-container>

View File

@@ -18,10 +18,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
import {
AdjustStorageDialogV2Component,
AdjustStorageDialogV2ResultType,
} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component";
import {
AdjustStorageDialogResult,
openAdjustStorageDialog,
} from "../shared/adjust-storage.component";
} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component";
import {
OffboardingSurveyDialogResultType,
openOffboardingSurvey,
@@ -71,6 +75,10 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.EnableUpgradePasswordManagerSub,
);
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
);
constructor(
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
@@ -458,17 +466,36 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
adjustStorage = (add: boolean) => {
return async () => {
const dialogRef = openAdjustStorageDialog(this.dialogService, {
data: {
storageGbPrice: this.storageGbPrice,
add: add,
organizationId: this.organizationId,
interval: this.billingInterval,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AdjustStorageDialogResult.Adjusted) {
await this.load();
const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$);
if (deprecateStripeSourcesAPI) {
const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, {
data: {
price: this.storageGbPrice,
cadence: this.billingInterval,
type: add ? "Add" : "Remove",
organizationId: this.organizationId,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AdjustStorageDialogV2ResultType.Submitted) {
await this.load();
}
} else {
const dialogRef = openAdjustStorageDialog(this.dialogService, {
data: {
storageGbPrice: this.storageGbPrice,
add: add,
organizationId: this.organizationId,
interval: this.billingInterval,
},
});
const result = await lastValueFrom(dialogRef.closed);
if (result === AdjustStorageDialogResult.Adjusted) {
await this.load();
}
}
};
};

View File

@@ -0,0 +1,34 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog [title]="title">
<ng-container bitDialogContent>
<p bitTypography="body1">{{ body }}</p>
<div class="tw-grid two-grid-cols-12">
<bit-form-field class="tw-col-span-7">
<bit-label>{{ storageFieldLabel }}</bit-label>
<input bitInput type="number" formControlName="storage" />
<bit-hint *ngIf="dialogParams.type === 'Add'">
<!-- Total: 10 GB × $0.50 = $5.00 /month -->
<strong>{{ "total" | i18n }}</strong>
{{ this.formGroup.value.storage }} GB &times; {{ this.price | currency: "$" }} =
{{ this.price * this.formGroup.value.storage | currency: "$" }} /
{{ this.cadence | i18n }}
</bit-hint>
</bit-form-field>
</div>
</ng-container>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
<button
type="button"
bitButton
bitFormButton
buttonType="secondary"
[bitDialogClose]="ResultType.Closed"
>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,104 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { FormControl, FormGroup, Validators } 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 { StorageRequest } from "@bitwarden/common/models/request/storage.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService } from "@bitwarden/components";
export interface AdjustStorageDialogV2Params {
price: number;
cadence: "month" | "year";
type: "Add" | "Remove";
organizationId?: string;
}
export enum AdjustStorageDialogV2ResultType {
Submitted = "submitted",
Closed = "closed",
}
@Component({
templateUrl: "./adjust-storage-dialog-v2.component.html",
})
export class AdjustStorageDialogV2Component {
protected formGroup = new FormGroup({
storage: new FormControl<number>(0, [
Validators.required,
Validators.min(0),
Validators.max(99),
]),
});
protected organizationId?: string;
protected price: number;
protected cadence: "month" | "year";
protected title: string;
protected body: string;
protected storageFieldLabel: string;
protected ResultType = AdjustStorageDialogV2ResultType;
constructor(
private apiService: ApiService,
@Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params,
private dialogRef: DialogRef<AdjustStorageDialogV2ResultType>,
private i18nService: I18nService,
private organizationApiService: OrganizationApiServiceAbstraction,
private toastService: ToastService,
) {
this.price = this.dialogParams.price;
this.cadence = this.dialogParams.cadence;
this.organizationId = this.dialogParams.organizationId;
switch (this.dialogParams.type) {
case "Add":
this.title = this.i18nService.t("addStorage");
this.body = this.i18nService.t("storageAddNote");
this.storageFieldLabel = this.i18nService.t("gbStorageAdd");
break;
case "Remove":
this.title = this.i18nService.t("removeStorage");
this.body = this.i18nService.t("storageRemoveNote");
this.storageFieldLabel = this.i18nService.t("gbStorageRemove");
break;
}
}
submit = async () => {
const request = new StorageRequest();
switch (this.dialogParams.type) {
case "Add":
request.storageGbAdjustment = this.formGroup.value.storage;
break;
case "Remove":
request.storageGbAdjustment = this.formGroup.value.storage * -1;
break;
}
if (this.organizationId) {
await this.organizationApiService.updateStorage(this.organizationId, request);
} else {
await this.apiService.postAccountStorage(request);
}
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()),
});
this.dialogRef.close(this.ResultType.Submitted);
};
static open = (
dialogService: DialogService,
dialogConfig: DialogConfig<AdjustStorageDialogV2Params>,
) =>
dialogService.open<AdjustStorageDialogV2ResultType>(
AdjustStorageDialogV2Component,
dialogConfig,
);
}

View File

@@ -12,7 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { PaymentComponent } from "./payment/payment.component";
import { PaymentComponent } from "../payment/payment.component";
export interface AdjustStorageDialogData {
storageGbPrice: number;
@@ -27,9 +27,9 @@ export enum AdjustStorageDialogResult {
}
@Component({
templateUrl: "adjust-storage.component.html",
templateUrl: "adjust-storage-dialog.component.html",
})
export class AdjustStorageComponent {
export class AdjustStorageDialogComponent {
storageGbPrice: number;
add: boolean;
organizationId: string;
@@ -126,5 +126,5 @@ export function openAdjustStorageDialog(
dialogService: DialogService,
config: DialogConfig<AdjustStorageDialogData>,
) {
return dialogService.open<AdjustStorageDialogResult>(AdjustStorageComponent, config);
return dialogService.open<AdjustStorageDialogResult>(AdjustStorageDialogComponent, config);
}

View File

@@ -6,7 +6,8 @@ import { SharedModule } from "../../shared";
import { AddCreditDialogComponent } from "./add-credit-dialog.component";
import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component";
import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component";
import { AdjustStorageComponent } from "./adjust-storage.component";
import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component";
import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component";
import { BillingHistoryComponent } from "./billing-history.component";
import { OffboardingSurveyComponent } from "./offboarding-survey.component";
import { PaymentV2Component } from "./payment/payment-v2.component";
@@ -30,7 +31,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
declarations: [
AddCreditDialogComponent,
AdjustPaymentDialogComponent,
AdjustStorageComponent,
AdjustStorageDialogComponent,
BillingHistoryComponent,
PaymentMethodComponent,
SecretsManagerSubscribeComponent,
@@ -38,18 +39,20 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
UpdateLicenseDialogComponent,
OffboardingSurveyComponent,
AdjustPaymentDialogV2Component,
AdjustStorageDialogV2Component,
],
exports: [
SharedModule,
PaymentComponent,
TaxInfoComponent,
AdjustStorageComponent,
AdjustStorageDialogComponent,
BillingHistoryComponent,
SecretsManagerSubscribeComponent,
UpdateLicenseComponent,
UpdateLicenseDialogComponent,
OffboardingSurveyComponent,
VerifyBankAccountComponent,
PaymentV2Component,
],
})
export class BillingSharedModule {}

View File

@@ -1,7 +1,7 @@
<form [formGroup]="updateLicenseForm" [bitSubmit]="submit">
<bit-form-field>
<bit-label *ngIf="showAutomaticSyncAndManualUpload">{{ "licenseFile" | i18n }}</bit-label>
<div>
<div class="tw-pb-1 tw-pt-2">
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>

View File

@@ -1,6 +1,7 @@
import { CommonModule } from "@angular/common";
import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
SECURE_STORAGE,
@@ -25,7 +26,6 @@ import {
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
@@ -33,6 +33,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@@ -42,6 +43,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
@@ -205,10 +207,15 @@ const safeProviders: SafeProvider[] = [
KdfConfigService,
InternalMasterPasswordServiceAbstraction,
OrganizationApiServiceAbstraction,
OrganizationUserService,
OrganizationUserApiService,
InternalUserDecryptionOptionsServiceAbstraction,
],
}),
safeProvider({
provide: AppIdService,
useClass: DefaultAppIdService,
deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService],
}),
safeProvider({
provide: LoginService,
useClass: WebLoginService,

View File

@@ -36,13 +36,13 @@
<bit-menu #accountMenu>
<div class="tw-flex tw-min-w-52 tw-max-w-72 tw-flex-col">
<div
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-info"
class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-muted"
appStopProp
>
<dynamic-avatar [id]="account.id" [text]="account | userName"></dynamic-avatar>
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
<span>{{ "loggedInAs" | i18n }}</span>
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap tw-text-muted">
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap">
{{ account | userName }}
</small>
</div>

View File

@@ -18,7 +18,7 @@
*ngFor="let d of custom; let i = index; trackBy: indexTrackBy"
>
<bit-form-field class="tw-flex-1 !tw-mb-0" formGroupName="{{ i }}">
<bit-label class="tw-sr-only">{{ "customDomainX" | i18n: i + 1 }} </bit-label>
<bit-label>{{ "customDomainX" | i18n: i + 1 }} </bit-label>
<textarea
rows="2"
bitInput

View File

@@ -13,9 +13,11 @@ import {
} from "rxjs";
import { first } from "rxjs/operators";
import {
OrganizationUserApiService,
OrganizationUserUserDetailsResponse,
} from "@bitwarden/admin-console/common";
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 { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses/organization-user.response";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -106,7 +108,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
private collectionAdminService: CollectionAdminService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,
) {
@@ -155,7 +157,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
collections: this.collectionAdminService.getAll(orgId),
groups: groups$,
// Collection(s) needed to map readonlypermission for (potential) access selector disabled state
users: this.organizationUserService.getAllUsers(orgId, { includeCollections: true }),
users: this.organizationUserApiService.getAllUsers(orgId, { includeCollections: true }),
})
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
.subscribe(({ organization, collections: allCollections, groups, users }) => {

View File

@@ -75,7 +75,7 @@
</button>
<button type="button" bitMenuItem (click)="access(false)">
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
{{ "editAccess" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="!canEditCollection && canViewCollectionInfo">

View File

@@ -47,7 +47,7 @@
(click)="bulkEditCollectionAccess()"
>
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
{{ "access" | i18n }}
{{ "editAccess" | i18n }}
</button>
<button
*ngIf="
@@ -81,14 +81,7 @@
>
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{
(showBulkTrashOptions
? "permanentlyDeleteSelected"
: vaultBulkManagementActionEnabled
? "delete"
: "deleteSelected"
) | i18n
}}
{{ (showBulkTrashOptions ? "permanentlyDeleteSelected" : "delete") | i18n }}
</span>
</button>
</bit-menu>

View File

@@ -24,7 +24,7 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { isCardExpired } from "@bitwarden/common/vault/utils";
import { DialogService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -123,7 +123,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
this.cardIsExpired = extensionRefreshEnabled && this.isCardExpiryInThePast();
this.cardIsExpired = extensionRefreshEnabled && isCardExpired(this.cipher.card);
}
ngOnDestroy() {
@@ -235,24 +235,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.viewingPasswordHistory = !this.viewingPasswordHistory;
}
isCardExpiryInThePast() {
if (this.cipher.card) {
const { expMonth, expYear }: CardView = this.cipher.card;
if (expYear && expMonth) {
// `Date` months are zero-indexed
const parsedMonth = parseInt(expMonth) - 1;
const parsedYear = parseInt(expYear);
// First day of the next month minus one, to get last day of the card month
const cardExpiry = new Date(parsedYear, parsedMonth + 1, 0);
const now = new Date();
return cardExpiry < now;
}
}
}
protected cleanUp() {
if (this.totpInterval) {
window.clearInterval(this.totpInterval);

View File

@@ -1,11 +1,13 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
@@ -45,7 +47,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
private policyService: PolicyService,
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private dialogService: DialogService,
private resetPasswordService: OrganizationUserResetPasswordService,
@@ -153,7 +155,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
this.dialogService,
{ organization: org },
this.resetPasswordService,
this.organizationUserService,
this.organizationUserApiService,
this.platformUtilsService,
this.i18nService,
this.syncService,
@@ -166,11 +168,12 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.masterPasswordHash = "ignored";
request.resetPasswordKey = null;
this.actionPromise = this.organizationUserService.putOrganizationUserResetPasswordEnrollment(
this.organization.id,
this.organization.userId,
request,
);
this.actionPromise =
this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
this.organization.id,
this.organization.userId,
request,
);
try {
await this.actionPromise;
this.platformUtilsService.showToast(

View File

@@ -1,4 +1,4 @@
<bit-dialog dialogSize="large">
<bit-dialog dialogSize="large" background="alt">
<span bitDialogTitle>
{{ cipherTypeString }}
</span>

View File

@@ -3,8 +3,8 @@ import { Component, Inject, OnDestroy } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -60,7 +60,7 @@ export class BulkCollectionsDialogComponent implements OnDestroy {
private formBuilder: FormBuilder,
private organizationService: OrganizationService,
private groupService: GroupService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private collectionAdminService: CollectionAdminService,
@@ -79,7 +79,7 @@ export class BulkCollectionsDialogComponent implements OnDestroy {
combineLatest([
organization$,
groups$,
this.organizationUserService.getAllUsers(this.params.organizationId),
this.organizationUserApiService.getAllUsers(this.params.organizationId),
])
.pipe(takeUntil(this.destroy$))
.subscribe(([organization, groups, users]) => {

View File

@@ -131,6 +131,7 @@
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
</ng-container>
<ng-container *ngSwitchCase="false">
<button type="button" bitMenuItem (click)="addCipher()">

View File

@@ -30,14 +30,16 @@ import {
tap,
} from "rxjs/operators";
import {
OrganizationUserApiService,
OrganizationUserUserDetailsResponse,
} from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
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 { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EventType } from "@bitwarden/common/enums";
@@ -215,7 +217,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private totpService: TotpService,
private apiService: ApiService,
private collectionService: CollectionService,
private organizationUserService: OrganizationUserService,
private organizationUserApiService: OrganizationUserApiService,
protected configService: ConfigService,
private toastService: ToastService,
private accountService: AccountService,
@@ -395,7 +397,7 @@ export class VaultComponent implements OnInit, OnDestroy {
// This will be passed into the usersCanManage call
this.orgRevokedUsers = (
await this.organizationUserService.getAllUsers(await firstValueFrom(organizationId$))
await this.organizationUserApiService.getAllUsers(await firstValueFrom(organizationId$))
).data.filter((user: OrganizationUserUserDetailsResponse) => {
return user.status === -1;
});

View File

@@ -24,5 +24,16 @@ describe("CollectionUtils Service", () => {
expect(result[0].node.name).toBe("Parent");
expect(result[0].children[0].node.name).toBe("Child");
});
it("should return an empty array if no collections are provided", () => {
// Arrange
const collections: CollectionView[] = [];
// Act
const result = getNestedCollectionTree(collections);
// Assert
expect(result).toEqual([]);
});
});
});

View File

@@ -14,6 +14,10 @@ export function getNestedCollectionTree(collections: CollectionView[]): TreeNode
export function getNestedCollectionTree(
collections: (CollectionView | CollectionAdminView)[],
): TreeNode<CollectionView | CollectionAdminView>[] {
if (!collections) {
return [];
}
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
// modifies the names of collections.
// These changes risk affecting collections store in StateService.

View File

@@ -48,6 +48,30 @@
"loginCredentials": {
"message": "Login credentials"
},
"personalDetails": {
"message": "Personal details"
},
"identification": {
"message": "Identification"
},
"contactInfo": {
"message": "Contact info"
},
"cardDetails": {
"message": "Card details"
},
"cardBrandDetails": {
"message": "$BRAND$ details",
"placeholders": {
"brand": {
"content": "$1",
"example": "Visa"
}
}
},
"itemHistory": {
"message": "Item history"
},
"authenticatorKey": {
"message": "Authenticator key"
},
@@ -439,6 +463,9 @@
"fullName": {
"message": "Full name"
},
"address": {
"message": "Address"
},
"address1": {
"message": "Address 1"
},
@@ -9028,5 +9055,8 @@
},
"additionalContentAvailable": {
"message": "Additional content is available"
},
"editAccess": {
"message": "Edit access"
}
}