1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00

Merge branch 'main' into auth/pm-8111/browser-refresh-login-component

This commit is contained in:
rr-bw
2024-08-21 11:15:03 -07:00
64 changed files with 623 additions and 653 deletions

View File

@@ -21,6 +21,7 @@ import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-con
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -72,6 +73,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
private ssoLoginService: SsoLoginServiceAbstraction,
dialogService: DialogService,
kdfConfigService: KdfConfigService,
private encryptService: EncryptService,
) {
super(
i18nService,
@@ -160,7 +162,23 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
// Existing JIT provisioned user in a MP encryption org setting first password
// Users in this state will not already have a user asymmetric key pair so must create it for them
// We don't want to re-create the user key pair if the user already has one (TDE user case)
newKeyPair = await this.cryptoService.makeKeyPair(userKey[0]);
// in case we have a local private key, and are not sure whether it has been posted to the server, we post the local private key instead of generating a new one
const existingUserPrivateKey = (await firstValueFrom(
this.cryptoService.userPrivateKey$(this.userId),
)) as Uint8Array;
const existingUserPublicKey = await firstValueFrom(
this.cryptoService.userPublicKey$(this.userId),
);
if (existingUserPrivateKey != null && existingUserPublicKey != null) {
const existingUserPublicKeyB64 = Utils.fromBufferToB64(existingUserPublicKey);
newKeyPair = [
existingUserPublicKeyB64,
await this.encryptService.encrypt(existingUserPrivateKey, userKey[0]),
];
} else {
newKeyPair = await this.cryptoService.makeKeyPair(userKey[0]);
}
keysRequest = new KeysRequest(newKeyPair[0], newKeyPair[1].encryptedString);
}

View File

@@ -123,14 +123,12 @@ import {
BillingApiServiceAbstraction,
BraintreeServiceAbstraction,
OrganizationBillingServiceAbstraction,
PaymentMethodWarningsServiceAbstraction,
StripeServiceAbstraction,
} from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service";
import { BraintreeService } from "@bitwarden/common/billing/services/payment-processors/braintree.service";
import { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service";
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
@@ -1201,11 +1199,6 @@ const safeProviders: SafeProvider[] = [
useClass: BillingApiService,
deps: [ApiServiceAbstraction, LogService, ToastService],
}),
safeProvider({
provide: PaymentMethodWarningsServiceAbstraction,
useClass: PaymentMethodWarningsService,
deps: [BillingApiServiceAbstraction, StateProvider],
}),
safeProvider({
provide: BillingAccountProfileStateService,
useClass: DefaultBillingAccountProfileStateService,

View File

@@ -0,0 +1,32 @@
import { Type, inject } from "@angular/core";
import { Route, Routes } from "@angular/router";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { componentRouteSwap } from "./component-route-swap";
/**
* Helper function to swap between two components based on the ExtensionRefresh feature flag.
* @param defaultComponent - The current non-refreshed component to render.
* @param refreshedComponent - The new refreshed component to render.
* @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
* @param altOptions - The alt route options to apply to the alt component.
*/
export function extensionRefreshSwap(
defaultComponent: Type<any>,
refreshedComponent: Type<any>,
options: Route,
altOptions?: Route,
): Routes {
return componentRouteSwap(
defaultComponent,
refreshedComponent,
async () => {
const configService = inject(ConfigService);
return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
},
options,
altOptions,
);
}

View File

@@ -8,7 +8,6 @@ import { PaymentInformationResponse } from "@bitwarden/common/billing/models/res
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
import { PlanResponse } from "../../billing/models/response/plan.response";
import { ListResponse } from "../../models/response/list.response";
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
@@ -34,8 +33,6 @@ export abstract class BillingApiServiceAbstraction {
organizationId: string,
) => Promise<OrganizationBillingMetadataResponse>;
getOrganizationBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>;
getPlans: () => Promise<ListResponse<PlanResponse>>;
getProviderClientInvoiceReport: (providerId: string, invoiceId: string) => Promise<string>;

View File

@@ -1,7 +1,6 @@
export * from "./account/billing-account-profile-state.service";
export * from "./billilng-api.service.abstraction";
export * from "./organization-billing.service";
export * from "./payment-method-warnings-service.abstraction";
export * from "./payment-processors/braintree.service.abstraction";
export * from "./payment-processors/stripe.service.abstraction";
export * from "./provider-billing.service.abstraction";

View File

@@ -1,31 +0,0 @@
import { Observable } from "rxjs";
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
export abstract class PaymentMethodWarningsServiceAbstraction {
/**
* An {@link Observable} record in the {@link ActiveUserState} of the user's organization IDs each mapped to their respective {@link PaymentMethodWarning}.
*/
paymentMethodWarnings$: Observable<Record<string, PaymentMethodWarning>>;
/**
* Updates the {@link ActiveUserState} by setting `acknowledged` to `true` for the {@link PaymentMethodWarning} represented by the provided organization ID.
* @param organizationId - The ID of the organization whose warning you'd like to acknowledge.
*/
acknowledge: (organizationId: string) => Promise<void>;
/**
* Updates the {@link ActiveUserState} by setting `risksSubscriptionFailure` to `false` for the {@link PaymentMethodWarning} represented by the provided organization ID.
* @param organizationId - The ID of the organization whose subscription risk you'd like to remove.
*/
removeSubscriptionRisk: (organizationId: string) => Promise<void>;
/**
* Clears the {@link PaymentMethodWarning} record from the {@link ActiveUserState}.
*/
clear: () => Promise<void>;
/**
* Tries to retrieve the {@link PaymentMethodWarning} for the provided organization ID from the {@link ActiveUserState}.
* If the warning does not exist, or if the warning has been in state for longer than a week, fetches the current {@link OrganizationBillingStatusResponse} for the organization
* from the API and uses it to update the warning in state.
* @param organizationId - The ID of the organization whose {@link PaymentMethodWarning} you'd like to update.
*/
update: (organizationId: string) => Promise<void>;
}

View File

@@ -1,14 +0,0 @@
import { BILLING_DISK, UserKeyDefinition } from "../../platform/state";
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
export const PAYMENT_METHOD_WARNINGS_KEY = UserKeyDefinition.record<PaymentMethodWarning>(
BILLING_DISK,
"paymentMethodWarnings",
{
deserializer: (warnings) => ({
...warnings,
savedAt: new Date(warnings.savedAt),
}),
clearOn: ["logout"],
},
);

View File

@@ -1,5 +1,4 @@
export * from "./bank-account";
export * from "./masked-payment-method";
export * from "./payment-method-warning";
export * from "./tax-information";
export * from "./tokenized-payment-method";

View File

@@ -1,6 +0,0 @@
export type PaymentMethodWarning = {
organizationName: string;
risksSubscriptionFailure: boolean;
acknowledged: boolean;
savedAt: Date;
};

View File

@@ -1,15 +0,0 @@
import { BaseResponse } from "../../../models/response/base.response";
export class OrganizationBillingStatusResponse extends BaseResponse {
organizationId: string;
organizationName: string;
risksSubscriptionFailure: boolean;
constructor(response: any) {
super(response);
this.organizationId = this.getResponseProperty("OrganizationId");
this.organizationName = this.getResponseProperty("OrganizationName");
this.risksSubscriptionFailure = this.getResponseProperty("RisksSubscriptionFailure");
}
}

View File

@@ -12,7 +12,6 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/su
import { TokenizedPaymentMethodRequest } from "../../billing/models/request/tokenized-payment-method.request";
import { VerifyBankAccountRequest } from "../../billing/models/request/verify-bank-account.request";
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
import { PaymentInformationResponse } from "../../billing/models/response/payment-information.response";
import { PlanResponse } from "../../billing/models/response/plan.response";
import { ListResponse } from "../../models/response/list.response";
@@ -72,17 +71,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
return response as string;
}
async getOrganizationBillingStatus(id: string): Promise<OrganizationBillingStatusResponse> {
const r = await this.apiService.send(
"GET",
"/organizations/" + id + "/billing-status",
null,
true,
true,
);
return new OrganizationBillingStatusResponse(r);
}
async getOrganizationBillingMetadata(
organizationId: string,
): Promise<OrganizationBillingMetadataResponse> {

View File

@@ -1,186 +0,0 @@
import { any, mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import { FakeActiveUserState } from "../../../spec/fake-state";
import { Utils } from "../../platform/misc/utils";
import { UserId } from "../../types/guid";
import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction";
import { PAYMENT_METHOD_WARNINGS_KEY } from "../models/billing-keys.state";
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
import { OrganizationBillingStatusResponse } from "../models/response/organization-billing-status.response";
import { PaymentMethodWarningsService } from "./payment-method-warnings.service";
describe("Payment Method Warnings Service", () => {
let paymentMethodWarningsService: PaymentMethodWarningsService;
let billingApiService: MockProxy<BillingApiService>;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
let stateProvider: FakeStateProvider;
let activeUserState: FakeActiveUserState<Record<string, PaymentMethodWarning>>;
function getPastDate(daysAgo: number) {
const date = new Date();
date.setDate(date.getDate() - daysAgo);
return date;
}
const getBillingStatusResponse = (organizationId: string) =>
new OrganizationBillingStatusResponse({
OrganizationId: organizationId,
OrganizationName: "Teams Organization",
RisksSubscriptionFailure: true,
});
beforeEach(() => {
accountService = mockAccountServiceWith(mockUserId);
stateProvider = new FakeStateProvider(accountService);
activeUserState = stateProvider.activeUser.getFake(PAYMENT_METHOD_WARNINGS_KEY);
billingApiService = mock<BillingApiService>();
paymentMethodWarningsService = new PaymentMethodWarningsService(
billingApiService,
stateProvider,
);
});
it("acknowledge", async () => {
const organizationId = "1";
const state: Record<string, PaymentMethodWarning> = {
[organizationId]: {
organizationName: "Teams Organization",
risksSubscriptionFailure: true,
acknowledged: false,
savedAt: getPastDate(3),
},
};
activeUserState.nextState(state);
await paymentMethodWarningsService.acknowledge(organizationId);
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
[organizationId]: {
...state[organizationId],
acknowledged: true,
},
});
});
it("clear", async () => {
const state: Record<string, PaymentMethodWarning> = {
"1": {
organizationName: "Teams Organization",
risksSubscriptionFailure: true,
acknowledged: false,
savedAt: getPastDate(3),
},
};
activeUserState.nextState(state);
await paymentMethodWarningsService.clear();
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({});
});
it("removeSubscriptionRisk", async () => {
const organizationId = "1";
const state: Record<string, PaymentMethodWarning> = {
[organizationId]: {
organizationName: "Teams Organization",
risksSubscriptionFailure: true,
acknowledged: false,
savedAt: getPastDate(3),
},
};
activeUserState.nextState(state);
await paymentMethodWarningsService.removeSubscriptionRisk(organizationId);
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
[organizationId]: {
...state[organizationId],
risksSubscriptionFailure: false,
},
});
});
describe("update", () => {
it("Does nothing if the stored payment method warning is less than a week old", async () => {
const organizationId = "1";
const state: Record<string, PaymentMethodWarning> = {
[organizationId]: {
organizationName: "Teams Organization",
risksSubscriptionFailure: true,
acknowledged: false,
savedAt: getPastDate(3),
},
};
activeUserState.nextState(state);
await paymentMethodWarningsService.update(organizationId);
expect(billingApiService.getOrganizationBillingStatus).not.toHaveBeenCalled();
});
it("Retrieves the billing status from the API and uses it to update the state if the state is null", async () => {
const organizationId = "1";
activeUserState.nextState(null);
billingApiService.getOrganizationBillingStatus.mockResolvedValue(
getBillingStatusResponse(organizationId),
);
await paymentMethodWarningsService.update(organizationId);
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
[organizationId]: {
organizationName: "Teams Organization",
risksSubscriptionFailure: true,
acknowledged: false,
savedAt: any(),
},
});
expect(billingApiService.getOrganizationBillingStatus).toHaveBeenCalledTimes(1);
});
it("Retrieves the billing status from the API and uses it to update the state if the stored warning is null", async () => {
const organizationId = "1";
activeUserState.nextState({
[organizationId]: null,
});
billingApiService.getOrganizationBillingStatus.mockResolvedValue(
getBillingStatusResponse(organizationId),
);
await paymentMethodWarningsService.update(organizationId);
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
[organizationId]: {
organizationName: "Teams Organization",
risksSubscriptionFailure: true,
acknowledged: false,
savedAt: any(),
},
});
expect(billingApiService.getOrganizationBillingStatus).toHaveBeenCalledTimes(1);
});
it("Retrieves the billing status from the API and uses it to update the state if the stored warning is older than a week", async () => {
const organizationId = "1";
activeUserState.nextState({
[organizationId]: {
organizationName: "Teams Organization",
risksSubscriptionFailure: false,
acknowledged: false,
savedAt: getPastDate(10),
},
});
billingApiService.getOrganizationBillingStatus.mockResolvedValue(
new OrganizationBillingStatusResponse({
OrganizationId: organizationId,
OrganizationName: "Teams Organization",
RisksSubscriptionFailure: true,
}),
);
await paymentMethodWarningsService.update(organizationId);
expect(await firstValueFrom(paymentMethodWarningsService.paymentMethodWarnings$)).toEqual({
[organizationId]: {
organizationName: "Teams Organization",
risksSubscriptionFailure: true,
acknowledged: false,
savedAt: any(),
},
});
expect(billingApiService.getOrganizationBillingStatus).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -1,74 +0,0 @@
import { firstValueFrom, map, Observable } from "rxjs";
import { ActiveUserState, StateProvider } from "../../platform/state";
import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction";
import { PaymentMethodWarningsServiceAbstraction } from "../abstractions/payment-method-warnings-service.abstraction";
import { PAYMENT_METHOD_WARNINGS_KEY } from "../models/billing-keys.state";
import { PaymentMethodWarning } from "../models/domain/payment-method-warning";
export class PaymentMethodWarningsService implements PaymentMethodWarningsServiceAbstraction {
private paymentMethodWarningsState: ActiveUserState<Record<string, PaymentMethodWarning>>;
paymentMethodWarnings$: Observable<Record<string, PaymentMethodWarning>>;
constructor(
private billingApiService: BillingApiService,
private stateProvider: StateProvider,
) {
this.paymentMethodWarningsState = this.stateProvider.getActive(PAYMENT_METHOD_WARNINGS_KEY);
this.paymentMethodWarnings$ = this.paymentMethodWarningsState.state$;
}
async acknowledge(organizationId: string): Promise<void> {
await this.paymentMethodWarningsState.update((state) => {
const current = state[organizationId];
state[organizationId] = {
...current,
acknowledged: true,
};
return state;
});
}
async removeSubscriptionRisk(organizationId: string): Promise<void> {
await this.paymentMethodWarningsState.update((state) => {
const current = state[organizationId];
state[organizationId] = {
...current,
risksSubscriptionFailure: false,
};
return state;
});
}
async clear(): Promise<void> {
await this.paymentMethodWarningsState.update(() => ({}));
}
async update(organizationId: string): Promise<void> {
const warning = await firstValueFrom(
this.paymentMethodWarningsState.state$.pipe(
map((state) => (!state ? null : state[organizationId])),
),
);
if (!warning || warning.savedAt < this.getOneWeekAgo()) {
const { organizationName, risksSubscriptionFailure } =
await this.billingApiService.getOrganizationBillingStatus(organizationId);
await this.paymentMethodWarningsState.update((state) => {
state ??= {};
state[organizationId] = {
organizationName,
risksSubscriptionFailure,
acknowledged: false,
savedAt: new Date(),
};
return state;
});
}
}
private getOneWeekAgo = (): Date => {
const date = new Date();
date.setDate(date.getDate() - 7);
return date;
};
}

View File

@@ -7,7 +7,6 @@ export enum FeatureFlag {
BrowserFilelessImport = "browser-fileless-import",
ItemShare = "item-share",
GeneratorToolsModernization = "generator-tools-modernization",
ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners",
EnableConsolidatedBilling = "enable-consolidated-billing",
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
EnableDeleteProvider = "AC-1218-delete-provider",
@@ -50,7 +49,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.BrowserFilelessImport]: FALSE,
[FeatureFlag.ItemShare]: FALSE,
[FeatureFlag.GeneratorToolsModernization]: FALSE,
[FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE,
[FeatureFlag.EnableConsolidatedBilling]: FALSE,
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
[FeatureFlag.EnableDeleteProvider]: FALSE,

View File

@@ -1,5 +1,7 @@
import { Meta, Story, Source } from "@storybook/addon-docs";
import * as stories from "./dialog.service.stories";
<Meta title="Component Library/Dialogs" />
# Dialog
@@ -24,7 +26,7 @@ dialog should become scrollable.
A backdrop should be used to hide the content below the dialog. Use `#000000` with `30% opacity`.
<Story id="component-library-dialogs-service--default" />
<Story of={stories.Default} />
## Accessibility

View File

@@ -1,5 +1,14 @@
import { Meta, Story, Source } from "@storybook/addon-docs";
import * as formStories from "./form.stories";
import * as fieldStories from "../form-field/form-field.stories";
import * as passwordToggleStories from "../form-field/password-input-toggle.stories";
import * as searchStories from "../search/search.stories";
import * as selectStories from "../select/select.stories";
import * as multiSelectStories from "../form-field/multi-select.stories";
import * as radioStories from "../radio-button/radio-button.stories";
import * as checkboxStories from "../checkbox/checkbox.stories";
<Meta title="Component Library/Form" />
# Forms
@@ -8,9 +17,9 @@ Component Library forms should always be built using [Angular Reactive Forms][re
[ADR-0001][adr-0001] for a background to this decision. In practice this means that forms should
always use the native `form` element and bind a `formGroup`.
<Story id="component-library-form--full-example" />
<Story of={formStories.FullExample} />
<Source id="component-library-form--full-example" />
<br />
## Form spacing and sections
@@ -48,25 +57,25 @@ controls like email verification, number selection, and more.
#### Default with required attribute
<Story id="component-library-form-field--default" />
<Story of={fieldStories.Default} />
#### Password Toggle
<Story id="component-library-form-password-toggle--default" />
<Story of={passwordToggleStories.Default} />
#### Search
### Search
<Story id="component-library-form-search--default" />
<Story of={searchStories.Default} />
### Selects
#### Searchable single select (default)
<Story id="component-library-form-select--default" />
<Story of={selectStories.Default} />
#### Multi-select
<Story id="component-library-form-multi-select--members" />
<Story of={multiSelectStories.Members} />
### Radio group
@@ -89,14 +98,11 @@ using a radio group for more than 5 options even if the options require addition
#### Block
<Story id="component-library-form-radio-button--block" />
<Story of={radioStories.Block} />
#### Inline
<Story id="component-library-form-radio-button--inline" />
[reactive]: https://angular.io/guide/reactive-forms
[adr-0001]: https://contributing.bitwarden.com/architecture/adr/reactive-forms
<Story of={radioStories.Inline} />
### Checkbox
@@ -116,7 +122,7 @@ If a checkbox group has more than 4 options a
#### Single checkbox
<Story id="component-library-form-checkbox--default" />
<Story of={checkboxStories.Default} />
## Accessibility
@@ -176,3 +182,6 @@ the fields label.
Maintain a ratio of 3:1 with the form's background.
- Error styling should not rely only on using the `danger-600`color change. Use
<i class="bwi bwi-error"></i> as a prefix to highlight the text as error text versus helper
[reactive]: https://angular.io/guide/reactive-forms
[adr-0001]: https://contributing.bitwarden.com/architecture/adr/reactive-forms

View File

@@ -64,6 +64,7 @@ export default {
skipToContent: "Skip to content",
submenu: "submenu",
toggleCollapse: "toggle collapse",
toggleSideNavigation: "toggle side navigation",
});
},
},

View File

@@ -74,6 +74,21 @@
bitPasswordInputToggle
[(toggled)]="showFilePassword"
></button>
<button
type="button"
bitIconButton="bwi-generate"
appStopClick
bitSuffix
(click)="generatePassword()"
></button>
<button
type="button"
bitIconButton="bwi-clone"
[disabled]="!filePassword"
appStopClick
bitSuffix
(click)="copyPasswordToClipboard()"
></button>
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
</bit-form-field>
<tools-password-strength [password]="filePassword" [showText]="true">

View File

@@ -24,6 +24,7 @@ import { EventType } from "@bitwarden/common/enums";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import {
@@ -38,6 +39,7 @@ import {
SelectModule,
ToastService,
} from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { EncryptedExportType } from "../enums/encrypted-export-type.enum";
@@ -157,6 +159,8 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
protected toastService: ToastService,
protected exportService: VaultExportServiceAbstraction,
protected eventCollectionService: EventCollectionService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private policyService: PolicyService,
private logService: LogService,
private formBuilder: UntypedFormBuilder,
@@ -272,6 +276,22 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
}
}
generatePassword = async () => {
const [options] = await this.passwordGenerationService.getOptions();
this.filePasswordValue = await this.passwordGenerationService.generatePassword(options);
this.exportForm.get("filePassword").setValue(this.filePasswordValue);
this.exportForm.get("confirmFilePassword").setValue(this.filePasswordValue);
};
copyPasswordToClipboard = async () => {
this.platformUtilsService.copyToClipboard(this.filePasswordValue);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
});
};
submit = async () => {
if (this.isFileEncryptedExport && this.filePassword != this.confirmFilePassword) {
this.toastService.showToast({

View File

@@ -1,4 +1,8 @@
<ng-container *ngIf="!!cipher">
<bit-callout *ngIf="cardIsExpired" type="info" [title]="'cardExpiredTitle' | i18n">
{{ "cardExpiredMessage" | i18n }}
</bit-callout>
<!-- ITEM DETAILS -->
<app-item-details-v2
[cipher]="cipher"

View File

@@ -8,10 +8,11 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { CollectionId } from "@bitwarden/common/types/guid";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { SearchModule } from "@bitwarden/components";
import { SearchModule, CalloutModule } from "@bitwarden/components";
import { AdditionalOptionsComponent } from "./additional-options/additional-options.component";
import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.component";
@@ -28,6 +29,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
templateUrl: "cipher-view.component.html",
standalone: true,
imports: [
CalloutModule,
CommonModule,
SearchModule,
JslibModule,
@@ -48,6 +50,7 @@ export class CipherViewComponent implements OnInit, OnDestroy {
folder$: Observable<FolderView>;
collections$: Observable<CollectionView[]>;
private destroyed$: Subject<void> = new Subject();
cardIsExpired: boolean = false;
constructor(
private organizationService: OrganizationService,
@@ -57,6 +60,8 @@ export class CipherViewComponent implements OnInit, OnDestroy {
async ngOnInit() {
await this.loadCipherData();
this.cardIsExpired = this.isCardExpiryInThePast();
}
ngOnDestroy(): void {
@@ -97,4 +102,24 @@ export class CipherViewComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroyed$));
}
}
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;
}
}
return false;
}
}