mirror of
https://github.com/bitwarden/browser
synced 2026-02-19 02:44:01 +00:00
Merge branch 'main' of github.com:bitwarden/clients into PM-25525-DEBT-Fix-SystemServiceProvider-dependency-injection
pull latest from main to troubleshoot github action error
This commit is contained in:
@@ -77,14 +77,10 @@ import {
|
||||
} from "../auth/models/response/two-factor-web-authn.response";
|
||||
import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response";
|
||||
import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request";
|
||||
import { PaymentRequest } from "../billing/models/request/payment.request";
|
||||
import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request";
|
||||
import { BillingHistoryResponse } from "../billing/models/response/billing-history.response";
|
||||
import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response";
|
||||
import { PaymentResponse } from "../billing/models/response/payment.response";
|
||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
@@ -171,10 +167,8 @@ export abstract class ApiService {
|
||||
|
||||
abstract getProfile(): Promise<ProfileResponse>;
|
||||
abstract getUserSubscription(): Promise<SubscriptionResponse>;
|
||||
abstract getTaxInfo(): Promise<TaxInfoResponse>;
|
||||
abstract putProfile(request: UpdateProfileRequest): Promise<ProfileResponse>;
|
||||
abstract putAvatar(request: UpdateAvatarRequest): Promise<ProfileResponse>;
|
||||
abstract putTaxInfo(request: TaxInfoUpdateRequest): Promise<any>;
|
||||
abstract postPrelogin(request: PreloginRequest): Promise<PreloginResponse>;
|
||||
abstract postEmailToken(request: EmailTokenRequest): Promise<any>;
|
||||
abstract postEmail(request: EmailRequest): Promise<any>;
|
||||
@@ -185,7 +179,6 @@ export abstract class ApiService {
|
||||
abstract postPremium(data: FormData): Promise<PaymentResponse>;
|
||||
abstract postReinstatePremium(): Promise<any>;
|
||||
abstract postAccountStorage(request: StorageRequest): Promise<PaymentResponse>;
|
||||
abstract postAccountPayment(request: PaymentRequest): Promise<void>;
|
||||
abstract postAccountLicense(data: FormData): Promise<any>;
|
||||
abstract postAccountKeys(request: KeysRequest): Promise<any>;
|
||||
abstract postAccountVerifyEmail(): Promise<any>;
|
||||
@@ -209,7 +202,6 @@ export abstract class ApiService {
|
||||
abstract getLastAuthRequest(): Promise<AuthRequestResponse>;
|
||||
|
||||
abstract getUserBillingHistory(): Promise<BillingHistoryResponse>;
|
||||
abstract getUserBillingPayment(): Promise<BillingPaymentResponse>;
|
||||
|
||||
abstract getCipher(id: string): Promise<CipherResponse>;
|
||||
abstract getFullCipherDetails(id: string): Promise<CipherResponse>;
|
||||
@@ -469,6 +461,13 @@ export abstract class ApiService {
|
||||
end: string,
|
||||
token: string,
|
||||
): Promise<ListResponse<EventResponse>>;
|
||||
abstract getEventsServiceAccount(
|
||||
orgId: string,
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
token: string,
|
||||
): Promise<ListResponse<EventResponse>>;
|
||||
abstract getEventsProject(
|
||||
orgId: string,
|
||||
id: string,
|
||||
|
||||
@@ -3,21 +3,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request";
|
||||
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||
import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response";
|
||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
||||
import { ImportDirectoryRequest } from "../../../models/request/import-directory.request";
|
||||
import { SeatRequest } from "../../../models/request/seat.request";
|
||||
import { StorageRequest } from "../../../models/request/storage.request";
|
||||
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { OrganizationApiKeyType } from "../../enums";
|
||||
import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request";
|
||||
@@ -45,7 +41,6 @@ export abstract class OrganizationApiServiceAbstraction {
|
||||
): Promise<OrganizationResponse>;
|
||||
abstract createLicense(data: FormData): Promise<OrganizationResponse>;
|
||||
abstract save(id: string, request: OrganizationUpdateRequest): Promise<OrganizationResponse>;
|
||||
abstract updatePayment(id: string, request: PaymentRequest): Promise<void>;
|
||||
abstract upgrade(id: string, request: OrganizationUpgradeRequest): Promise<PaymentResponse>;
|
||||
abstract updatePasswordManagerSeats(
|
||||
id: string,
|
||||
@@ -57,7 +52,6 @@ export abstract class OrganizationApiServiceAbstraction {
|
||||
): Promise<ProfileOrganizationResponse>;
|
||||
abstract updateSeats(id: string, request: SeatRequest): Promise<PaymentResponse>;
|
||||
abstract updateStorage(id: string, request: StorageRequest): Promise<PaymentResponse>;
|
||||
abstract verifyBank(id: string, request: VerifyBankRequest): Promise<void>;
|
||||
abstract reinstate(id: string): Promise<void>;
|
||||
abstract leave(id: string): Promise<void>;
|
||||
abstract delete(id: string, request: SecretVerificationRequest): Promise<void>;
|
||||
@@ -76,8 +70,6 @@ export abstract class OrganizationApiServiceAbstraction {
|
||||
organizationApiKeyType?: OrganizationApiKeyType,
|
||||
): Promise<ListResponse<OrganizationApiKeyInformationResponse>>;
|
||||
abstract rotateApiKey(id: string, request: OrganizationApiKeyRequest): Promise<ApiKeyResponse>;
|
||||
abstract getTaxInfo(id: string): Promise<TaxInfoResponse>;
|
||||
abstract updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise<void>;
|
||||
abstract getKeys(id: string): Promise<OrganizationKeysResponse>;
|
||||
abstract updateKeys(
|
||||
id: string,
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../../../../billing/models/request/expanded-tax-info-update.request";
|
||||
import { TokenizedPaymentSourceRequest } from "../../../../billing/models/request/tokenized-payment-source.request";
|
||||
interface TokenizedPaymentMethod {
|
||||
type: "bankAccount" | "card" | "payPal";
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface BillingAddress {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
line1: string | null;
|
||||
line2: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
taxId: { code: string; value: string } | null;
|
||||
}
|
||||
|
||||
export class ProviderSetupRequest {
|
||||
name: string;
|
||||
@@ -9,6 +21,6 @@ export class ProviderSetupRequest {
|
||||
billingEmail: string;
|
||||
token: string;
|
||||
key: string;
|
||||
taxInfo: ExpandedTaxInfoUpdateRequest;
|
||||
paymentSource?: TokenizedPaymentSourceRequest;
|
||||
paymentMethod: TokenizedPaymentMethod;
|
||||
billingAddress: BillingAddress;
|
||||
}
|
||||
|
||||
@@ -7,21 +7,17 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "../../../auth/models/response/api-key.response";
|
||||
import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../../../billing/models/request/organization-no-payment-method-create-request";
|
||||
import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request";
|
||||
import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request";
|
||||
import { PaymentRequest } from "../../../billing/models/request/payment.request";
|
||||
import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request";
|
||||
import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response";
|
||||
import { BillingResponse } from "../../../billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response";
|
||||
import { PaymentResponse } from "../../../billing/models/response/payment.response";
|
||||
import { TaxInfoResponse } from "../../../billing/models/response/tax-info.response";
|
||||
import { ImportDirectoryRequest } from "../../../models/request/import-directory.request";
|
||||
import { SeatRequest } from "../../../models/request/seat.request";
|
||||
import { StorageRequest } from "../../../models/request/storage.request";
|
||||
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||
import { ListResponse } from "../../../models/response/list.response";
|
||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
||||
@@ -143,10 +139,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
return data;
|
||||
}
|
||||
|
||||
async updatePayment(id: string, request: PaymentRequest): Promise<void> {
|
||||
return this.apiService.send("POST", "/organizations/" + id + "/payment", request, true, false);
|
||||
}
|
||||
|
||||
async upgrade(id: string, request: OrganizationUpgradeRequest): Promise<PaymentResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
@@ -208,16 +200,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
return new PaymentResponse(r);
|
||||
}
|
||||
|
||||
async verifyBank(id: string, request: VerifyBankRequest): Promise<void> {
|
||||
await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + id + "/verify-bank",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async reinstate(id: string): Promise<void> {
|
||||
return this.apiService.send("POST", "/organizations/" + id + "/reinstate", null, true, false);
|
||||
}
|
||||
@@ -299,16 +281,6 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
||||
return new ApiKeyResponse(r);
|
||||
}
|
||||
|
||||
async getTaxInfo(id: string): Promise<TaxInfoResponse> {
|
||||
const r = await this.apiService.send("GET", "/organizations/" + id + "/tax", null, true, true);
|
||||
return new TaxInfoResponse(r);
|
||||
}
|
||||
|
||||
async updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise<void> {
|
||||
// Can't broadcast anything because the response doesn't have content
|
||||
return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false);
|
||||
}
|
||||
|
||||
async getKeys(id: string): Promise<OrganizationKeysResponse> {
|
||||
const r = await this.apiService.send("GET", "/organizations/" + id + "/keys", null, true, true);
|
||||
return new OrganizationKeysResponse(r);
|
||||
|
||||
1
libs/common/src/auth/send-access/abstractions/index.ts
Normal file
1
libs/common/src/auth/send-access/abstractions/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./send-token.service";
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SendAccessToken } from "../models/send-access-token";
|
||||
import { GetSendAccessTokenError } from "../types/get-send-access-token-error.type";
|
||||
import { SendAccessDomainCredentials } from "../types/send-access-domain-credentials.type";
|
||||
import { SendHashedPasswordB64 } from "../types/send-hashed-password-b64.type";
|
||||
import { TryGetSendAccessTokenError } from "../types/try-get-send-access-token-error.type";
|
||||
|
||||
/**
|
||||
* Service to manage send access tokens.
|
||||
*/
|
||||
export abstract class SendTokenService {
|
||||
/**
|
||||
* Attempts to retrieve a {@link SendAccessToken} for the given sendId.
|
||||
* If the access token is found in session storage and is not expired, then it returns the token.
|
||||
* If the access token is expired, then it returns a {@link TryGetSendAccessTokenError} expired error.
|
||||
* If an access token is not found in storage, then it attempts to retrieve it from the server (will succeed for sends that don't require any credentials to view).
|
||||
* If the access token is successfully retrieved from the server, then it stores the token in session storage and returns it.
|
||||
* If an access token cannot be granted b/c the send requires credentials, then it returns a {@link TryGetSendAccessTokenError} indicating which credentials are required.
|
||||
* Any submissions of credentials will be handled by the getSendAccessToken$ method.
|
||||
* @param sendId The ID of the send to retrieve the access token for.
|
||||
* @returns An observable that emits a SendAccessToken if successful, or a TryGetSendAccessTokenError if not.
|
||||
*/
|
||||
abstract tryGetSendAccessToken$: (
|
||||
sendId: string,
|
||||
) => Observable<SendAccessToken | TryGetSendAccessTokenError>;
|
||||
|
||||
/**
|
||||
* Retrieves a SendAccessToken for the given sendId using the provided credentials.
|
||||
* If the access token is successfully retrieved from the server, it stores the token in session storage and returns it.
|
||||
* If the access token cannot be granted due to invalid credentials, it returns a {@link GetSendAccessTokenError}.
|
||||
* @param sendId The ID of the send to retrieve the access token for.
|
||||
* @param sendAccessCredentials The credentials to use for accessing the send.
|
||||
* @returns An observable that emits a SendAccessToken if successful, or a GetSendAccessTokenError if not.
|
||||
*/
|
||||
abstract getSendAccessToken$: (
|
||||
sendId: string,
|
||||
sendAccessCredentials: SendAccessDomainCredentials,
|
||||
) => Observable<SendAccessToken | GetSendAccessTokenError>;
|
||||
|
||||
/**
|
||||
* Hashes a password for send access which is required to create a {@link SendAccessTokenRequest}
|
||||
* (more specifically, to create a {@link SendAccessDomainCredentials} for sends that require a password)
|
||||
* @param password The raw password string to hash.
|
||||
* @param keyMaterialUrlB64 The base64 URL encoded key material string.
|
||||
* @returns A promise that resolves to the hashed password as a SendHashedPasswordB64.
|
||||
*/
|
||||
abstract hashSendPassword: (
|
||||
password: string,
|
||||
keyMaterialUrlB64: string,
|
||||
) => Promise<SendHashedPasswordB64>;
|
||||
|
||||
/**
|
||||
* Clears a send access token from storage.
|
||||
*/
|
||||
abstract invalidateSendAccessToken: (sendId: string) => Promise<void>;
|
||||
}
|
||||
4
libs/common/src/auth/send-access/index.ts
Normal file
4
libs/common/src/auth/send-access/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./abstractions";
|
||||
export * from "./models";
|
||||
export * from "./services";
|
||||
export * from "./types";
|
||||
1
libs/common/src/auth/send-access/models/index.ts
Normal file
1
libs/common/src/auth/send-access/models/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./send-access-token";
|
||||
@@ -0,0 +1,75 @@
|
||||
import { SendAccessTokenResponse } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SendAccessToken } from "./send-access-token";
|
||||
|
||||
describe("SendAccessToken", () => {
|
||||
const sendId = "sendId";
|
||||
|
||||
const NOW = 1_000_000; // fixed timestamp for predictable results
|
||||
|
||||
const expiresAt: number = NOW + 1000 * 60 * 5; // 5 minutes from now
|
||||
|
||||
const expiredExpiresAt: number = NOW - 1000 * 60 * 5; // 5 minutes ago
|
||||
|
||||
let nowSpy: jest.SpyInstance<number, []>;
|
||||
|
||||
beforeAll(() => {
|
||||
nowSpy = jest.spyOn(Date, "now");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Ensure every test starts from the same fixed time
|
||||
nowSpy.mockReturnValue(NOW);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should create a valid, unexpired token", () => {
|
||||
const token = new SendAccessToken(sendId, expiresAt);
|
||||
expect(token).toBeTruthy();
|
||||
expect(token.isExpired()).toBe(false);
|
||||
});
|
||||
|
||||
it("should be expired after the expiration time", () => {
|
||||
const token = new SendAccessToken(sendId, expiredExpiresAt);
|
||||
expect(token.isExpired()).toBe(true);
|
||||
});
|
||||
|
||||
it("should be considered expired if within 5 seconds of expiration", () => {
|
||||
const token = new SendAccessToken(sendId, expiresAt);
|
||||
nowSpy.mockReturnValue(expiresAt - 4_000); // 4 seconds before expiry
|
||||
expect(token.isExpired()).toBe(true);
|
||||
});
|
||||
|
||||
it("should return the correct time until expiry in seconds", () => {
|
||||
const token = new SendAccessToken(sendId, expiresAt);
|
||||
expect(token.timeUntilExpirySeconds()).toBe(300); // 5 minutes
|
||||
});
|
||||
|
||||
it("should return 0 if the token is expired", () => {
|
||||
const token = new SendAccessToken(sendId, expiredExpiresAt);
|
||||
expect(token.timeUntilExpirySeconds()).toBe(0);
|
||||
});
|
||||
|
||||
it("should create a token from JSON", () => {
|
||||
const json = {
|
||||
token: sendId,
|
||||
expiresAt: expiresAt,
|
||||
};
|
||||
const token = SendAccessToken.fromJson(json);
|
||||
expect(token).toBeTruthy();
|
||||
expect(token.isExpired()).toBe(false);
|
||||
});
|
||||
|
||||
it("should create a token from SendAccessTokenResponse", () => {
|
||||
const response = {
|
||||
token: sendId,
|
||||
expiresAt: expiresAt,
|
||||
} as SendAccessTokenResponse;
|
||||
const token = SendAccessToken.fromSendAccessTokenResponse(response);
|
||||
expect(token).toBeTruthy();
|
||||
expect(token.isExpired()).toBe(false);
|
||||
});
|
||||
});
|
||||
46
libs/common/src/auth/send-access/models/send-access-token.ts
Normal file
46
libs/common/src/auth/send-access/models/send-access-token.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SendAccessTokenResponse } from "@bitwarden/sdk-internal";
|
||||
|
||||
export class SendAccessToken {
|
||||
constructor(
|
||||
/**
|
||||
* The access token string
|
||||
*/
|
||||
readonly token: string,
|
||||
/**
|
||||
* The time (in milliseconds since the epoch) when the token expires
|
||||
*/
|
||||
readonly expiresAt: number,
|
||||
) {}
|
||||
|
||||
/** Returns whether the send access token is expired or not
|
||||
* Has a 5 second threshold to avoid race conditions with the token
|
||||
* expiring in flight
|
||||
*/
|
||||
isExpired(threshold: number = 5_000): boolean {
|
||||
return Date.now() >= this.expiresAt - threshold;
|
||||
}
|
||||
|
||||
/** Returns how many full seconds remain until expiry. Returns 0 if expired. */
|
||||
timeUntilExpirySeconds(): number {
|
||||
return Math.max(0, Math.floor((this.expiresAt - Date.now()) / 1_000));
|
||||
}
|
||||
|
||||
static fromJson(parsedJson: Jsonify<SendAccessToken>): SendAccessToken {
|
||||
return new SendAccessToken(parsedJson.token, parsedJson.expiresAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SendAccessToken from a SendAccessTokenResponse.
|
||||
* @param sendAccessTokenResponse The SDK response object containing the token and expiry information.
|
||||
* @returns A new instance of SendAccessToken.
|
||||
* note: we need to convert from the SDK response type to our internal type so that we can
|
||||
* be sure it will serialize/deserialize correctly in state provider.
|
||||
*/
|
||||
static fromSendAccessTokenResponse(
|
||||
sendAccessTokenResponse: SendAccessTokenResponse,
|
||||
): SendAccessToken {
|
||||
return new SendAccessToken(sendAccessTokenResponse.token, sendAccessTokenResponse.expiresAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
SendAccessTokenApiErrorResponse,
|
||||
SendAccessTokenError,
|
||||
SendAccessTokenInvalidGrantError,
|
||||
SendAccessTokenInvalidRequestError,
|
||||
SendAccessTokenResponse,
|
||||
UnexpectedIdentityError,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/state-test-utils";
|
||||
|
||||
import {
|
||||
SendHashedPassword,
|
||||
SendPasswordKeyMaterial,
|
||||
SendPasswordService,
|
||||
} from "../../../key-management/sends";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { MockSdkService } from "../../../platform/spec/mock-sdk.service";
|
||||
import { SendAccessToken } from "../models/send-access-token";
|
||||
import { GetSendAccessTokenError } from "../types/get-send-access-token-error.type";
|
||||
import { SendAccessDomainCredentials } from "../types/send-access-domain-credentials.type";
|
||||
import { SendHashedPasswordB64 } from "../types/send-hashed-password-b64.type";
|
||||
import { SendOtp } from "../types/send-otp.type";
|
||||
|
||||
import { DefaultSendTokenService } from "./default-send-token.service";
|
||||
import { SEND_ACCESS_TOKEN_DICT } from "./send-access-token-dict.state";
|
||||
|
||||
describe("SendTokenService", () => {
|
||||
let service: DefaultSendTokenService;
|
||||
|
||||
// Deps
|
||||
let sdkService: MockSdkService;
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let sendPasswordService: MockProxy<SendPasswordService>;
|
||||
|
||||
beforeEach(() => {
|
||||
globalStateProvider = new FakeGlobalStateProvider();
|
||||
sdkService = new MockSdkService();
|
||||
sendPasswordService = mock<SendPasswordService>();
|
||||
|
||||
service = new DefaultSendTokenService(globalStateProvider, sdkService, sendPasswordService);
|
||||
});
|
||||
|
||||
it("instantiates", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("Send access token retrieval tests", () => {
|
||||
let sendAccessTokenDictGlobalState: FakeGlobalState<Record<string, SendAccessToken>>;
|
||||
|
||||
let sendAccessTokenResponse: SendAccessTokenResponse;
|
||||
|
||||
let sendId: string;
|
||||
let sendAccessToken: SendAccessToken;
|
||||
let token: string;
|
||||
let tokenExpiresAt: number;
|
||||
|
||||
const EXPECTED_SERVER_KIND: GetSendAccessTokenError["kind"] = "expected_server";
|
||||
const UNEXPECTED_SERVER_KIND: GetSendAccessTokenError["kind"] = "unexpected_server";
|
||||
|
||||
const INVALID_REQUEST_CODES: SendAccessTokenInvalidRequestError[] = [
|
||||
"send_id_required",
|
||||
"password_hash_b64_required",
|
||||
"email_required",
|
||||
"email_and_otp_required_otp_sent",
|
||||
"unknown",
|
||||
];
|
||||
|
||||
const INVALID_GRANT_CODES: SendAccessTokenInvalidGrantError[] = [
|
||||
"send_id_invalid",
|
||||
"password_hash_b64_invalid",
|
||||
"email_invalid",
|
||||
"otp_invalid",
|
||||
"otp_generation_failed",
|
||||
"unknown",
|
||||
];
|
||||
|
||||
const CREDS = [
|
||||
{ kind: "password", passwordHashB64: "h4sh" as SendHashedPasswordB64 },
|
||||
{ kind: "email", email: "u@example.com" },
|
||||
{ kind: "email_otp", email: "u@example.com", otp: "123456" as SendOtp },
|
||||
] as const satisfies readonly SendAccessDomainCredentials[];
|
||||
|
||||
type SendAccessTokenApiErrorResponseErrorCode = SendAccessTokenApiErrorResponse["error"];
|
||||
|
||||
type SimpleErrorType = Exclude<
|
||||
SendAccessTokenApiErrorResponseErrorCode,
|
||||
"invalid_request" | "invalid_grant"
|
||||
>;
|
||||
|
||||
// Extract out simple error types which don't have complex send_access_error_types to handle.
|
||||
const SIMPLE_ERROR_TYPES = [
|
||||
"invalid_client",
|
||||
"unauthorized_client",
|
||||
"unsupported_grant_type",
|
||||
"invalid_scope",
|
||||
"invalid_target",
|
||||
] as const satisfies readonly SimpleErrorType[];
|
||||
|
||||
beforeEach(() => {
|
||||
sendId = "sendId";
|
||||
token = "sendAccessToken";
|
||||
tokenExpiresAt = Date.now() + 1000 * 60 * 5; // 5 minutes from now
|
||||
|
||||
sendAccessTokenResponse = {
|
||||
token: token,
|
||||
expiresAt: tokenExpiresAt,
|
||||
};
|
||||
|
||||
sendAccessToken = SendAccessToken.fromSendAccessTokenResponse(sendAccessTokenResponse);
|
||||
|
||||
sendAccessTokenDictGlobalState = globalStateProvider.getFake(SEND_ACCESS_TOKEN_DICT);
|
||||
// Ensure the state is empty before each test
|
||||
sendAccessTokenDictGlobalState.stateSubject.next({});
|
||||
});
|
||||
|
||||
describe("tryGetSendAccessToken$", () => {
|
||||
it("returns the send access token from session storage when token exists and isn't expired", async () => {
|
||||
// Arrange
|
||||
// Store the send access token in the global state
|
||||
sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: sendAccessToken });
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(sendAccessToken);
|
||||
});
|
||||
|
||||
it("returns expired error and clears token from storage when token is expired", async () => {
|
||||
// Arrange
|
||||
const oldDate = new Date("2025-01-01");
|
||||
const expiredSendAccessToken = new SendAccessToken(token, oldDate.getTime());
|
||||
sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: expiredSendAccessToken });
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).not.toBeInstanceOf(SendAccessToken);
|
||||
expect(result).toStrictEqual({ kind: "expired" });
|
||||
|
||||
// assert that we removed the expired token from storage.
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
expect(sendAccessTokenDict).not.toHaveProperty(sendId);
|
||||
});
|
||||
|
||||
it("calls to get a new token if none is found in storage and stores the retrieved token in session storage", async () => {
|
||||
// Arrange
|
||||
sdkService.client.auth
|
||||
.mockDeep()
|
||||
.send_access.mockDeep()
|
||||
.request_send_access_token.mockResolvedValue(sendAccessTokenResponse);
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toBeInstanceOf(SendAccessToken);
|
||||
expect(result).toEqual(sendAccessToken);
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken);
|
||||
});
|
||||
|
||||
describe("handles expected invalid_request scenarios appropriately", () => {
|
||||
it.each(INVALID_REQUEST_CODES)(
|
||||
"surfaces %s as an expected invalid_request error",
|
||||
async (code) => {
|
||||
// Arrange
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_request",
|
||||
error_description: code,
|
||||
send_access_error_type: code,
|
||||
};
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: EXPECTED_SERVER_KIND,
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("handles bare expected invalid_request scenario appropriately", async () => {
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_request",
|
||||
};
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: EXPECTED_SERVER_KIND,
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each(SIMPLE_ERROR_TYPES)("handles expected %s error appropriately", async (errorType) => {
|
||||
const api: SendAccessTokenApiErrorResponse = {
|
||||
error: errorType,
|
||||
error_description: `The ${errorType} error occurred`,
|
||||
};
|
||||
mockSdkRejectWith({ kind: "expected", data: api });
|
||||
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api });
|
||||
});
|
||||
|
||||
it.each(SIMPLE_ERROR_TYPES)(
|
||||
"handles expected %s bare error appropriately",
|
||||
async (errorType) => {
|
||||
const api: SendAccessTokenApiErrorResponse = { error: errorType };
|
||||
mockSdkRejectWith({ kind: "expected", data: api });
|
||||
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api });
|
||||
},
|
||||
);
|
||||
|
||||
describe("handles expected invalid_grant scenarios appropriately", () => {
|
||||
it.each(INVALID_GRANT_CODES)(
|
||||
"surfaces %s as an expected invalid_grant error",
|
||||
async (code) => {
|
||||
// Arrange
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_grant",
|
||||
error_description: code,
|
||||
send_access_error_type: code,
|
||||
};
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: EXPECTED_SERVER_KIND,
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("handles bare expected invalid_grant scenario appropriately", async () => {
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_grant",
|
||||
};
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: EXPECTED_SERVER_KIND,
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces unexpected errors as unexpected_server error", async () => {
|
||||
// Arrange
|
||||
const unexpectedIdentityError: UnexpectedIdentityError = "unexpected error occurred";
|
||||
|
||||
mockSdkRejectWith({
|
||||
kind: "unexpected",
|
||||
data: unexpectedIdentityError,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: UNEXPECTED_SERVER_KIND,
|
||||
error: unexpectedIdentityError,
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces an unknown error as an unknown error", async () => {
|
||||
// Arrange
|
||||
const unknownError = "unknown error occurred";
|
||||
|
||||
sdkService.client.auth
|
||||
.mockDeep()
|
||||
.send_access.mockDeep()
|
||||
.request_send_access_token.mockRejectedValue(new Error(unknownError));
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.tryGetSendAccessToken$(sendId));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: "unknown",
|
||||
error: unknownError,
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSendAccessTokenFromStorage", () => {
|
||||
it("returns undefined if no token is found in storage", async () => {
|
||||
const result = await (service as any).getSendAccessTokenFromStorage("nonexistentSendId");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns the token if found in storage", async () => {
|
||||
sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: sendAccessToken });
|
||||
const result = await (service as any).getSendAccessTokenFromStorage(sendId);
|
||||
expect(result).toEqual(sendAccessToken);
|
||||
});
|
||||
|
||||
it("returns undefined if the global state isn't initialized yet", async () => {
|
||||
sendAccessTokenDictGlobalState.stateSubject.next(null);
|
||||
|
||||
const result = await (service as any).getSendAccessTokenFromStorage(sendId);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setSendAccessTokenInStorage", () => {
|
||||
it("stores the token in storage", async () => {
|
||||
await (service as any).setSendAccessTokenInStorage(sendId, sendAccessToken);
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken);
|
||||
});
|
||||
|
||||
it("initializes the dictionary if it isn't already", async () => {
|
||||
sendAccessTokenDictGlobalState.stateSubject.next(null);
|
||||
|
||||
await (service as any).setSendAccessTokenInStorage(sendId, sendAccessToken);
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken);
|
||||
});
|
||||
|
||||
it("merges with existing tokens in storage", async () => {
|
||||
const anotherSendId = "anotherSendId";
|
||||
const anotherSendAccessToken = new SendAccessToken(
|
||||
"anotherToken",
|
||||
Date.now() + 1000 * 60,
|
||||
);
|
||||
|
||||
sendAccessTokenDictGlobalState.stateSubject.next({
|
||||
[anotherSendId]: anotherSendAccessToken,
|
||||
});
|
||||
await (service as any).setSendAccessTokenInStorage(sendId, sendAccessToken);
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken);
|
||||
expect(sendAccessTokenDict).toHaveProperty(anotherSendId, anotherSendAccessToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSendAccessToken$", () => {
|
||||
it("returns a send access token for a password protected send when given valid password credentials", async () => {
|
||||
// Arrange
|
||||
const sendPasswordCredentials: SendAccessDomainCredentials = {
|
||||
kind: "password",
|
||||
passwordHashB64: "testPassword" as SendHashedPasswordB64,
|
||||
};
|
||||
|
||||
sdkService.client.auth
|
||||
.mockDeep()
|
||||
.send_access.mockDeep()
|
||||
.request_send_access_token.mockResolvedValue(sendAccessTokenResponse);
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(
|
||||
service.getSendAccessToken$(sendId, sendPasswordCredentials),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(sendAccessToken);
|
||||
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken);
|
||||
});
|
||||
|
||||
// Note: we deliberately aren't testing the "success" scenario of passing
|
||||
// just SendEmailCredentials as that will never return a send access token on it's own.
|
||||
|
||||
it("returns a send access token for a email + otp protected send when given valid email and otp", async () => {
|
||||
// Arrange
|
||||
const sendEmailOtpCredentials: SendAccessDomainCredentials = {
|
||||
kind: "email_otp",
|
||||
email: "test@example.com",
|
||||
otp: "123456" as SendOtp,
|
||||
};
|
||||
|
||||
sdkService.client.auth
|
||||
.mockDeep()
|
||||
.send_access.mockDeep()
|
||||
.request_send_access_token.mockResolvedValue(sendAccessTokenResponse);
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(
|
||||
service.getSendAccessToken$(sendId, sendEmailOtpCredentials),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(sendAccessToken);
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
expect(sendAccessTokenDict).toHaveProperty(sendId, sendAccessToken);
|
||||
});
|
||||
|
||||
describe.each(CREDS.map((c) => [c.kind, c] as const))(
|
||||
"scenarios with %s credentials",
|
||||
(_label, creds) => {
|
||||
it.each(INVALID_REQUEST_CODES)(
|
||||
"handles expected invalid_request.%s scenario appropriately",
|
||||
async (code) => {
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_request",
|
||||
error_description: code,
|
||||
send_access_error_type: code,
|
||||
};
|
||||
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(service.getSendAccessToken$("abc123", creds));
|
||||
|
||||
expect(result).toEqual({
|
||||
kind: "expected_server",
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("handles expected invalid_request scenario appropriately", async () => {
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_request",
|
||||
};
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: "expected_server",
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(INVALID_GRANT_CODES)(
|
||||
"handles expected invalid_grant.%s scenario appropriately",
|
||||
async (code) => {
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_grant",
|
||||
error_description: code,
|
||||
send_access_error_type: code,
|
||||
};
|
||||
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(service.getSendAccessToken$("abc123", creds));
|
||||
|
||||
expect(result).toEqual({
|
||||
kind: "expected_server",
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("handles expected invalid_grant scenario appropriately", async () => {
|
||||
const sendAccessTokenApiErrorResponse: SendAccessTokenApiErrorResponse = {
|
||||
error: "invalid_grant",
|
||||
};
|
||||
mockSdkRejectWith({
|
||||
kind: "expected",
|
||||
data: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: "expected_server",
|
||||
error: sendAccessTokenApiErrorResponse,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(SIMPLE_ERROR_TYPES)(
|
||||
"handles expected %s error appropriately",
|
||||
async (errorType) => {
|
||||
const api: SendAccessTokenApiErrorResponse = {
|
||||
error: errorType,
|
||||
error_description: `The ${errorType} error occurred`,
|
||||
};
|
||||
mockSdkRejectWith({ kind: "expected", data: api });
|
||||
|
||||
const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds));
|
||||
|
||||
expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api });
|
||||
},
|
||||
);
|
||||
|
||||
it.each(SIMPLE_ERROR_TYPES)(
|
||||
"handles expected %s bare error appropriately",
|
||||
async (errorType) => {
|
||||
const api: SendAccessTokenApiErrorResponse = { error: errorType };
|
||||
mockSdkRejectWith({ kind: "expected", data: api });
|
||||
|
||||
const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds));
|
||||
|
||||
expect(result).toEqual({ kind: EXPECTED_SERVER_KIND, error: api });
|
||||
},
|
||||
);
|
||||
|
||||
it("surfaces unexpected errors as unexpected_server error", async () => {
|
||||
// Arrange
|
||||
const unexpectedIdentityError: UnexpectedIdentityError = "unexpected error occurred";
|
||||
|
||||
mockSdkRejectWith({
|
||||
kind: "unexpected",
|
||||
data: unexpectedIdentityError,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: UNEXPECTED_SERVER_KIND,
|
||||
error: unexpectedIdentityError,
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces an unknown error as an unknown error", async () => {
|
||||
// Arrange
|
||||
const unknownError = "unknown error occurred";
|
||||
|
||||
sdkService.client.auth
|
||||
.mockDeep()
|
||||
.send_access.mockDeep()
|
||||
.request_send_access_token.mockRejectedValue(new Error(unknownError));
|
||||
|
||||
// Act
|
||||
const result = await firstValueFrom(service.getSendAccessToken$(sendId, creds));
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({
|
||||
kind: "unknown",
|
||||
error: unknownError,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("errors if passwordHashB64 is missing for password credentials", async () => {
|
||||
const creds: SendAccessDomainCredentials = {
|
||||
kind: "password",
|
||||
passwordHashB64: "" as SendHashedPasswordB64,
|
||||
};
|
||||
await expect(firstValueFrom(service.getSendAccessToken$(sendId, creds))).rejects.toThrow(
|
||||
"passwordHashB64 must be provided for password credentials.",
|
||||
);
|
||||
});
|
||||
|
||||
it("errors if email is missing for email credentials", async () => {
|
||||
const creds: SendAccessDomainCredentials = {
|
||||
kind: "email",
|
||||
email: "",
|
||||
};
|
||||
await expect(firstValueFrom(service.getSendAccessToken$(sendId, creds))).rejects.toThrow(
|
||||
"email must be provided for email credentials.",
|
||||
);
|
||||
});
|
||||
|
||||
it("errors if email or otp is missing for email_otp credentials", async () => {
|
||||
const creds: SendAccessDomainCredentials = {
|
||||
kind: "email_otp",
|
||||
email: "",
|
||||
otp: "" as SendOtp,
|
||||
};
|
||||
await expect(firstValueFrom(service.getSendAccessToken$(sendId, creds))).rejects.toThrow(
|
||||
"email and otp must be provided for email_otp credentials.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalidateSendAccessToken", () => {
|
||||
it("removes a send access token from storage", async () => {
|
||||
// Arrange
|
||||
sendAccessTokenDictGlobalState.stateSubject.next({ [sendId]: sendAccessToken });
|
||||
|
||||
// Act
|
||||
await service.invalidateSendAccessToken(sendId);
|
||||
const sendAccessTokenDict = await firstValueFrom(sendAccessTokenDictGlobalState.state$);
|
||||
|
||||
// Assert
|
||||
expect(sendAccessTokenDict).not.toHaveProperty(sendId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashSendPassword", () => {
|
||||
test.each(["", null, undefined])("rejects if password is %p", async (pwd) => {
|
||||
await expect(service.hashSendPassword(pwd as any, "keyMaterialUrlB64")).rejects.toThrow(
|
||||
"Password must be provided.",
|
||||
);
|
||||
});
|
||||
|
||||
test.each(["", null, undefined])(
|
||||
"rejects if keyMaterialUrlB64 is %p",
|
||||
async (keyMaterialUrlB64) => {
|
||||
await expect(
|
||||
service.hashSendPassword("password", keyMaterialUrlB64 as any),
|
||||
).rejects.toThrow("KeyMaterialUrlB64 must be provided.");
|
||||
},
|
||||
);
|
||||
|
||||
it("correctly hashes the password", async () => {
|
||||
// Arrange
|
||||
const password = "testPassword";
|
||||
const keyMaterialUrlB64 = "testKeyMaterialUrlB64";
|
||||
const keyMaterialArray = new Uint8Array([1, 2, 3]) as SendPasswordKeyMaterial;
|
||||
const hashedPasswordArray = new Uint8Array([4, 5, 6]) as SendHashedPassword;
|
||||
const sendHashedPasswordB64 = "hashedPasswordB64" as SendHashedPasswordB64;
|
||||
|
||||
const utilsFromUrlB64ToArraySpy = jest
|
||||
.spyOn(Utils, "fromUrlB64ToArray")
|
||||
.mockReturnValue(keyMaterialArray);
|
||||
|
||||
sendPasswordService.hashPassword.mockResolvedValue(hashedPasswordArray);
|
||||
|
||||
const utilsFromBufferToB64Spy = jest
|
||||
.spyOn(Utils, "fromBufferToB64")
|
||||
.mockReturnValue(sendHashedPasswordB64);
|
||||
|
||||
// Act
|
||||
const result = await service.hashSendPassword(password, keyMaterialUrlB64);
|
||||
|
||||
// Assert
|
||||
expect(sendPasswordService.hashPassword).toHaveBeenCalledWith(password, keyMaterialArray);
|
||||
expect(utilsFromUrlB64ToArraySpy).toHaveBeenCalledWith(keyMaterialUrlB64);
|
||||
expect(utilsFromBufferToB64Spy).toHaveBeenCalledWith(hashedPasswordArray);
|
||||
expect(result).toBe(sendHashedPasswordB64);
|
||||
});
|
||||
});
|
||||
|
||||
function mockSdkRejectWith(sendAccessTokenError: SendAccessTokenError) {
|
||||
sdkService.client.auth
|
||||
.mockDeep()
|
||||
.send_access.mockDeep()
|
||||
.request_send_access_token.mockRejectedValue(sendAccessTokenError);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,316 @@
|
||||
import { Observable, defer, firstValueFrom, from } from "rxjs";
|
||||
|
||||
import {
|
||||
BitwardenClient,
|
||||
SendAccessCredentials,
|
||||
SendAccessTokenError,
|
||||
SendAccessTokenRequest,
|
||||
SendAccessTokenResponse,
|
||||
} from "@bitwarden/sdk-internal";
|
||||
import { GlobalState, GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { SendPasswordService } from "../../../key-management/sends/abstractions/send-password.service";
|
||||
import {
|
||||
SendHashedPassword,
|
||||
SendPasswordKeyMaterial,
|
||||
} from "../../../key-management/sends/types/send-hashed-password.type";
|
||||
import { SdkService } from "../../../platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { SendTokenService as SendTokenServiceAbstraction } from "../abstractions/send-token.service";
|
||||
import { SendAccessToken } from "../models/send-access-token";
|
||||
import { GetSendAccessTokenError } from "../types/get-send-access-token-error.type";
|
||||
import { SendAccessDomainCredentials } from "../types/send-access-domain-credentials.type";
|
||||
import { SendHashedPasswordB64 } from "../types/send-hashed-password-b64.type";
|
||||
import { TryGetSendAccessTokenError } from "../types/try-get-send-access-token-error.type";
|
||||
|
||||
import { SEND_ACCESS_TOKEN_DICT } from "./send-access-token-dict.state";
|
||||
|
||||
export class DefaultSendTokenService implements SendTokenServiceAbstraction {
|
||||
private sendAccessTokenDictGlobalState: GlobalState<Record<string, SendAccessToken>> | undefined;
|
||||
|
||||
constructor(
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private sdkService: SdkService,
|
||||
private sendPasswordService: SendPasswordService,
|
||||
) {
|
||||
this.initializeState();
|
||||
}
|
||||
|
||||
private initializeState(): void {
|
||||
this.sendAccessTokenDictGlobalState = this.globalStateProvider.get(SEND_ACCESS_TOKEN_DICT);
|
||||
}
|
||||
|
||||
tryGetSendAccessToken$(sendId: string): Observable<SendAccessToken | TryGetSendAccessTokenError> {
|
||||
// Defer the execution to ensure that a cold observable is returned.
|
||||
return defer(() => from(this._tryGetSendAccessToken(sendId)));
|
||||
}
|
||||
|
||||
private async _tryGetSendAccessToken(
|
||||
sendId: string,
|
||||
): Promise<SendAccessToken | TryGetSendAccessTokenError> {
|
||||
// Validate the sendId is a non-empty string.
|
||||
this.validateSendId(sendId);
|
||||
|
||||
// Check in storage for the access token for the given sendId.
|
||||
const sendAccessTokenFromStorage = await this.getSendAccessTokenFromStorage(sendId);
|
||||
|
||||
if (sendAccessTokenFromStorage != null) {
|
||||
// If it is expired, we clear the token from storage and return the expired error
|
||||
if (sendAccessTokenFromStorage.isExpired()) {
|
||||
await this.clearSendAccessTokenFromStorage(sendId);
|
||||
return { kind: "expired" };
|
||||
} else {
|
||||
// If it is not expired, we return it
|
||||
return sendAccessTokenFromStorage;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a token in storage, we can try to request a new token from the server.
|
||||
const request: SendAccessTokenRequest = {
|
||||
sendId: sendId,
|
||||
};
|
||||
|
||||
const anonSdkClient: BitwardenClient = await firstValueFrom(this.sdkService.client$);
|
||||
|
||||
try {
|
||||
const result: SendAccessTokenResponse = await anonSdkClient
|
||||
.auth()
|
||||
.send_access()
|
||||
.request_send_access_token(request);
|
||||
|
||||
// Convert from SDK shape to SendAccessToken so it can be serialized into & out of state provider
|
||||
const sendAccessToken = SendAccessToken.fromSendAccessTokenResponse(result);
|
||||
|
||||
// If we get a token back, we need to store it in the global state.
|
||||
await this.setSendAccessTokenInStorage(sendId, sendAccessToken);
|
||||
|
||||
return sendAccessToken;
|
||||
} catch (error: unknown) {
|
||||
return this.normalizeSendAccessTokenError(error);
|
||||
}
|
||||
}
|
||||
|
||||
getSendAccessToken$(
|
||||
sendId: string,
|
||||
sendCredentials: SendAccessDomainCredentials,
|
||||
): Observable<SendAccessToken | GetSendAccessTokenError> {
|
||||
// Defer the execution to ensure that a cold observable is returned.
|
||||
return defer(() => from(this._getSendAccessToken(sendId, sendCredentials)));
|
||||
}
|
||||
|
||||
private async _getSendAccessToken(
|
||||
sendId: string,
|
||||
sendAccessCredentials: SendAccessDomainCredentials,
|
||||
): Promise<SendAccessToken | GetSendAccessTokenError> {
|
||||
// Validate inputs to account for non-strict TS call sites.
|
||||
this.validateCredentialsRequest(sendId, sendAccessCredentials);
|
||||
|
||||
// Convert inputs to SDK request shape
|
||||
const request: SendAccessTokenRequest = {
|
||||
sendId: sendId,
|
||||
sendAccessCredentials: this.convertDomainCredentialsToSdkCredentials(sendAccessCredentials),
|
||||
};
|
||||
|
||||
const anonSdkClient: BitwardenClient = await firstValueFrom(this.sdkService.client$);
|
||||
|
||||
try {
|
||||
const result: SendAccessTokenResponse = await anonSdkClient
|
||||
.auth()
|
||||
.send_access()
|
||||
.request_send_access_token(request);
|
||||
|
||||
// Convert from SDK interface to SendAccessToken class so it can be serialized into & out of state provider
|
||||
const sendAccessToken = SendAccessToken.fromSendAccessTokenResponse(result);
|
||||
|
||||
// Any time we get a token from the server, we need to store it in the global state.
|
||||
await this.setSendAccessTokenInStorage(sendId, sendAccessToken);
|
||||
|
||||
return sendAccessToken;
|
||||
} catch (error: unknown) {
|
||||
return this.normalizeSendAccessTokenError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async invalidateSendAccessToken(sendId: string): Promise<void> {
|
||||
await this.clearSendAccessTokenFromStorage(sendId);
|
||||
}
|
||||
|
||||
async hashSendPassword(
|
||||
password: string,
|
||||
keyMaterialUrlB64: string,
|
||||
): Promise<SendHashedPasswordB64> {
|
||||
// Validate the password and key material
|
||||
if (password == null || password.trim() === "") {
|
||||
throw new Error("Password must be provided.");
|
||||
}
|
||||
if (keyMaterialUrlB64 == null || keyMaterialUrlB64.trim() === "") {
|
||||
throw new Error("KeyMaterialUrlB64 must be provided.");
|
||||
}
|
||||
|
||||
// Convert the base64 URL encoded key material to a Uint8Array
|
||||
const keyMaterialUrlB64Array = Utils.fromUrlB64ToArray(
|
||||
keyMaterialUrlB64,
|
||||
) as SendPasswordKeyMaterial;
|
||||
|
||||
const sendHashedPasswordArray: SendHashedPassword = await this.sendPasswordService.hashPassword(
|
||||
password,
|
||||
keyMaterialUrlB64Array,
|
||||
);
|
||||
|
||||
// Convert the Uint8Array to a base64 encoded string which is required
|
||||
// for the server to be able to compare the password hash.
|
||||
const sendHashedPasswordB64 = Utils.fromBufferToB64(
|
||||
sendHashedPasswordArray,
|
||||
) as SendHashedPasswordB64;
|
||||
|
||||
return sendHashedPasswordB64;
|
||||
}
|
||||
|
||||
private async getSendAccessTokenFromStorage(
|
||||
sendId: string,
|
||||
): Promise<SendAccessToken | undefined> {
|
||||
if (this.sendAccessTokenDictGlobalState != null) {
|
||||
const sendAccessTokenDict = await firstValueFrom(this.sendAccessTokenDictGlobalState.state$);
|
||||
return sendAccessTokenDict?.[sendId];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async setSendAccessTokenInStorage(
|
||||
sendId: string,
|
||||
sendAccessToken: SendAccessToken,
|
||||
): Promise<void> {
|
||||
if (this.sendAccessTokenDictGlobalState != null) {
|
||||
await this.sendAccessTokenDictGlobalState.update(
|
||||
(sendAccessTokenDict) => {
|
||||
sendAccessTokenDict ??= {}; // Initialize if undefined
|
||||
|
||||
sendAccessTokenDict[sendId] = sendAccessToken;
|
||||
return sendAccessTokenDict;
|
||||
},
|
||||
{
|
||||
// only update if the value is different (to avoid unnecessary writes)
|
||||
shouldUpdate: (prevDict) => {
|
||||
const prevSendAccessToken = prevDict?.[sendId];
|
||||
return (
|
||||
prevSendAccessToken?.token !== sendAccessToken.token ||
|
||||
prevSendAccessToken?.expiresAt !== sendAccessToken.expiresAt
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async clearSendAccessTokenFromStorage(sendId: string): Promise<void> {
|
||||
if (this.sendAccessTokenDictGlobalState != null) {
|
||||
await this.sendAccessTokenDictGlobalState.update(
|
||||
(sendAccessTokenDict) => {
|
||||
if (!sendAccessTokenDict) {
|
||||
// If the dict is empty or undefined, there's nothing to clear
|
||||
return sendAccessTokenDict;
|
||||
}
|
||||
|
||||
if (sendAccessTokenDict[sendId] == null) {
|
||||
// If the specific sendId does not exist, nothing to clear
|
||||
return sendAccessTokenDict;
|
||||
}
|
||||
|
||||
// Destructure to omit the specific sendId and get new reference for the rest of the dict for an immutable update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [sendId]: _, ...rest } = sendAccessTokenDict;
|
||||
|
||||
return rest;
|
||||
},
|
||||
{
|
||||
// only update if the value is defined (to avoid unnecessary writes)
|
||||
shouldUpdate: (prevDict) => prevDict?.[sendId] != null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an error from the SDK send access token request process.
|
||||
* @param e The error to normalize.
|
||||
* @returns A normalized GetSendAccessTokenError.
|
||||
*/
|
||||
private normalizeSendAccessTokenError(e: unknown): GetSendAccessTokenError {
|
||||
if (this.isSendAccessTokenError(e)) {
|
||||
if (e.kind === "unexpected") {
|
||||
return { kind: "unexpected_server", error: e.data };
|
||||
}
|
||||
return { kind: "expected_server", error: e.data };
|
||||
}
|
||||
|
||||
if (e instanceof Error) {
|
||||
return { kind: "unknown", error: e.message };
|
||||
}
|
||||
|
||||
try {
|
||||
return { kind: "unknown", error: JSON.stringify(e) };
|
||||
} catch {
|
||||
return { kind: "unknown", error: "error cannot be stringified" };
|
||||
}
|
||||
}
|
||||
|
||||
private isSendAccessTokenError(e: unknown): e is SendAccessTokenError {
|
||||
return (
|
||||
typeof e === "object" &&
|
||||
e !== null &&
|
||||
"kind" in e &&
|
||||
(e.kind === "expected" || e.kind === "unexpected")
|
||||
);
|
||||
}
|
||||
|
||||
private validateSendId(sendId: string): void {
|
||||
if (sendId == null || sendId.trim() === "") {
|
||||
throw new Error("sendId must be provided.");
|
||||
}
|
||||
}
|
||||
|
||||
private validateCredentialsRequest(
|
||||
sendId: string,
|
||||
sendAccessCredentials: SendAccessDomainCredentials,
|
||||
): void {
|
||||
this.validateSendId(sendId);
|
||||
if (sendAccessCredentials == null) {
|
||||
throw new Error("sendAccessCredentials must be provided.");
|
||||
}
|
||||
|
||||
if (sendAccessCredentials.kind === "password" && !sendAccessCredentials.passwordHashB64) {
|
||||
throw new Error("passwordHashB64 must be provided for password credentials.");
|
||||
}
|
||||
|
||||
if (sendAccessCredentials.kind === "email" && !sendAccessCredentials.email) {
|
||||
throw new Error("email must be provided for email credentials.");
|
||||
}
|
||||
|
||||
if (
|
||||
sendAccessCredentials.kind === "email_otp" &&
|
||||
(!sendAccessCredentials.email || !sendAccessCredentials.otp)
|
||||
) {
|
||||
throw new Error("email and otp must be provided for email_otp credentials.");
|
||||
}
|
||||
}
|
||||
|
||||
private convertDomainCredentialsToSdkCredentials(
|
||||
sendAccessCredentials: SendAccessDomainCredentials,
|
||||
): SendAccessCredentials {
|
||||
switch (sendAccessCredentials.kind) {
|
||||
case "password":
|
||||
return {
|
||||
passwordHashB64: sendAccessCredentials.passwordHashB64,
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
email: sendAccessCredentials.email,
|
||||
};
|
||||
case "email_otp":
|
||||
return {
|
||||
email: sendAccessCredentials.email,
|
||||
otp: sendAccessCredentials.otp,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/common/src/auth/send-access/services/index.ts
Normal file
1
libs/common/src/auth/send-access/services/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./default-send-token.service";
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { KeyDefinition, SEND_ACCESS_DISK } from "@bitwarden/state";
|
||||
|
||||
import { SendAccessToken } from "../models/send-access-token";
|
||||
|
||||
export const SEND_ACCESS_TOKEN_DICT = KeyDefinition.record<SendAccessToken, string>(
|
||||
SEND_ACCESS_DISK,
|
||||
"accessTokenDict",
|
||||
{
|
||||
deserializer: (sendAccessTokenJson: Jsonify<SendAccessToken>) => {
|
||||
return SendAccessToken.fromJson(sendAccessTokenJson);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { UnexpectedIdentityError, SendAccessTokenApiErrorResponse } from "@bitwarden/sdk-internal";
|
||||
|
||||
/**
|
||||
* Represents the possible errors that can occur when retrieving a SendAccessToken.
|
||||
* Note: for expected_server errors, see invalid-request-errors.type.ts and
|
||||
* invalid-grant-errors.type.ts for type guards that identify specific
|
||||
* SendAccessTokenApiErrorResponse errors
|
||||
*/
|
||||
export type GetSendAccessTokenError =
|
||||
| { kind: "unexpected_server"; error: UnexpectedIdentityError }
|
||||
| { kind: "expected_server"; error: SendAccessTokenApiErrorResponse }
|
||||
| { kind: "unknown"; error: string };
|
||||
7
libs/common/src/auth/send-access/types/index.ts
Normal file
7
libs/common/src/auth/send-access/types/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./try-get-send-access-token-error.type";
|
||||
export * from "./send-otp.type";
|
||||
export * from "./send-hashed-password-b64.type";
|
||||
export * from "./send-access-domain-credentials.type";
|
||||
export * from "./invalid-request-errors.type";
|
||||
export * from "./invalid-grant-errors.type";
|
||||
export * from "./get-send-access-token-error.type";
|
||||
@@ -0,0 +1,62 @@
|
||||
import { SendAccessTokenApiErrorResponse } from "@bitwarden/sdk-internal";
|
||||
|
||||
export type InvalidGrant = Extract<SendAccessTokenApiErrorResponse, { error: "invalid_grant" }>;
|
||||
|
||||
export function isInvalidGrant(e: SendAccessTokenApiErrorResponse): e is InvalidGrant {
|
||||
return e.error === "invalid_grant";
|
||||
}
|
||||
|
||||
export type BareInvalidGrant = Extract<
|
||||
SendAccessTokenApiErrorResponse,
|
||||
{ error: "invalid_grant" }
|
||||
> & { send_access_error_type?: undefined };
|
||||
|
||||
export function isBareInvalidGrant(e: SendAccessTokenApiErrorResponse): e is BareInvalidGrant {
|
||||
return e.error === "invalid_grant" && e.send_access_error_type === undefined;
|
||||
}
|
||||
|
||||
export type SendIdInvalid = InvalidGrant & {
|
||||
send_access_error_type: "send_id_invalid";
|
||||
};
|
||||
export function sendIdInvalid(e: SendAccessTokenApiErrorResponse): e is SendIdInvalid {
|
||||
return e.error === "invalid_grant" && e.send_access_error_type === "send_id_invalid";
|
||||
}
|
||||
|
||||
export type PasswordHashB64Invalid = InvalidGrant & {
|
||||
send_access_error_type: "password_hash_b64_invalid";
|
||||
};
|
||||
export function passwordHashB64Invalid(
|
||||
e: SendAccessTokenApiErrorResponse,
|
||||
): e is PasswordHashB64Invalid {
|
||||
return e.error === "invalid_grant" && e.send_access_error_type === "password_hash_b64_invalid";
|
||||
}
|
||||
|
||||
export type EmailInvalid = InvalidGrant & {
|
||||
send_access_error_type: "email_invalid";
|
||||
};
|
||||
export function emailInvalid(e: SendAccessTokenApiErrorResponse): e is EmailInvalid {
|
||||
return e.error === "invalid_grant" && e.send_access_error_type === "email_invalid";
|
||||
}
|
||||
|
||||
export type OtpInvalid = InvalidGrant & {
|
||||
send_access_error_type: "otp_invalid";
|
||||
};
|
||||
export function otpInvalid(e: SendAccessTokenApiErrorResponse): e is OtpInvalid {
|
||||
return e.error === "invalid_grant" && e.send_access_error_type === "otp_invalid";
|
||||
}
|
||||
|
||||
export type OtpGenerationFailed = InvalidGrant & {
|
||||
send_access_error_type: "otp_generation_failed";
|
||||
};
|
||||
export function otpGenerationFailed(e: SendAccessTokenApiErrorResponse): e is OtpGenerationFailed {
|
||||
return e.error === "invalid_grant" && e.send_access_error_type === "otp_generation_failed";
|
||||
}
|
||||
|
||||
export type UnknownInvalidGrant = InvalidGrant & {
|
||||
send_access_error_type: "unknown";
|
||||
};
|
||||
export function isUnknownInvalidGrant(
|
||||
e: SendAccessTokenApiErrorResponse,
|
||||
): e is UnknownInvalidGrant {
|
||||
return e.error === "invalid_grant" && e.send_access_error_type === "unknown";
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { SendAccessTokenApiErrorResponse } from "@bitwarden/sdk-internal";
|
||||
|
||||
export type InvalidRequest = Extract<SendAccessTokenApiErrorResponse, { error: "invalid_request" }>;
|
||||
|
||||
export function isInvalidRequest(e: SendAccessTokenApiErrorResponse): e is InvalidRequest {
|
||||
return e.error === "invalid_request";
|
||||
}
|
||||
|
||||
export type BareInvalidRequest = Extract<
|
||||
SendAccessTokenApiErrorResponse,
|
||||
{ error: "invalid_request" }
|
||||
> & { send_access_error_type?: undefined };
|
||||
|
||||
export function isBareInvalidRequest(e: SendAccessTokenApiErrorResponse): e is BareInvalidRequest {
|
||||
return e.error === "invalid_request" && e.send_access_error_type === undefined;
|
||||
}
|
||||
|
||||
export type SendIdRequired = InvalidRequest & {
|
||||
send_access_error_type: "send_id_required";
|
||||
};
|
||||
|
||||
export function sendIdRequired(e: SendAccessTokenApiErrorResponse): e is SendIdRequired {
|
||||
return e.error === "invalid_request" && e.send_access_error_type === "send_id_required";
|
||||
}
|
||||
|
||||
export type PasswordHashB64Required = InvalidRequest & {
|
||||
send_access_error_type: "password_hash_b64_required";
|
||||
};
|
||||
|
||||
export function passwordHashB64Required(
|
||||
e: SendAccessTokenApiErrorResponse,
|
||||
): e is PasswordHashB64Required {
|
||||
return e.error === "invalid_request" && e.send_access_error_type === "password_hash_b64_required";
|
||||
}
|
||||
|
||||
export type EmailRequired = InvalidRequest & { send_access_error_type: "email_required" };
|
||||
|
||||
export function emailRequired(e: SendAccessTokenApiErrorResponse): e is EmailRequired {
|
||||
return e.error === "invalid_request" && e.send_access_error_type === "email_required";
|
||||
}
|
||||
|
||||
export type EmailAndOtpRequiredEmailSent = InvalidRequest & {
|
||||
send_access_error_type: "email_and_otp_required_otp_sent";
|
||||
};
|
||||
|
||||
export function emailAndOtpRequiredEmailSent(
|
||||
e: SendAccessTokenApiErrorResponse,
|
||||
): e is EmailAndOtpRequiredEmailSent {
|
||||
return (
|
||||
e.error === "invalid_request" && e.send_access_error_type === "email_and_otp_required_otp_sent"
|
||||
);
|
||||
}
|
||||
|
||||
export type UnknownInvalidRequest = InvalidRequest & {
|
||||
send_access_error_type: "unknown";
|
||||
};
|
||||
|
||||
export function isUnknownInvalidRequest(
|
||||
e: SendAccessTokenApiErrorResponse,
|
||||
): e is UnknownInvalidRequest {
|
||||
return e.error === "invalid_request" && e.send_access_error_type === "unknown";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SendHashedPasswordB64 } from "./send-hashed-password-b64.type";
|
||||
import { SendOtp } from "./send-otp.type";
|
||||
|
||||
/**
|
||||
* The domain facing send access credentials
|
||||
* Will be internally mapped to the SDK types
|
||||
*/
|
||||
export type SendAccessDomainCredentials =
|
||||
| { kind: "password"; passwordHashB64: SendHashedPasswordB64 }
|
||||
| { kind: "email"; email: string }
|
||||
| { kind: "email_otp"; email: string; otp: SendOtp };
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
export type SendHashedPasswordB64 = Opaque<string, "SendHashedPasswordB64">;
|
||||
3
libs/common/src/auth/send-access/types/send-otp.type.ts
Normal file
3
libs/common/src/auth/send-access/types/send-otp.type.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Opaque } from "type-fest";
|
||||
|
||||
export type SendOtp = Opaque<string, "SendOtp">;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { GetSendAccessTokenError } from "./get-send-access-token-error.type";
|
||||
|
||||
/**
|
||||
* Represents the possible errors that can occur when trying to retrieve a SendAccessToken by
|
||||
* just a sendId. Extends {@link GetSendAccessTokenError}.
|
||||
*/
|
||||
export type TryGetSendAccessTokenError = { kind: "expired" } | GetSendAccessTokenError;
|
||||
@@ -38,7 +38,7 @@ describe("WebAuthnLoginService", () => {
|
||||
|
||||
// We must do this to make the mocked classes available for all the
|
||||
// assertCredential(...) tests.
|
||||
global.PublicKeyCredential = MockPublicKeyCredential;
|
||||
global.PublicKeyCredential = MockPublicKeyCredential as any;
|
||||
global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
|
||||
|
||||
// Save the original navigator
|
||||
@@ -316,6 +316,10 @@ class MockPublicKeyCredential implements PublicKeyCredential {
|
||||
static isUserVerifyingPlatformAuthenticatorAvailable(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
function buildCredentialAssertionOptions(): WebAuthnLoginCredentialAssertionOptionsView {
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
|
||||
import { PlanResponse } from "../../billing/models/response/plan.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { PaymentMethodType } from "../enums";
|
||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request";
|
||||
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
|
||||
import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request";
|
||||
import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request";
|
||||
import { InvoicesResponse } from "../models/response/invoices.response";
|
||||
import { PaymentMethodResponse } from "../models/response/payment-method.response";
|
||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||
|
||||
export abstract class BillingApiServiceAbstraction {
|
||||
@@ -29,14 +22,10 @@ export abstract class BillingApiServiceAbstraction {
|
||||
request: CreateClientOrganizationRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract createSetupIntent(paymentMethodType: PaymentMethodType): Promise<string>;
|
||||
|
||||
abstract getOrganizationBillingMetadata(
|
||||
organizationId: string,
|
||||
): Promise<OrganizationBillingMetadataResponse>;
|
||||
|
||||
abstract getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse>;
|
||||
|
||||
abstract getPlans(): Promise<ListResponse<PlanResponse>>;
|
||||
|
||||
abstract getProviderClientInvoiceReport(providerId: string, invoiceId: string): Promise<string>;
|
||||
@@ -49,44 +38,12 @@ export abstract class BillingApiServiceAbstraction {
|
||||
|
||||
abstract getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse>;
|
||||
|
||||
abstract getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse>;
|
||||
|
||||
abstract updateOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract updateOrganizationTaxInformation(
|
||||
organizationId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract updateProviderClientOrganization(
|
||||
providerId: string,
|
||||
organizationId: string,
|
||||
request: UpdateClientOrganizationRequest,
|
||||
): Promise<any>;
|
||||
|
||||
abstract updateProviderPaymentMethod(
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract updateProviderTaxInformation(
|
||||
providerId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract verifyOrganizationBankAccount(
|
||||
organizationId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract verifyProviderBankAccount(
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void>;
|
||||
|
||||
abstract restartSubscription(
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { UserId } from "@bitwarden/user-core";
|
||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||
import { InitiationPath } from "../../models/request/reference-event.request";
|
||||
import { PaymentMethodType, PlanType } from "../enums";
|
||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||
|
||||
export type OrganizationInformation = {
|
||||
name: string;
|
||||
@@ -45,8 +44,6 @@ export type SubscriptionInformation = {
|
||||
};
|
||||
|
||||
export abstract class OrganizationBillingServiceAbstraction {
|
||||
abstract getPaymentSource(organizationId: string): Promise<PaymentSourceResponse>;
|
||||
|
||||
abstract purchaseSubscription(
|
||||
subscription: SubscriptionInformation,
|
||||
activeUserId: UserId,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { CountryListItem } from "../models/domain";
|
||||
import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
|
||||
import { PreviewTaxAmountForOrganizationTrialRequest } from "../models/request/tax";
|
||||
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
|
||||
|
||||
export abstract class TaxServiceAbstraction {
|
||||
abstract getCountries(): CountryListItem[];
|
||||
|
||||
abstract isCountrySupported(country: string): Promise<boolean>;
|
||||
|
||||
abstract previewIndividualInvoice(
|
||||
request: PreviewIndividualInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse>;
|
||||
|
||||
abstract previewOrganizationInvoice(
|
||||
request: PreviewOrganizationInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse>;
|
||||
|
||||
abstract previewTaxAmountForOrganizationTrial: (
|
||||
request: PreviewTaxAmountForOrganizationTrialRequest,
|
||||
) => Promise<number>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum BitwardenProductType {
|
||||
PasswordManager = 0,
|
||||
SecretsManager = 1,
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export * from "./payment-method-type.enum";
|
||||
export * from "./plan-sponsorship-type.enum";
|
||||
export * from "./plan-type.enum";
|
||||
export * from "./transaction-type.enum";
|
||||
export * from "./bitwarden-product-type.enum";
|
||||
export * from "./product-tier-type.enum";
|
||||
export * from "./product-type.enum";
|
||||
export * from "./plan-interval.enum";
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { TaxInformation } from "../domain/tax-information";
|
||||
|
||||
import { TaxInfoUpdateRequest } from "./tax-info-update.request";
|
||||
|
||||
export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest {
|
||||
taxId: string;
|
||||
line1: string;
|
||||
line2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
|
||||
static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest {
|
||||
if (!taxInformation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const request = new ExpandedTaxInfoUpdateRequest();
|
||||
request.country = taxInformation.country;
|
||||
request.postalCode = taxInformation.postalCode;
|
||||
request.taxId = taxInformation.taxId;
|
||||
request.line1 = taxInformation.line1;
|
||||
request.line2 = taxInformation.line2;
|
||||
request.city = taxInformation.city;
|
||||
request.state = taxInformation.state;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { PaymentMethodType } from "../../enums";
|
||||
|
||||
import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request";
|
||||
|
||||
export class PaymentRequest extends ExpandedTaxInfoUpdateRequest {
|
||||
paymentMethodType: PaymentMethodType;
|
||||
paymentToken: string;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// @ts-strict-ignore
|
||||
export class PreviewIndividualInvoiceRequest {
|
||||
passwordManager: PasswordManager;
|
||||
taxInformation: TaxInformation;
|
||||
|
||||
constructor(passwordManager: PasswordManager, taxInformation: TaxInformation) {
|
||||
this.passwordManager = passwordManager;
|
||||
this.taxInformation = taxInformation;
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordManager {
|
||||
additionalStorage: number;
|
||||
|
||||
constructor(additionalStorage: number) {
|
||||
this.additionalStorage = additionalStorage;
|
||||
}
|
||||
}
|
||||
|
||||
class TaxInformation {
|
||||
postalCode: string;
|
||||
country: string;
|
||||
|
||||
constructor(postalCode: string, country: string) {
|
||||
this.postalCode = postalCode;
|
||||
this.country = country;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { PlanSponsorshipType, PlanType } from "../../enums";
|
||||
|
||||
export class PreviewOrganizationInvoiceRequest {
|
||||
organizationId?: string;
|
||||
passwordManager: PasswordManager;
|
||||
secretsManager?: SecretsManager;
|
||||
taxInformation: TaxInformation;
|
||||
|
||||
constructor(
|
||||
passwordManager: PasswordManager,
|
||||
taxInformation: TaxInformation,
|
||||
organizationId?: string,
|
||||
secretsManager?: SecretsManager,
|
||||
) {
|
||||
this.organizationId = organizationId;
|
||||
this.passwordManager = passwordManager;
|
||||
this.secretsManager = secretsManager;
|
||||
this.taxInformation = taxInformation;
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordManager {
|
||||
plan: PlanType;
|
||||
sponsoredPlan?: PlanSponsorshipType;
|
||||
seats: number;
|
||||
additionalStorage: number;
|
||||
|
||||
constructor(plan: PlanType, seats: number, additionalStorage: number) {
|
||||
this.plan = plan;
|
||||
this.seats = seats;
|
||||
this.additionalStorage = additionalStorage;
|
||||
}
|
||||
}
|
||||
|
||||
class SecretsManager {
|
||||
seats: number;
|
||||
additionalMachineAccounts: number;
|
||||
|
||||
constructor(seats: number, additionalMachineAccounts: number) {
|
||||
this.seats = seats;
|
||||
this.additionalMachineAccounts = additionalMachineAccounts;
|
||||
}
|
||||
}
|
||||
|
||||
class TaxInformation {
|
||||
postalCode: string;
|
||||
country: string;
|
||||
taxId: string;
|
||||
|
||||
constructor(postalCode: string, country: string, taxId: string) {
|
||||
this.postalCode = postalCode;
|
||||
this.country = country;
|
||||
this.taxId = taxId;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
export class TaxInfoUpdateRequest {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./preview-tax-amount-for-organization-trial.request";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { PlanType, ProductType } from "../../../enums";
|
||||
|
||||
export type PreviewTaxAmountForOrganizationTrialRequest = {
|
||||
planType: PlanType;
|
||||
productType: ProductType;
|
||||
taxInformation: {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
taxId?: string;
|
||||
};
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { PaymentMethodType } from "../../enums";
|
||||
|
||||
export class TokenizedPaymentSourceRequest {
|
||||
type: PaymentMethodType;
|
||||
token: string;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request";
|
||||
import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request";
|
||||
|
||||
export class UpdatePaymentMethodRequest {
|
||||
paymentSource: TokenizedPaymentSourceRequest;
|
||||
taxInformation: ExpandedTaxInfoUpdateRequest;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export class VerifyBankAccountRequest {
|
||||
descriptorCode: string;
|
||||
|
||||
constructor(descriptorCode: string) {
|
||||
this.descriptorCode = descriptorCode;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { BillingSourceResponse } from "./billing.response";
|
||||
|
||||
export class BillingPaymentResponse extends BaseResponse {
|
||||
balance: number;
|
||||
paymentSource: BillingSourceResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.balance = this.getResponseProperty("Balance");
|
||||
const paymentSource = this.getResponseProperty("PaymentSource");
|
||||
this.paymentSource = paymentSource == null ? null : new BillingSourceResponse(paymentSource);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { PaymentSourceResponse } from "./payment-source.response";
|
||||
import { TaxInfoResponse } from "./tax-info.response";
|
||||
|
||||
export class PaymentMethodResponse extends BaseResponse {
|
||||
accountCredit: number;
|
||||
paymentSource?: PaymentSourceResponse;
|
||||
subscriptionStatus?: string;
|
||||
taxInformation?: TaxInfoResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.accountCredit = this.getResponseProperty("AccountCredit");
|
||||
|
||||
const paymentSource = this.getResponseProperty("PaymentSource");
|
||||
if (paymentSource) {
|
||||
this.paymentSource = new PaymentSourceResponse(paymentSource);
|
||||
}
|
||||
|
||||
this.subscriptionStatus = this.getResponseProperty("SubscriptionStatus");
|
||||
|
||||
const taxInformation = this.getResponseProperty("TaxInformation");
|
||||
if (taxInformation) {
|
||||
this.taxInformation = new TaxInfoResponse(taxInformation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class TaxIdTypesResponse extends BaseResponse {
|
||||
taxIdTypes: TaxIdTypeResponse[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
const taxIdTypes = this.getResponseProperty("TaxIdTypes");
|
||||
if (taxIdTypes && taxIdTypes.length) {
|
||||
this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class TaxIdTypeResponse extends BaseResponse {
|
||||
code: string;
|
||||
country: string;
|
||||
description: string;
|
||||
example: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.code = this.getResponseProperty("Code");
|
||||
this.country = this.getResponseProperty("Country");
|
||||
this.description = this.getResponseProperty("Description");
|
||||
this.example = this.getResponseProperty("Example");
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./preview-tax-amount.response";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BaseResponse } from "../../../../models/response/base.response";
|
||||
|
||||
export class PreviewTaxAmountResponse extends BaseResponse {
|
||||
taxAmount: number;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.taxAmount = this.getResponseProperty("TaxAmount");
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { BillingApiServiceAbstraction } from "../abstractions";
|
||||
import { PaymentMethodType } from "../enums";
|
||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request";
|
||||
import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request";
|
||||
import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request";
|
||||
import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request";
|
||||
import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request";
|
||||
import { InvoicesResponse } from "../models/response/invoices.response";
|
||||
import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response";
|
||||
import { PaymentMethodResponse } from "../models/response/payment-method.response";
|
||||
import { PlanResponse } from "../models/response/plan.response";
|
||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||
|
||||
@@ -54,21 +47,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async createSetupIntent(type: PaymentMethodType) {
|
||||
const getPath = () => {
|
||||
switch (type) {
|
||||
case PaymentMethodType.BankAccount: {
|
||||
return "/setup-intent/bank-account";
|
||||
}
|
||||
case PaymentMethodType.Card: {
|
||||
return "/setup-intent/card";
|
||||
}
|
||||
}
|
||||
};
|
||||
const response = await this.apiService.send("POST", getPath(), null, true, true);
|
||||
return response as string;
|
||||
}
|
||||
|
||||
async getOrganizationBillingMetadata(
|
||||
organizationId: string,
|
||||
): Promise<OrganizationBillingMetadataResponse> {
|
||||
@@ -83,17 +61,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
return new OrganizationBillingMetadataResponse(r);
|
||||
}
|
||||
|
||||
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PaymentMethodResponse(response);
|
||||
}
|
||||
|
||||
async getPlans(): Promise<ListResponse<PlanResponse>> {
|
||||
const r = await this.apiService.send("GET", "/plans", null, false, true);
|
||||
return new ListResponse(r, PlanResponse);
|
||||
@@ -145,43 +112,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
return new ProviderSubscriptionResponse(response);
|
||||
}
|
||||
|
||||
async getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/tax-information",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new TaxInfoResponse(response);
|
||||
}
|
||||
|
||||
async updateOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateOrganizationTaxInformation(
|
||||
organizationId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/organizations/" + organizationId + "/billing/tax-information",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderClientOrganization(
|
||||
providerId: string,
|
||||
organizationId: string,
|
||||
@@ -196,55 +126,6 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderPaymentMethod(
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/payment-method",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/tax-information",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async verifyOrganizationBankAccount(
|
||||
organizationId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + organizationId + "/billing/payment-method/verify-bank-account",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async verifyProviderBankAccount(
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + providerId + "/billing/payment-method/verify-bank-account",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async restartSubscription(
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
|
||||
@@ -23,7 +23,6 @@ import { OrganizationResponse } from "../../admin-console/models/response/organi
|
||||
import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { OrgKey } from "../../types/key";
|
||||
import { PaymentMethodResponse } from "../models/response/payment-method.response";
|
||||
|
||||
describe("OrganizationBillingService", () => {
|
||||
let apiService: jest.Mocked<ApiService>;
|
||||
@@ -62,47 +61,6 @@ describe("OrganizationBillingService", () => {
|
||||
return jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("getPaymentSource()", () => {
|
||||
it("given a valid organization id, then it returns a payment source", async () => {
|
||||
//Arrange
|
||||
const orgId = "organization-test";
|
||||
const paymentMethodResponse = {
|
||||
paymentSource: { type: PaymentMethodType.Card },
|
||||
} as PaymentMethodResponse;
|
||||
billingApiService.getOrganizationPaymentMethod.mockResolvedValue(paymentMethodResponse);
|
||||
|
||||
//Act
|
||||
const returnedPaymentSource = await sut.getPaymentSource(orgId);
|
||||
|
||||
//Assert
|
||||
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||
expect(returnedPaymentSource).toEqual(paymentMethodResponse.paymentSource);
|
||||
});
|
||||
|
||||
it("given an invalid organizationId, it should return undefined", async () => {
|
||||
//Arrange
|
||||
const orgId = "invalid-id";
|
||||
billingApiService.getOrganizationPaymentMethod.mockResolvedValue(null);
|
||||
|
||||
//Act
|
||||
const returnedPaymentSource = await sut.getPaymentSource(orgId);
|
||||
|
||||
//Assert
|
||||
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||
expect(returnedPaymentSource).toBeUndefined();
|
||||
});
|
||||
|
||||
it("given an API error occurs, then it throws the error", async () => {
|
||||
// Arrange
|
||||
const orgId = "error-org";
|
||||
billingApiService.getOrganizationPaymentMethod.mockRejectedValue(new Error("API Error"));
|
||||
|
||||
// Act & Assert
|
||||
await expect(sut.getPaymentSource(orgId)).rejects.toThrow("API Error");
|
||||
expect(billingApiService.getOrganizationPaymentMethod).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("purchaseSubscription()", () => {
|
||||
it("given valid subscription information, then it returns successful response", async () => {
|
||||
//Arrange
|
||||
@@ -118,7 +76,7 @@ describe("OrganizationBillingService", () => {
|
||||
const organizationResponse = {
|
||||
name: subscriptionInformation.organization.name,
|
||||
billingEmail: subscriptionInformation.organization.billingEmail,
|
||||
planType: subscriptionInformation.plan.type,
|
||||
planType: subscriptionInformation.plan!.type,
|
||||
} as OrganizationResponse;
|
||||
|
||||
organizationApiService.create.mockResolvedValue(organizationResponse);
|
||||
@@ -201,8 +159,8 @@ describe("OrganizationBillingService", () => {
|
||||
|
||||
const organizationResponse = {
|
||||
name: subscriptionInformation.organization.name,
|
||||
plan: { type: subscriptionInformation.plan.type },
|
||||
planType: subscriptionInformation.plan.type,
|
||||
plan: { type: subscriptionInformation.plan!.type },
|
||||
planType: subscriptionInformation.plan!.type,
|
||||
} as OrganizationResponse;
|
||||
|
||||
organizationApiService.createWithoutPayment.mockResolvedValue(organizationResponse);
|
||||
@@ -262,7 +220,7 @@ describe("OrganizationBillingService", () => {
|
||||
const organizationResponse = {
|
||||
name: subscriptionInformation.organization.name,
|
||||
billingEmail: subscriptionInformation.organization.billingEmail,
|
||||
planType: subscriptionInformation.plan.type,
|
||||
planType: subscriptionInformation.plan!.type,
|
||||
} as OrganizationResponse;
|
||||
|
||||
organizationApiService.create.mockResolvedValue(organizationResponse);
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from "../abstractions";
|
||||
import { PlanType } from "../enums";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
|
||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||
|
||||
interface OrganizationKeys {
|
||||
encryptedKey: EncString;
|
||||
@@ -45,11 +44,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
|
||||
const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId);
|
||||
return paymentMethod?.paymentSource;
|
||||
}
|
||||
|
||||
async purchaseSubscription(
|
||||
subscription: SubscriptionInformation,
|
||||
activeUserId: UserId,
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
import { PreviewTaxAmountForOrganizationTrialRequest } from "@bitwarden/common/billing/models/request/tax";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction";
|
||||
import { CountryListItem } from "../models/domain";
|
||||
import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request";
|
||||
import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response";
|
||||
|
||||
export class TaxService implements TaxServiceAbstraction {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
getCountries(): CountryListItem[] {
|
||||
return [
|
||||
{ name: "-- Select --", value: "", disabled: false },
|
||||
{ name: "United States", value: "US", disabled: false },
|
||||
{ name: "China", value: "CN", disabled: false },
|
||||
{ name: "France", value: "FR", disabled: false },
|
||||
{ name: "Germany", value: "DE", disabled: false },
|
||||
{ name: "Canada", value: "CA", disabled: false },
|
||||
{ name: "United Kingdom", value: "GB", disabled: false },
|
||||
{ name: "Australia", value: "AU", disabled: false },
|
||||
{ name: "India", value: "IN", disabled: false },
|
||||
{ name: "", value: "-", disabled: true },
|
||||
{ name: "Afghanistan", value: "AF", disabled: false },
|
||||
{ name: "Åland Islands", value: "AX", disabled: false },
|
||||
{ name: "Albania", value: "AL", disabled: false },
|
||||
{ name: "Algeria", value: "DZ", disabled: false },
|
||||
{ name: "American Samoa", value: "AS", disabled: false },
|
||||
{ name: "Andorra", value: "AD", disabled: false },
|
||||
{ name: "Angola", value: "AO", disabled: false },
|
||||
{ name: "Anguilla", value: "AI", disabled: false },
|
||||
{ name: "Antarctica", value: "AQ", disabled: false },
|
||||
{ name: "Antigua and Barbuda", value: "AG", disabled: false },
|
||||
{ name: "Argentina", value: "AR", disabled: false },
|
||||
{ name: "Armenia", value: "AM", disabled: false },
|
||||
{ name: "Aruba", value: "AW", disabled: false },
|
||||
{ name: "Austria", value: "AT", disabled: false },
|
||||
{ name: "Azerbaijan", value: "AZ", disabled: false },
|
||||
{ name: "Bahamas", value: "BS", disabled: false },
|
||||
{ name: "Bahrain", value: "BH", disabled: false },
|
||||
{ name: "Bangladesh", value: "BD", disabled: false },
|
||||
{ name: "Barbados", value: "BB", disabled: false },
|
||||
{ name: "Belarus", value: "BY", disabled: false },
|
||||
{ name: "Belgium", value: "BE", disabled: false },
|
||||
{ name: "Belize", value: "BZ", disabled: false },
|
||||
{ name: "Benin", value: "BJ", disabled: false },
|
||||
{ name: "Bermuda", value: "BM", disabled: false },
|
||||
{ name: "Bhutan", value: "BT", disabled: false },
|
||||
{ name: "Bolivia, Plurinational State of", value: "BO", disabled: false },
|
||||
{ name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false },
|
||||
{ name: "Bosnia and Herzegovina", value: "BA", disabled: false },
|
||||
{ name: "Botswana", value: "BW", disabled: false },
|
||||
{ name: "Bouvet Island", value: "BV", disabled: false },
|
||||
{ name: "Brazil", value: "BR", disabled: false },
|
||||
{ name: "British Indian Ocean Territory", value: "IO", disabled: false },
|
||||
{ name: "Brunei Darussalam", value: "BN", disabled: false },
|
||||
{ name: "Bulgaria", value: "BG", disabled: false },
|
||||
{ name: "Burkina Faso", value: "BF", disabled: false },
|
||||
{ name: "Burundi", value: "BI", disabled: false },
|
||||
{ name: "Cambodia", value: "KH", disabled: false },
|
||||
{ name: "Cameroon", value: "CM", disabled: false },
|
||||
{ name: "Cape Verde", value: "CV", disabled: false },
|
||||
{ name: "Cayman Islands", value: "KY", disabled: false },
|
||||
{ name: "Central African Republic", value: "CF", disabled: false },
|
||||
{ name: "Chad", value: "TD", disabled: false },
|
||||
{ name: "Chile", value: "CL", disabled: false },
|
||||
{ name: "Christmas Island", value: "CX", disabled: false },
|
||||
{ name: "Cocos (Keeling) Islands", value: "CC", disabled: false },
|
||||
{ name: "Colombia", value: "CO", disabled: false },
|
||||
{ name: "Comoros", value: "KM", disabled: false },
|
||||
{ name: "Congo", value: "CG", disabled: false },
|
||||
{ name: "Congo, the Democratic Republic of the", value: "CD", disabled: false },
|
||||
{ name: "Cook Islands", value: "CK", disabled: false },
|
||||
{ name: "Costa Rica", value: "CR", disabled: false },
|
||||
{ name: "Côte d'Ivoire", value: "CI", disabled: false },
|
||||
{ name: "Croatia", value: "HR", disabled: false },
|
||||
{ name: "Cuba", value: "CU", disabled: false },
|
||||
{ name: "Curaçao", value: "CW", disabled: false },
|
||||
{ name: "Cyprus", value: "CY", disabled: false },
|
||||
{ name: "Czech Republic", value: "CZ", disabled: false },
|
||||
{ name: "Denmark", value: "DK", disabled: false },
|
||||
{ name: "Djibouti", value: "DJ", disabled: false },
|
||||
{ name: "Dominica", value: "DM", disabled: false },
|
||||
{ name: "Dominican Republic", value: "DO", disabled: false },
|
||||
{ name: "Ecuador", value: "EC", disabled: false },
|
||||
{ name: "Egypt", value: "EG", disabled: false },
|
||||
{ name: "El Salvador", value: "SV", disabled: false },
|
||||
{ name: "Equatorial Guinea", value: "GQ", disabled: false },
|
||||
{ name: "Eritrea", value: "ER", disabled: false },
|
||||
{ name: "Estonia", value: "EE", disabled: false },
|
||||
{ name: "Ethiopia", value: "ET", disabled: false },
|
||||
{ name: "Falkland Islands (Malvinas)", value: "FK", disabled: false },
|
||||
{ name: "Faroe Islands", value: "FO", disabled: false },
|
||||
{ name: "Fiji", value: "FJ", disabled: false },
|
||||
{ name: "Finland", value: "FI", disabled: false },
|
||||
{ name: "French Guiana", value: "GF", disabled: false },
|
||||
{ name: "French Polynesia", value: "PF", disabled: false },
|
||||
{ name: "French Southern Territories", value: "TF", disabled: false },
|
||||
{ name: "Gabon", value: "GA", disabled: false },
|
||||
{ name: "Gambia", value: "GM", disabled: false },
|
||||
{ name: "Georgia", value: "GE", disabled: false },
|
||||
{ name: "Ghana", value: "GH", disabled: false },
|
||||
{ name: "Gibraltar", value: "GI", disabled: false },
|
||||
{ name: "Greece", value: "GR", disabled: false },
|
||||
{ name: "Greenland", value: "GL", disabled: false },
|
||||
{ name: "Grenada", value: "GD", disabled: false },
|
||||
{ name: "Guadeloupe", value: "GP", disabled: false },
|
||||
{ name: "Guam", value: "GU", disabled: false },
|
||||
{ name: "Guatemala", value: "GT", disabled: false },
|
||||
{ name: "Guernsey", value: "GG", disabled: false },
|
||||
{ name: "Guinea", value: "GN", disabled: false },
|
||||
{ name: "Guinea-Bissau", value: "GW", disabled: false },
|
||||
{ name: "Guyana", value: "GY", disabled: false },
|
||||
{ name: "Haiti", value: "HT", disabled: false },
|
||||
{ name: "Heard Island and McDonald Islands", value: "HM", disabled: false },
|
||||
{ name: "Holy See (Vatican City State)", value: "VA", disabled: false },
|
||||
{ name: "Honduras", value: "HN", disabled: false },
|
||||
{ name: "Hong Kong", value: "HK", disabled: false },
|
||||
{ name: "Hungary", value: "HU", disabled: false },
|
||||
{ name: "Iceland", value: "IS", disabled: false },
|
||||
{ name: "Indonesia", value: "ID", disabled: false },
|
||||
{ name: "Iran, Islamic Republic of", value: "IR", disabled: false },
|
||||
{ name: "Iraq", value: "IQ", disabled: false },
|
||||
{ name: "Ireland", value: "IE", disabled: false },
|
||||
{ name: "Isle of Man", value: "IM", disabled: false },
|
||||
{ name: "Israel", value: "IL", disabled: false },
|
||||
{ name: "Italy", value: "IT", disabled: false },
|
||||
{ name: "Jamaica", value: "JM", disabled: false },
|
||||
{ name: "Japan", value: "JP", disabled: false },
|
||||
{ name: "Jersey", value: "JE", disabled: false },
|
||||
{ name: "Jordan", value: "JO", disabled: false },
|
||||
{ name: "Kazakhstan", value: "KZ", disabled: false },
|
||||
{ name: "Kenya", value: "KE", disabled: false },
|
||||
{ name: "Kiribati", value: "KI", disabled: false },
|
||||
{ name: "Korea, Democratic People's Republic of", value: "KP", disabled: false },
|
||||
{ name: "Korea, Republic of", value: "KR", disabled: false },
|
||||
{ name: "Kuwait", value: "KW", disabled: false },
|
||||
{ name: "Kyrgyzstan", value: "KG", disabled: false },
|
||||
{ name: "Lao People's Democratic Republic", value: "LA", disabled: false },
|
||||
{ name: "Latvia", value: "LV", disabled: false },
|
||||
{ name: "Lebanon", value: "LB", disabled: false },
|
||||
{ name: "Lesotho", value: "LS", disabled: false },
|
||||
{ name: "Liberia", value: "LR", disabled: false },
|
||||
{ name: "Libya", value: "LY", disabled: false },
|
||||
{ name: "Liechtenstein", value: "LI", disabled: false },
|
||||
{ name: "Lithuania", value: "LT", disabled: false },
|
||||
{ name: "Luxembourg", value: "LU", disabled: false },
|
||||
{ name: "Macao", value: "MO", disabled: false },
|
||||
{ name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false },
|
||||
{ name: "Madagascar", value: "MG", disabled: false },
|
||||
{ name: "Malawi", value: "MW", disabled: false },
|
||||
{ name: "Malaysia", value: "MY", disabled: false },
|
||||
{ name: "Maldives", value: "MV", disabled: false },
|
||||
{ name: "Mali", value: "ML", disabled: false },
|
||||
{ name: "Malta", value: "MT", disabled: false },
|
||||
{ name: "Marshall Islands", value: "MH", disabled: false },
|
||||
{ name: "Martinique", value: "MQ", disabled: false },
|
||||
{ name: "Mauritania", value: "MR", disabled: false },
|
||||
{ name: "Mauritius", value: "MU", disabled: false },
|
||||
{ name: "Mayotte", value: "YT", disabled: false },
|
||||
{ name: "Mexico", value: "MX", disabled: false },
|
||||
{ name: "Micronesia, Federated States of", value: "FM", disabled: false },
|
||||
{ name: "Moldova, Republic of", value: "MD", disabled: false },
|
||||
{ name: "Monaco", value: "MC", disabled: false },
|
||||
{ name: "Mongolia", value: "MN", disabled: false },
|
||||
{ name: "Montenegro", value: "ME", disabled: false },
|
||||
{ name: "Montserrat", value: "MS", disabled: false },
|
||||
{ name: "Morocco", value: "MA", disabled: false },
|
||||
{ name: "Mozambique", value: "MZ", disabled: false },
|
||||
{ name: "Myanmar", value: "MM", disabled: false },
|
||||
{ name: "Namibia", value: "NA", disabled: false },
|
||||
{ name: "Nauru", value: "NR", disabled: false },
|
||||
{ name: "Nepal", value: "NP", disabled: false },
|
||||
{ name: "Netherlands", value: "NL", disabled: false },
|
||||
{ name: "New Caledonia", value: "NC", disabled: false },
|
||||
{ name: "New Zealand", value: "NZ", disabled: false },
|
||||
{ name: "Nicaragua", value: "NI", disabled: false },
|
||||
{ name: "Niger", value: "NE", disabled: false },
|
||||
{ name: "Nigeria", value: "NG", disabled: false },
|
||||
{ name: "Niue", value: "NU", disabled: false },
|
||||
{ name: "Norfolk Island", value: "NF", disabled: false },
|
||||
{ name: "Northern Mariana Islands", value: "MP", disabled: false },
|
||||
{ name: "Norway", value: "NO", disabled: false },
|
||||
{ name: "Oman", value: "OM", disabled: false },
|
||||
{ name: "Pakistan", value: "PK", disabled: false },
|
||||
{ name: "Palau", value: "PW", disabled: false },
|
||||
{ name: "Palestinian Territory, Occupied", value: "PS", disabled: false },
|
||||
{ name: "Panama", value: "PA", disabled: false },
|
||||
{ name: "Papua New Guinea", value: "PG", disabled: false },
|
||||
{ name: "Paraguay", value: "PY", disabled: false },
|
||||
{ name: "Peru", value: "PE", disabled: false },
|
||||
{ name: "Philippines", value: "PH", disabled: false },
|
||||
{ name: "Pitcairn", value: "PN", disabled: false },
|
||||
{ name: "Poland", value: "PL", disabled: false },
|
||||
{ name: "Portugal", value: "PT", disabled: false },
|
||||
{ name: "Puerto Rico", value: "PR", disabled: false },
|
||||
{ name: "Qatar", value: "QA", disabled: false },
|
||||
{ name: "Réunion", value: "RE", disabled: false },
|
||||
{ name: "Romania", value: "RO", disabled: false },
|
||||
{ name: "Russian Federation", value: "RU", disabled: false },
|
||||
{ name: "Rwanda", value: "RW", disabled: false },
|
||||
{ name: "Saint Barthélemy", value: "BL", disabled: false },
|
||||
{ name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false },
|
||||
{ name: "Saint Kitts and Nevis", value: "KN", disabled: false },
|
||||
{ name: "Saint Lucia", value: "LC", disabled: false },
|
||||
{ name: "Saint Martin (French part)", value: "MF", disabled: false },
|
||||
{ name: "Saint Pierre and Miquelon", value: "PM", disabled: false },
|
||||
{ name: "Saint Vincent and the Grenadines", value: "VC", disabled: false },
|
||||
{ name: "Samoa", value: "WS", disabled: false },
|
||||
{ name: "San Marino", value: "SM", disabled: false },
|
||||
{ name: "Sao Tome and Principe", value: "ST", disabled: false },
|
||||
{ name: "Saudi Arabia", value: "SA", disabled: false },
|
||||
{ name: "Senegal", value: "SN", disabled: false },
|
||||
{ name: "Serbia", value: "RS", disabled: false },
|
||||
{ name: "Seychelles", value: "SC", disabled: false },
|
||||
{ name: "Sierra Leone", value: "SL", disabled: false },
|
||||
{ name: "Singapore", value: "SG", disabled: false },
|
||||
{ name: "Sint Maarten (Dutch part)", value: "SX", disabled: false },
|
||||
{ name: "Slovakia", value: "SK", disabled: false },
|
||||
{ name: "Slovenia", value: "SI", disabled: false },
|
||||
{ name: "Solomon Islands", value: "SB", disabled: false },
|
||||
{ name: "Somalia", value: "SO", disabled: false },
|
||||
{ name: "South Africa", value: "ZA", disabled: false },
|
||||
{ name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false },
|
||||
{ name: "South Sudan", value: "SS", disabled: false },
|
||||
{ name: "Spain", value: "ES", disabled: false },
|
||||
{ name: "Sri Lanka", value: "LK", disabled: false },
|
||||
{ name: "Sudan", value: "SD", disabled: false },
|
||||
{ name: "Suriname", value: "SR", disabled: false },
|
||||
{ name: "Svalbard and Jan Mayen", value: "SJ", disabled: false },
|
||||
{ name: "Swaziland", value: "SZ", disabled: false },
|
||||
{ name: "Sweden", value: "SE", disabled: false },
|
||||
{ name: "Switzerland", value: "CH", disabled: false },
|
||||
{ name: "Syrian Arab Republic", value: "SY", disabled: false },
|
||||
{ name: "Taiwan", value: "TW", disabled: false },
|
||||
{ name: "Tajikistan", value: "TJ", disabled: false },
|
||||
{ name: "Tanzania, United Republic of", value: "TZ", disabled: false },
|
||||
{ name: "Thailand", value: "TH", disabled: false },
|
||||
{ name: "Timor-Leste", value: "TL", disabled: false },
|
||||
{ name: "Togo", value: "TG", disabled: false },
|
||||
{ name: "Tokelau", value: "TK", disabled: false },
|
||||
{ name: "Tonga", value: "TO", disabled: false },
|
||||
{ name: "Trinidad and Tobago", value: "TT", disabled: false },
|
||||
{ name: "Tunisia", value: "TN", disabled: false },
|
||||
{ name: "Turkey", value: "TR", disabled: false },
|
||||
{ name: "Turkmenistan", value: "TM", disabled: false },
|
||||
{ name: "Turks and Caicos Islands", value: "TC", disabled: false },
|
||||
{ name: "Tuvalu", value: "TV", disabled: false },
|
||||
{ name: "Uganda", value: "UG", disabled: false },
|
||||
{ name: "Ukraine", value: "UA", disabled: false },
|
||||
{ name: "United Arab Emirates", value: "AE", disabled: false },
|
||||
{ name: "United States Minor Outlying Islands", value: "UM", disabled: false },
|
||||
{ name: "Uruguay", value: "UY", disabled: false },
|
||||
{ name: "Uzbekistan", value: "UZ", disabled: false },
|
||||
{ name: "Vanuatu", value: "VU", disabled: false },
|
||||
{ name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false },
|
||||
{ name: "Viet Nam", value: "VN", disabled: false },
|
||||
{ name: "Virgin Islands, British", value: "VG", disabled: false },
|
||||
{ name: "Virgin Islands, U.S.", value: "VI", disabled: false },
|
||||
{ name: "Wallis and Futuna", value: "WF", disabled: false },
|
||||
{ name: "Western Sahara", value: "EH", disabled: false },
|
||||
{ name: "Yemen", value: "YE", disabled: false },
|
||||
{ name: "Zambia", value: "ZM", disabled: false },
|
||||
{ name: "Zimbabwe", value: "ZW", disabled: false },
|
||||
];
|
||||
}
|
||||
|
||||
async isCountrySupported(country: string): Promise<boolean> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/tax/is-country-supported?country=" + country,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async previewIndividualInvoice(
|
||||
request: PreviewIndividualInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/billing/preview-invoice",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PreviewInvoiceResponse(response);
|
||||
}
|
||||
|
||||
async previewOrganizationInvoice(
|
||||
request: PreviewOrganizationInvoiceRequest,
|
||||
): Promise<PreviewInvoiceResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/invoices/preview-organization`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PreviewInvoiceResponse(response);
|
||||
}
|
||||
|
||||
async previewTaxAmountForOrganizationTrial(
|
||||
request: PreviewTaxAmountForOrganizationTrialRequest,
|
||||
): Promise<number> {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/tax/preview-amount/organization-trial",
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return response as number;
|
||||
}
|
||||
}
|
||||
@@ -108,4 +108,11 @@ export enum EventType {
|
||||
Project_Created = 2201,
|
||||
Project_Edited = 2202,
|
||||
Project_Deleted = 2203,
|
||||
|
||||
ServiceAccount_UserAdded = 2300,
|
||||
ServiceAccount_UserRemoved = 2301,
|
||||
ServiceAccount_GroupAdded = 2302,
|
||||
ServiceAccount_GroupRemoved = 2303,
|
||||
ServiceAccount_Created = 2304,
|
||||
ServiceAccount_Deleted = 2305,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||
CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors",
|
||||
|
||||
/* Auth */
|
||||
PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods",
|
||||
@@ -24,7 +23,6 @@ export enum FeatureFlag {
|
||||
/* Billing */
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships",
|
||||
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
|
||||
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
|
||||
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
|
||||
|
||||
@@ -74,7 +72,6 @@ const FALSE = false as boolean;
|
||||
export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.CreateDefaultLocation]: FALSE,
|
||||
[FeatureFlag.CollectionVaultRefactor]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
@@ -102,7 +99,6 @@ export const DefaultFeatureFlagValue = {
|
||||
/* Billing */
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.PM17772_AdminInitiatedSponsorships]: FALSE,
|
||||
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
|
||||
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
|
||||
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE,
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export class EventResponse extends BaseResponse {
|
||||
secretId: string;
|
||||
projectId: string;
|
||||
serviceAccountId: string;
|
||||
grantedServiceAccountId: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -48,5 +49,6 @@ export class EventResponse extends BaseResponse {
|
||||
this.secretId = this.getResponseProperty("SecretId");
|
||||
this.projectId = this.getResponseProperty("ProjectId");
|
||||
this.serviceAccountId = this.getResponseProperty("ServiceAccountId");
|
||||
this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ const CanLaunchWhitelist = [
|
||||
];
|
||||
|
||||
export class SafeUrls {
|
||||
static canLaunch(uri: string): boolean {
|
||||
static canLaunch(uri: string | null | undefined): boolean {
|
||||
if (Utils.isNullOrWhitespace(uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < CanLaunchWhitelist.length; i++) {
|
||||
if (uri.indexOf(CanLaunchWhitelist[i]) === 0) {
|
||||
if (uri!.indexOf(CanLaunchWhitelist[i]) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("Utils Service", () => {
|
||||
expect(b64String).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("should return an empty string for an empty ArrayBuffer", () => {
|
||||
runInBothEnvironments("should return empty string for an empty ArrayBuffer", () => {
|
||||
const buffer = new Uint8Array([]).buffer;
|
||||
const b64String = Utils.fromBufferToB64(buffer);
|
||||
expect(b64String).toBe("");
|
||||
@@ -312,6 +312,81 @@ describe("Utils Service", () => {
|
||||
const b64String = Utils.fromBufferToB64(null);
|
||||
expect(b64String).toBeNull();
|
||||
});
|
||||
|
||||
runInBothEnvironments("returns null for undefined input", () => {
|
||||
const b64 = Utils.fromBufferToB64(undefined as unknown as ArrayBuffer);
|
||||
expect(b64).toBeNull();
|
||||
});
|
||||
|
||||
runInBothEnvironments("returns empty string for empty input", () => {
|
||||
const b64 = Utils.fromBufferToB64(new ArrayBuffer(0));
|
||||
expect(b64).toBe("");
|
||||
});
|
||||
|
||||
runInBothEnvironments("accepts Uint8Array directly", () => {
|
||||
const u8 = new Uint8Array(asciiHelloWorldArray);
|
||||
const b64 = Utils.fromBufferToB64(u8);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("respects byteOffset/byteLength (view window)", () => {
|
||||
// [xx, 'hello world', yy] — view should only encode the middle slice
|
||||
const prefix = [1, 2, 3];
|
||||
const suffix = [4, 5];
|
||||
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
|
||||
const view = new Uint8Array(all.buffer, prefix.length, asciiHelloWorldArray.length);
|
||||
const b64 = Utils.fromBufferToB64(view);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("handles DataView (ArrayBufferView other than Uint8Array)", () => {
|
||||
const u8 = new Uint8Array(asciiHelloWorldArray);
|
||||
const dv = new DataView(u8.buffer, 0, u8.byteLength);
|
||||
const b64 = Utils.fromBufferToB64(dv);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments("handles DataView with offset/length window", () => {
|
||||
// Buffer: [xx, 'hello world', yy]
|
||||
const prefix = [9, 9, 9];
|
||||
const suffix = [8, 8];
|
||||
const all = new Uint8Array([...prefix, ...asciiHelloWorldArray, ...suffix]);
|
||||
|
||||
// DataView over just the "hello world" window
|
||||
const dv = new DataView(all.buffer, prefix.length, asciiHelloWorldArray.length);
|
||||
|
||||
const b64 = Utils.fromBufferToB64(dv);
|
||||
expect(b64).toBe(b64HelloWorldString);
|
||||
});
|
||||
|
||||
runInBothEnvironments(
|
||||
"encodes empty view (offset-length window of zero) as empty string",
|
||||
() => {
|
||||
const backing = new Uint8Array([1, 2, 3, 4]);
|
||||
const emptyView = new Uint8Array(backing.buffer, 2, 0);
|
||||
const b64 = Utils.fromBufferToB64(emptyView);
|
||||
expect(b64).toBe("");
|
||||
},
|
||||
);
|
||||
|
||||
runInBothEnvironments("does not mutate the input", () => {
|
||||
const original = new Uint8Array(asciiHelloWorldArray);
|
||||
const copyBefore = new Uint8Array(original); // snapshot
|
||||
Utils.fromBufferToB64(original);
|
||||
expect(original).toEqual(copyBefore); // unchanged
|
||||
});
|
||||
|
||||
it("produces the same Base64 in Node vs non-Node mode", () => {
|
||||
const bytes = new Uint8Array(asciiHelloWorldArray);
|
||||
|
||||
Utils.isNode = true;
|
||||
const nodeB64 = Utils.fromBufferToB64(bytes);
|
||||
|
||||
Utils.isNode = false;
|
||||
const browserB64 = Utils.fromBufferToB64(bytes);
|
||||
|
||||
expect(browserB64).toBe(nodeB64);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromB64ToArray(...)", () => {
|
||||
|
||||
@@ -128,15 +128,52 @@ export class Utils {
|
||||
return arr;
|
||||
}
|
||||
|
||||
static fromBufferToB64(buffer: ArrayBuffer): string {
|
||||
/**
|
||||
* Convert binary data into a Base64 string.
|
||||
*
|
||||
* Overloads are provided for two categories of input:
|
||||
*
|
||||
* 1. ArrayBuffer
|
||||
* - A raw, fixed-length chunk of memory (no element semantics).
|
||||
* - Example: `const buf = new ArrayBuffer(16);`
|
||||
*
|
||||
* 2. ArrayBufferView
|
||||
* - A *view* onto an existing buffer that gives the bytes meaning.
|
||||
* - Examples: Uint8Array, Int32Array, DataView, etc.
|
||||
* - Views can expose only a *window* of the underlying buffer via
|
||||
* `byteOffset` and `byteLength`.
|
||||
* Example:
|
||||
* ```ts
|
||||
* const buf = new ArrayBuffer(8);
|
||||
* const full = new Uint8Array(buf); // sees all 8 bytes
|
||||
* const half = new Uint8Array(buf, 4, 4); // sees only last 4 bytes
|
||||
* ```
|
||||
*
|
||||
* Returns:
|
||||
* - Base64 string for non-empty inputs,
|
||||
* - null if `buffer` is `null` or `undefined`
|
||||
* - empty string if `buffer` is empty (0 bytes)
|
||||
*/
|
||||
static fromBufferToB64(buffer: null | undefined): null;
|
||||
static fromBufferToB64(buffer: ArrayBuffer): string;
|
||||
static fromBufferToB64(buffer: ArrayBufferView): string;
|
||||
static fromBufferToB64(buffer: ArrayBuffer | ArrayBufferView | null | undefined): string | null {
|
||||
// Handle null / undefined input
|
||||
if (buffer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes: Uint8Array = Utils.normalizeToUint8Array(buffer);
|
||||
|
||||
// Handle empty input
|
||||
if (bytes.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (Utils.isNode) {
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
return Buffer.from(bytes).toString("base64");
|
||||
} else {
|
||||
let binary = "";
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
@@ -144,6 +181,30 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes input into a Uint8Array so we always have a uniform,
|
||||
* byte-level view of the data. This avoids dealing with differences
|
||||
* between ArrayBuffer (raw memory with no indexing) and other typed
|
||||
* views (which may have element sizes, offsets, and lengths).
|
||||
* @param buffer ArrayBuffer or ArrayBufferView (e.g. Uint8Array, DataView, etc.)
|
||||
*/
|
||||
private static normalizeToUint8Array(buffer: ArrayBuffer | ArrayBufferView): Uint8Array {
|
||||
/**
|
||||
* 1) Uint8Array: already bytes → use directly.
|
||||
* 2) ArrayBuffer: wrap whole buffer.
|
||||
* 3) Other ArrayBufferView (e.g., DataView, Int32Array):
|
||||
* wrap the view’s window (byteOffset..byteOffset+byteLength).
|
||||
*/
|
||||
if (buffer instanceof Uint8Array) {
|
||||
return buffer;
|
||||
} else if (buffer instanceof ArrayBuffer) {
|
||||
return new Uint8Array(buffer);
|
||||
} else {
|
||||
const view = buffer as ArrayBufferView;
|
||||
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
||||
}
|
||||
}
|
||||
|
||||
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
|
||||
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
|
||||
}
|
||||
@@ -314,7 +375,7 @@ export class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
static getDomain(uriString: string): string {
|
||||
static getDomain(uriString: string | null | undefined): string {
|
||||
if (Utils.isNullOrWhitespace(uriString)) {
|
||||
return null;
|
||||
}
|
||||
@@ -392,11 +453,11 @@ export class Utils {
|
||||
};
|
||||
}
|
||||
|
||||
static isNullOrWhitespace(str: string): boolean {
|
||||
static isNullOrWhitespace(str: string | null | undefined): boolean {
|
||||
return str == null || typeof str !== "string" || str.trim() === "";
|
||||
}
|
||||
|
||||
static isNullOrEmpty(str: string | null): boolean {
|
||||
static isNullOrEmpty(str: string | null | undefined): boolean {
|
||||
return str == null || typeof str !== "string" || str == "";
|
||||
}
|
||||
|
||||
@@ -418,7 +479,7 @@ export class Utils {
|
||||
return (Object.keys(obj).filter((k) => Number.isNaN(+k)) as K[]).map((k) => obj[k]);
|
||||
}
|
||||
|
||||
static getUrl(uriString: string): URL {
|
||||
static getUrl(uriString: string | undefined | null): URL {
|
||||
if (this.isNullOrWhitespace(uriString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ export async function getCredentialsForAutofill(
|
||||
cipherId: cipher.id,
|
||||
credentialId: credId,
|
||||
rpId: credential.rpId,
|
||||
userHandle: credential.userHandle,
|
||||
userName: credential.userName,
|
||||
};
|
||||
userHandle: credential.userHandle!,
|
||||
userName: credential.userName!,
|
||||
} satisfies Fido2CredentialAutofillView;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,5 +292,100 @@ describe("DefaultSyncService", () => {
|
||||
expect(masterPasswordAbstraction.setMasterPasswordUnlockData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("mutate 'last update time'", () => {
|
||||
let mockUserState: { update: jest.Mock };
|
||||
|
||||
const setupMockUserState = () => {
|
||||
const mockUserState = { update: jest.fn() };
|
||||
jest.spyOn(stateProvider, "getUser").mockReturnValue(mockUserState as any);
|
||||
return mockUserState;
|
||||
};
|
||||
|
||||
const setupSyncScenario = (revisionDate: Date, lastSyncDate: Date) => {
|
||||
jest.spyOn(apiService, "getAccountRevisionDate").mockResolvedValue(revisionDate.getTime());
|
||||
jest.spyOn(sut as any, "getLastSync").mockResolvedValue(lastSyncDate);
|
||||
};
|
||||
|
||||
const expectUpdateCallCount = (
|
||||
mockUserState: { update: jest.Mock },
|
||||
expectedCount: number,
|
||||
) => {
|
||||
if (expectedCount === 0) {
|
||||
expect(mockUserState.update).not.toHaveBeenCalled();
|
||||
} else {
|
||||
expect(mockUserState.update).toHaveBeenCalledTimes(expectedCount);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultSyncOptions = { allowThrowOnError: true, skipTokenRefresh: true };
|
||||
const errorTolerantSyncOptions = { allowThrowOnError: false, skipTokenRefresh: true };
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserState = setupMockUserState();
|
||||
});
|
||||
|
||||
it("uses the current time when a sync is forced", async () => {
|
||||
// Mock the value of this observable because it's used in `syncProfile`. Without it, the test breaks.
|
||||
keyConnectorService.convertAccountRequired$ = of(false);
|
||||
|
||||
// Baseline date/time to compare sync time to, in order to avoid needing to use some kind of fake date provider.
|
||||
const beforeSync = Date.now();
|
||||
|
||||
// send it!
|
||||
await sut.fullSync(true, defaultSyncOptions);
|
||||
|
||||
expectUpdateCallCount(mockUserState, 1);
|
||||
// Get the first and only call to update(...)
|
||||
const updateCall = mockUserState.update.mock.calls[0];
|
||||
// Get the first argument to update(...) -- this will be the date callback that returns the date of the last successful sync
|
||||
const dateCallback = updateCall[0];
|
||||
const actualTime = dateCallback() as Date;
|
||||
|
||||
expect(Math.abs(actualTime.getTime() - beforeSync)).toBeLessThan(1);
|
||||
});
|
||||
|
||||
it("updates last sync time when no sync is necessary", async () => {
|
||||
const revisionDate = new Date(1);
|
||||
setupSyncScenario(revisionDate, revisionDate);
|
||||
|
||||
const syncResult = await sut.fullSync(false, defaultSyncOptions);
|
||||
|
||||
// Sync should complete but return false since no sync was needed
|
||||
expect(syncResult).toBe(false);
|
||||
expectUpdateCallCount(mockUserState, 1);
|
||||
});
|
||||
|
||||
it("updates last sync time when sync is successful", async () => {
|
||||
setupSyncScenario(new Date(2), new Date(1));
|
||||
|
||||
const syncResult = await sut.fullSync(false, defaultSyncOptions);
|
||||
|
||||
expect(syncResult).toBe(true);
|
||||
expectUpdateCallCount(mockUserState, 1);
|
||||
});
|
||||
|
||||
describe("error scenarios", () => {
|
||||
it("does not update last sync time when sync fails", async () => {
|
||||
apiService.getSync.mockRejectedValue(new Error("not connected"));
|
||||
|
||||
const syncResult = await sut.fullSync(true, errorTolerantSyncOptions);
|
||||
|
||||
expect(syncResult).toBe(false);
|
||||
expectUpdateCallCount(mockUserState, 0);
|
||||
});
|
||||
|
||||
it("does not update last sync time when account revision check fails", async () => {
|
||||
jest
|
||||
.spyOn(apiService, "getAccountRevisionDate")
|
||||
.mockRejectedValue(new Error("not connected"));
|
||||
|
||||
const syncResult = await sut.fullSync(false, errorTolerantSyncOptions);
|
||||
|
||||
expect(syncResult).toBe(false);
|
||||
expectUpdateCallCount(mockUserState, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,9 +134,11 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
|
||||
const now = new Date();
|
||||
let needsSync = false;
|
||||
let needsSyncSucceeded = true;
|
||||
try {
|
||||
needsSync = await this.needsSyncing(forceSync);
|
||||
} catch (e) {
|
||||
needsSyncSucceeded = false;
|
||||
if (allowThrowOnError) {
|
||||
this.syncCompleted(false, userId);
|
||||
throw e;
|
||||
@@ -144,7 +146,9 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
}
|
||||
|
||||
if (!needsSync) {
|
||||
await this.setLastSync(now, userId);
|
||||
if (needsSyncSucceeded) {
|
||||
await this.setLastSync(now, userId);
|
||||
}
|
||||
return this.syncCompleted(false, userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,14 +90,10 @@ import {
|
||||
} from "../auth/models/response/two-factor-web-authn.response";
|
||||
import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response";
|
||||
import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request";
|
||||
import { PaymentRequest } from "../billing/models/request/payment.request";
|
||||
import { TaxInfoUpdateRequest } from "../billing/models/request/tax-info-update.request";
|
||||
import { BillingHistoryResponse } from "../billing/models/response/billing-history.response";
|
||||
import { BillingPaymentResponse } from "../billing/models/response/billing-payment.response";
|
||||
import { PaymentResponse } from "../billing/models/response/payment.response";
|
||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { ClientType, DeviceType } from "../enums";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
@@ -294,11 +290,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new SubscriptionResponse(r);
|
||||
}
|
||||
|
||||
async getTaxInfo(): Promise<TaxInfoResponse> {
|
||||
const r = await this.send("GET", "/accounts/tax", null, true, true);
|
||||
return new TaxInfoResponse(r);
|
||||
}
|
||||
|
||||
async putProfile(request: UpdateProfileRequest): Promise<ProfileResponse> {
|
||||
const r = await this.send("PUT", "/accounts/profile", request, true, true);
|
||||
return new ProfileResponse(r);
|
||||
@@ -309,10 +300,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new ProfileResponse(r);
|
||||
}
|
||||
|
||||
putTaxInfo(request: TaxInfoUpdateRequest): Promise<any> {
|
||||
return this.send("PUT", "/accounts/tax", request, true, false);
|
||||
}
|
||||
|
||||
async postPrelogin(request: PreloginRequest): Promise<PreloginResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.send(
|
||||
@@ -365,10 +352,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new PaymentResponse(r);
|
||||
}
|
||||
|
||||
postAccountPayment(request: PaymentRequest): Promise<void> {
|
||||
return this.send("POST", "/accounts/payment", request, true, false);
|
||||
}
|
||||
|
||||
postAccountLicense(data: FormData): Promise<any> {
|
||||
return this.send("POST", "/accounts/license", data, true, false);
|
||||
}
|
||||
@@ -429,11 +412,6 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new BillingHistoryResponse(r);
|
||||
}
|
||||
|
||||
async getUserBillingPayment(): Promise<BillingPaymentResponse> {
|
||||
const r = await this.send("GET", "/accounts/billing/payment-method", null, true, true);
|
||||
return new BillingPaymentResponse(r);
|
||||
}
|
||||
|
||||
// Cipher APIs
|
||||
|
||||
async getCipher(id: string): Promise<CipherResponse> {
|
||||
@@ -1294,6 +1272,28 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new ListResponse(r, EventResponse);
|
||||
}
|
||||
|
||||
async getEventsServiceAccount(
|
||||
orgId: string,
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
token: string,
|
||||
): Promise<ListResponse<EventResponse>> {
|
||||
const r = await this.send(
|
||||
"GET",
|
||||
this.addEventParameters(
|
||||
"/organization/" + orgId + "/service-account/" + id + "/events",
|
||||
start,
|
||||
end,
|
||||
token,
|
||||
),
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ListResponse(r, EventResponse);
|
||||
}
|
||||
|
||||
async getEventsProject(
|
||||
orgId: string,
|
||||
id: string,
|
||||
|
||||
12
libs/common/src/vault/abstractions/cipher-archive.service.ts
Normal file
12
libs/common/src/vault/abstractions/cipher-archive.service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
export abstract class CipherArchiveService {
|
||||
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
||||
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
||||
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
||||
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export abstract class SearchService {
|
||||
ciphers: C[],
|
||||
query: string,
|
||||
deleted?: boolean,
|
||||
archived?: boolean,
|
||||
): C[];
|
||||
abstract searchSends(sends: SendView[], query: string): SendView[];
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export class Attachment extends Domain {
|
||||
): Promise<AttachmentView> {
|
||||
const view = await this.decryptObj<Attachment, AttachmentView>(
|
||||
this,
|
||||
// @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now.
|
||||
new AttachmentView(this),
|
||||
["fileName"],
|
||||
orgId,
|
||||
|
||||
@@ -63,7 +63,6 @@ describe("Card", () => {
|
||||
expect(view).toEqual({
|
||||
_brand: "brand",
|
||||
_number: "number",
|
||||
_subTitle: null,
|
||||
cardholderName: "cardHolder",
|
||||
code: "code",
|
||||
expMonth: "expMonth",
|
||||
|
||||
@@ -161,6 +161,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
|
||||
await this.decryptObj<Cipher, CipherView>(
|
||||
this,
|
||||
// @ts-expect-error Ciphers have optional Ids which are getting swallowed by the ViewEncryptableKeys type
|
||||
// The ViewEncryptableKeys type should be fixed to allow for optional Ids, but is out of scope for now.
|
||||
model,
|
||||
["name", "notes"],
|
||||
this.organizationId,
|
||||
@@ -349,7 +351,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
*/
|
||||
toSdkCipher(): SdkCipher {
|
||||
const sdkCipher: SdkCipher = {
|
||||
id: asUuid(this.id),
|
||||
id: this.id ? asUuid(this.id) : undefined,
|
||||
organizationId: this.organizationId ? asUuid(this.organizationId) : undefined,
|
||||
folderId: this.folderId ? asUuid(this.folderId) : undefined,
|
||||
collectionIds: this.collectionIds ? this.collectionIds.map(asUuid) : ([] as any),
|
||||
|
||||
@@ -56,6 +56,7 @@ export class Fido2Credential extends Domain {
|
||||
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<Fido2CredentialView> {
|
||||
const view = await this.decryptObj<Fido2Credential, Fido2CredentialView>(
|
||||
this,
|
||||
// @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now.
|
||||
new Fido2CredentialView(),
|
||||
[
|
||||
"credentialId",
|
||||
|
||||
@@ -39,6 +39,7 @@ export class Field extends Domain {
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<FieldView> {
|
||||
return this.decryptObj<Field, FieldView>(
|
||||
this,
|
||||
// @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now.
|
||||
new FieldView(this),
|
||||
["name", "value"],
|
||||
orgId,
|
||||
|
||||
@@ -112,7 +112,6 @@ describe("Identity", () => {
|
||||
expect(view).toEqual({
|
||||
_firstName: "mockFirstName",
|
||||
_lastName: "mockLastName",
|
||||
_subTitle: null,
|
||||
address1: "mockAddress1",
|
||||
address2: "mockAddress2",
|
||||
address3: "mockAddress3",
|
||||
|
||||
@@ -56,10 +56,6 @@ describe("LoginUri", () => {
|
||||
const view = await loginUri.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_canLaunch: null,
|
||||
_domain: null,
|
||||
_host: null,
|
||||
_hostname: null,
|
||||
_uri: "uri",
|
||||
match: 3,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
|
||||
import { mockEnc, mockFromJson } from "../../../../spec";
|
||||
import { EncryptedString, EncString } from "../../../key-management/crypto/models/enc-string";
|
||||
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||
import { UriMatchStrategy } from "../../../models/domain/domain-service";
|
||||
import { LoginData } from "../../models/data/login.data";
|
||||
import { Login } from "../../models/domain/login";
|
||||
import { LoginUri } from "../../models/domain/login-uri";
|
||||
@@ -82,12 +82,7 @@ describe("Login DTO", () => {
|
||||
totp: "encrypted totp",
|
||||
uris: [
|
||||
{
|
||||
match: null as UriMatchStrategySetting,
|
||||
_uri: "decrypted uri",
|
||||
_domain: null as string,
|
||||
_hostname: null as string,
|
||||
_host: null as string,
|
||||
_canLaunch: null as boolean,
|
||||
},
|
||||
],
|
||||
autofillOnPageLoad: true,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AttachmentView as SdkAttachmentView } from "@bitwarden/sdk-internal";
|
||||
@@ -10,12 +8,12 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
|
||||
import { Attachment } from "../domain/attachment";
|
||||
|
||||
export class AttachmentView implements View {
|
||||
id: string = null;
|
||||
url: string = null;
|
||||
size: string = null;
|
||||
sizeName: string = null;
|
||||
fileName: string = null;
|
||||
key: SymmetricCryptoKey = null;
|
||||
id?: string;
|
||||
url?: string;
|
||||
size?: string;
|
||||
sizeName?: string;
|
||||
fileName?: string;
|
||||
key?: SymmetricCryptoKey;
|
||||
/**
|
||||
* The SDK returns an encrypted key for the attachment.
|
||||
*/
|
||||
@@ -35,7 +33,7 @@ export class AttachmentView implements View {
|
||||
get fileSize(): number {
|
||||
try {
|
||||
if (this.size != null) {
|
||||
return parseInt(this.size, null);
|
||||
return parseInt(this.size);
|
||||
}
|
||||
} catch {
|
||||
// Invalid file size.
|
||||
@@ -71,7 +69,7 @@ export class AttachmentView implements View {
|
||||
fileName: this.fileName,
|
||||
key: this.encryptedKey?.toSdk(),
|
||||
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
|
||||
decryptedKey: this.key ? this.key.toBase64() : null,
|
||||
decryptedKey: this.key ? this.key.toBase64() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,13 +82,13 @@ export class AttachmentView implements View {
|
||||
}
|
||||
|
||||
const view = new AttachmentView();
|
||||
view.id = obj.id ?? null;
|
||||
view.url = obj.url ?? null;
|
||||
view.size = obj.size ?? null;
|
||||
view.sizeName = obj.sizeName ?? null;
|
||||
view.fileName = obj.fileName ?? null;
|
||||
view.id = obj.id;
|
||||
view.url = obj.url;
|
||||
view.size = obj.size;
|
||||
view.sizeName = obj.sizeName;
|
||||
view.fileName = obj.fileName;
|
||||
// TODO: PM-23005 - Temporary field, should be removed when encrypted migration is complete
|
||||
view.key = obj.decryptedKey ? SymmetricCryptoKey.fromString(obj.decryptedKey) : null;
|
||||
view.key = obj.decryptedKey ? SymmetricCryptoKey.fromString(obj.decryptedKey) : undefined;
|
||||
view.encryptedKey = obj.key ? new EncString(obj.key) : undefined;
|
||||
|
||||
return view;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CardView as SdkCardView } from "@bitwarden/sdk-internal";
|
||||
@@ -12,45 +10,45 @@ import { ItemView } from "./item.view";
|
||||
|
||||
export class CardView extends ItemView implements SdkCardView {
|
||||
@linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 })
|
||||
cardholderName: string = null;
|
||||
cardholderName: string | undefined;
|
||||
@linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" })
|
||||
expMonth: string = null;
|
||||
expMonth: string | undefined;
|
||||
@linkedFieldOption(LinkedId.ExpYear, { sortPosition: 4, i18nKey: "expirationYear" })
|
||||
expYear: string = null;
|
||||
expYear: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Code, { sortPosition: 5, i18nKey: "securityCode" })
|
||||
code: string = null;
|
||||
code: string | undefined;
|
||||
|
||||
private _brand: string = null;
|
||||
private _number: string = null;
|
||||
private _subTitle: string = null;
|
||||
private _brand?: string;
|
||||
private _number?: string;
|
||||
private _subTitle?: string;
|
||||
|
||||
get maskedCode(): string {
|
||||
return this.code != null ? "•".repeat(this.code.length) : null;
|
||||
get maskedCode(): string | undefined {
|
||||
return this.code != null ? "•".repeat(this.code.length) : undefined;
|
||||
}
|
||||
|
||||
get maskedNumber(): string {
|
||||
return this.number != null ? "•".repeat(this.number.length) : null;
|
||||
get maskedNumber(): string | undefined {
|
||||
return this.number != null ? "•".repeat(this.number.length) : undefined;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.Brand, { sortPosition: 2 })
|
||||
get brand(): string {
|
||||
get brand(): string | undefined {
|
||||
return this._brand;
|
||||
}
|
||||
set brand(value: string) {
|
||||
set brand(value: string | undefined) {
|
||||
this._brand = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.Number, { sortPosition: 1 })
|
||||
get number(): string {
|
||||
get number(): string | undefined {
|
||||
return this._number;
|
||||
}
|
||||
set number(value: string) {
|
||||
set number(value: string | undefined) {
|
||||
this._number = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
if (this._subTitle == null) {
|
||||
this._subTitle = this.brand;
|
||||
if (this.number != null && this.number.length >= 4) {
|
||||
@@ -69,11 +67,11 @@ export class CardView extends ItemView implements SdkCardView {
|
||||
return this._subTitle;
|
||||
}
|
||||
|
||||
get expiration(): string {
|
||||
const normalizedYear = normalizeExpiryYearFormat(this.expYear);
|
||||
get expiration(): string | undefined {
|
||||
const normalizedYear = this.expYear ? normalizeExpiryYearFormat(this.expYear) : undefined;
|
||||
|
||||
if (!this.expMonth && !normalizedYear) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__";
|
||||
@@ -82,14 +80,14 @@ export class CardView extends ItemView implements SdkCardView {
|
||||
return exp;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {
|
||||
static fromJSON(obj: Partial<Jsonify<CardView>> | undefined): CardView {
|
||||
return Object.assign(new CardView(), obj);
|
||||
}
|
||||
|
||||
// ref https://stackoverflow.com/a/5911300
|
||||
static getCardBrandByPatterns(cardNum: string): string {
|
||||
static getCardBrandByPatterns(cardNum: string | undefined | null): string | undefined {
|
||||
if (cardNum == null || typeof cardNum !== "string" || cardNum.trim() === "") {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Visa
|
||||
@@ -146,25 +144,21 @@ export class CardView extends ItemView implements SdkCardView {
|
||||
return "Visa";
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an SDK CardView to a CardView.
|
||||
*/
|
||||
static fromSdkCardView(obj: SdkCardView): CardView | undefined {
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkCardView(obj: SdkCardView): CardView {
|
||||
const cardView = new CardView();
|
||||
|
||||
cardView.cardholderName = obj.cardholderName ?? null;
|
||||
cardView.brand = obj.brand ?? null;
|
||||
cardView.number = obj.number ?? null;
|
||||
cardView.expMonth = obj.expMonth ?? null;
|
||||
cardView.expYear = obj.expYear ?? null;
|
||||
cardView.code = obj.code ?? null;
|
||||
cardView.cardholderName = obj.cardholderName;
|
||||
cardView.brand = obj.brand;
|
||||
cardView.number = obj.number;
|
||||
cardView.expMonth = obj.expMonth;
|
||||
cardView.expYear = obj.expYear;
|
||||
cardView.code = obj.code;
|
||||
|
||||
return cardView;
|
||||
}
|
||||
|
||||
@@ -180,15 +180,12 @@ describe("CipherView", () => {
|
||||
folderId: "folderId",
|
||||
collectionIds: ["collectionId"],
|
||||
name: "name",
|
||||
notes: null,
|
||||
type: CipherType.Login,
|
||||
favorite: true,
|
||||
edit: true,
|
||||
reprompt: CipherRepromptType.None,
|
||||
organizationUseTotp: false,
|
||||
viewPassword: true,
|
||||
localData: undefined,
|
||||
permissions: undefined,
|
||||
attachments: [
|
||||
{
|
||||
id: "attachmentId",
|
||||
@@ -224,7 +221,6 @@ describe("CipherView", () => {
|
||||
passwordHistory: [],
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
revisionDate: new Date("2022-01-02T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -283,18 +279,12 @@ describe("CipherView", () => {
|
||||
restore: true,
|
||||
delete: true,
|
||||
},
|
||||
deletedDate: undefined,
|
||||
creationDate: "2022-01-02T12:00:00.000Z",
|
||||
revisionDate: "2022-01-02T12:00:00.000Z",
|
||||
attachments: [],
|
||||
passwordHistory: [],
|
||||
login: undefined,
|
||||
identity: undefined,
|
||||
card: undefined,
|
||||
secureNote: undefined,
|
||||
sshKey: undefined,
|
||||
fields: [],
|
||||
} as SdkCipherView);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { asUuid, uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { ItemView } from "@bitwarden/common/vault/models/view/item.view";
|
||||
import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { View } from "../../../models/view/view";
|
||||
@@ -26,18 +25,18 @@ import { SshKeyView } from "./ssh-key.view";
|
||||
export class CipherView implements View, InitializerMetadata {
|
||||
readonly initializerKey = InitializerKey.CipherView;
|
||||
|
||||
id: string = null;
|
||||
organizationId: string | undefined = null;
|
||||
folderId: string = null;
|
||||
name: string = null;
|
||||
notes: string = null;
|
||||
type: CipherType = null;
|
||||
id: string = "";
|
||||
organizationId?: string;
|
||||
folderId?: string;
|
||||
name: string = "";
|
||||
notes?: string;
|
||||
type: CipherType = CipherType.Login;
|
||||
favorite = false;
|
||||
organizationUseTotp = false;
|
||||
permissions: CipherPermissionsApi = new CipherPermissionsApi();
|
||||
permissions?: CipherPermissionsApi = new CipherPermissionsApi();
|
||||
edit = false;
|
||||
viewPassword = true;
|
||||
localData: LocalData;
|
||||
localData?: LocalData;
|
||||
login = new LoginView();
|
||||
identity = new IdentityView();
|
||||
card = new CardView();
|
||||
@@ -46,11 +45,11 @@ export class CipherView implements View, InitializerMetadata {
|
||||
attachments: AttachmentView[] = [];
|
||||
fields: FieldView[] = [];
|
||||
passwordHistory: PasswordHistoryView[] = [];
|
||||
collectionIds: string[] = null;
|
||||
revisionDate: Date = null;
|
||||
creationDate: Date = null;
|
||||
deletedDate: Date | null = null;
|
||||
archivedDate: Date | null = null;
|
||||
collectionIds: string[] = [];
|
||||
revisionDate: Date;
|
||||
creationDate: Date;
|
||||
deletedDate?: Date;
|
||||
archivedDate?: Date;
|
||||
reprompt: CipherRepromptType = CipherRepromptType.None;
|
||||
// We need a copy of the encrypted key so we can pass it to
|
||||
// the SdkCipherView during encryption
|
||||
@@ -63,6 +62,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
|
||||
constructor(c?: Cipher) {
|
||||
if (!c) {
|
||||
this.creationDate = this.revisionDate = new Date();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
this.key = c.key;
|
||||
}
|
||||
|
||||
private get item() {
|
||||
private get item(): ItemView | undefined {
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
return this.login;
|
||||
@@ -102,10 +102,10 @@ export class CipherView implements View, InitializerMetadata {
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
return this.item?.subTitle;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export class CipherView implements View, InitializerMetadata {
|
||||
}
|
||||
|
||||
get hasAttachments(): boolean {
|
||||
return this.attachments && this.attachments.length > 0;
|
||||
return !!this.attachments && this.attachments.length > 0;
|
||||
}
|
||||
|
||||
get hasOldAttachments(): boolean {
|
||||
@@ -132,11 +132,11 @@ export class CipherView implements View, InitializerMetadata {
|
||||
return this.fields && this.fields.length > 0;
|
||||
}
|
||||
|
||||
get passwordRevisionDisplayDate(): Date {
|
||||
get passwordRevisionDisplayDate(): Date | undefined {
|
||||
if (this.type !== CipherType.Login || this.login == null) {
|
||||
return null;
|
||||
return undefined;
|
||||
} else if (this.login.password == null || this.login.password === "") {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
return this.login.passwordRevisionDate;
|
||||
}
|
||||
@@ -170,23 +170,17 @@ export class CipherView implements View, InitializerMetadata {
|
||||
* Determines if the cipher can be launched in a new browser tab.
|
||||
*/
|
||||
get canLaunch(): boolean {
|
||||
return this.type === CipherType.Login && this.login.canLaunch;
|
||||
return this.type === CipherType.Login && this.login!.canLaunch;
|
||||
}
|
||||
|
||||
linkedFieldValue(id: LinkedIdType) {
|
||||
const linkedFieldOption = this.linkedFieldOptions?.get(id);
|
||||
if (linkedFieldOption == null) {
|
||||
return null;
|
||||
const item = this.item;
|
||||
if (linkedFieldOption == null || item == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const item = this.item;
|
||||
return this.item[linkedFieldOption.propertyKey as keyof typeof item];
|
||||
}
|
||||
|
||||
linkedFieldI18nKey(id: LinkedIdType): string {
|
||||
return this.linkedFieldOptions.get(id)?.i18nKey;
|
||||
return item[linkedFieldOption.propertyKey as keyof typeof item];
|
||||
}
|
||||
|
||||
// This is used as a marker to indicate that the cipher view object still has its prototype
|
||||
@@ -194,23 +188,31 @@ export class CipherView implements View, InitializerMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<DeepJsonify<CipherView>>): CipherView {
|
||||
static fromJSON(obj: Partial<DeepJsonify<CipherView>>): CipherView | null {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = new CipherView();
|
||||
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||
const archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
|
||||
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
||||
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
||||
const permissions = CipherPermissionsApi.fromJSON(obj.permissions);
|
||||
let key: EncString | undefined;
|
||||
view.type = obj.type ?? CipherType.Login;
|
||||
view.id = obj.id ?? "";
|
||||
view.name = obj.name ?? "";
|
||||
if (obj.creationDate) {
|
||||
view.creationDate = new Date(obj.creationDate);
|
||||
}
|
||||
if (obj.revisionDate) {
|
||||
view.revisionDate = new Date(obj.revisionDate);
|
||||
}
|
||||
view.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
|
||||
view.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
|
||||
view.attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a)) ?? [];
|
||||
view.fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)) ?? [];
|
||||
view.passwordHistory =
|
||||
obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)) ?? [];
|
||||
view.permissions = obj.permissions ? CipherPermissionsApi.fromJSON(obj.permissions) : undefined;
|
||||
|
||||
if (obj.key != null) {
|
||||
let key: EncString | undefined;
|
||||
if (typeof obj.key === "string") {
|
||||
// If the key is a string, we need to parse it as EncString
|
||||
key = EncString.fromJSON(obj.key);
|
||||
@@ -218,20 +220,9 @@ export class CipherView implements View, InitializerMetadata {
|
||||
// If the key is already an EncString instance, we can use it directly
|
||||
key = obj.key;
|
||||
}
|
||||
view.key = key;
|
||||
}
|
||||
|
||||
Object.assign(view, obj, {
|
||||
creationDate: creationDate,
|
||||
revisionDate: revisionDate,
|
||||
deletedDate: deletedDate,
|
||||
archivedDate: archivedDate,
|
||||
attachments: attachments,
|
||||
fields: fields,
|
||||
passwordHistory: passwordHistory,
|
||||
permissions: permissions,
|
||||
key: key,
|
||||
});
|
||||
|
||||
switch (obj.type) {
|
||||
case CipherType.Card:
|
||||
view.card = CardView.fromJSON(obj.card);
|
||||
@@ -264,46 +255,54 @@ export class CipherView implements View, InitializerMetadata {
|
||||
}
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = uuidAsString(obj.id) ?? null;
|
||||
cipherView.organizationId = uuidAsString(obj.organizationId) ?? null;
|
||||
cipherView.folderId = uuidAsString(obj.folderId) ?? null;
|
||||
cipherView.id = uuidAsString(obj.id);
|
||||
cipherView.organizationId = uuidAsString(obj.organizationId);
|
||||
cipherView.folderId = uuidAsString(obj.folderId);
|
||||
cipherView.name = obj.name;
|
||||
cipherView.notes = obj.notes ?? null;
|
||||
cipherView.notes = obj.notes;
|
||||
cipherView.type = obj.type;
|
||||
cipherView.favorite = obj.favorite;
|
||||
cipherView.organizationUseTotp = obj.organizationUseTotp;
|
||||
cipherView.permissions = CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions);
|
||||
cipherView.permissions = obj.permissions
|
||||
? CipherPermissionsApi.fromSdkCipherPermissions(obj.permissions)
|
||||
: undefined;
|
||||
cipherView.edit = obj.edit;
|
||||
cipherView.viewPassword = obj.viewPassword;
|
||||
cipherView.localData = fromSdkLocalData(obj.localData);
|
||||
cipherView.attachments =
|
||||
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)) ?? [];
|
||||
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? [];
|
||||
obj.attachments?.map((a) => AttachmentView.fromSdkAttachmentView(a)!) ?? [];
|
||||
cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)!) ?? [];
|
||||
cipherView.passwordHistory =
|
||||
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? [];
|
||||
obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)!) ?? [];
|
||||
cipherView.collectionIds = obj.collectionIds?.map((i) => uuidAsString(i)) ?? [];
|
||||
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||
cipherView.archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
|
||||
cipherView.revisionDate = new Date(obj.revisionDate);
|
||||
cipherView.creationDate = new Date(obj.creationDate);
|
||||
cipherView.deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
|
||||
cipherView.archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
|
||||
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
||||
cipherView.key = EncString.fromJSON(obj.key);
|
||||
cipherView.key = obj.key ? EncString.fromJSON(obj.key) : undefined;
|
||||
|
||||
switch (obj.type) {
|
||||
case CipherType.Card:
|
||||
cipherView.card = CardView.fromSdkCardView(obj.card);
|
||||
cipherView.card = obj.card ? CardView.fromSdkCardView(obj.card) : new CardView();
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
cipherView.identity = IdentityView.fromSdkIdentityView(obj.identity);
|
||||
cipherView.identity = obj.identity
|
||||
? IdentityView.fromSdkIdentityView(obj.identity)
|
||||
: new IdentityView();
|
||||
break;
|
||||
case CipherType.Login:
|
||||
cipherView.login = LoginView.fromSdkLoginView(obj.login);
|
||||
cipherView.login = obj.login ? LoginView.fromSdkLoginView(obj.login) : new LoginView();
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
cipherView.secureNote = SecureNoteView.fromSdkSecureNoteView(obj.secureNote);
|
||||
cipherView.secureNote = obj.secureNote
|
||||
? SecureNoteView.fromSdkSecureNoteView(obj.secureNote)
|
||||
: new SecureNoteView();
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
cipherView.sshKey = SshKeyView.fromSdkSshKeyView(obj.sshKey);
|
||||
cipherView.sshKey = obj.sshKey
|
||||
? SshKeyView.fromSdkSshKeyView(obj.sshKey)
|
||||
: new SshKeyView();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -325,11 +324,11 @@ export class CipherView implements View, InitializerMetadata {
|
||||
name: this.name ?? "",
|
||||
notes: this.notes,
|
||||
type: this.type ?? CipherType.Login,
|
||||
favorite: this.favorite,
|
||||
organizationUseTotp: this.organizationUseTotp,
|
||||
favorite: this.favorite ?? false,
|
||||
organizationUseTotp: this.organizationUseTotp ?? false,
|
||||
permissions: this.permissions?.toSdkCipherPermissions(),
|
||||
edit: this.edit,
|
||||
viewPassword: this.viewPassword,
|
||||
edit: this.edit ?? true,
|
||||
viewPassword: this.viewPassword ?? true,
|
||||
localData: toSdkLocalData(this.localData),
|
||||
attachments: this.attachments?.map((a) => a.toSdkAttachmentView()),
|
||||
fields: this.fields?.map((f) => f.toSdkFieldView()),
|
||||
@@ -354,19 +353,19 @@ export class CipherView implements View, InitializerMetadata {
|
||||
|
||||
switch (this.type) {
|
||||
case CipherType.Card:
|
||||
sdkCipherView.card = this.card.toSdkCardView();
|
||||
sdkCipherView.card = this.card?.toSdkCardView();
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
sdkCipherView.identity = this.identity.toSdkIdentityView();
|
||||
sdkCipherView.identity = this.identity?.toSdkIdentityView();
|
||||
break;
|
||||
case CipherType.Login:
|
||||
sdkCipherView.login = this.login.toSdkLoginView();
|
||||
sdkCipherView.login = this.login?.toSdkLoginView();
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
sdkCipherView.secureNote = this.secureNote.toSdkSecureNoteView();
|
||||
sdkCipherView.secureNote = this.secureNote?.toSdkSecureNoteView();
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
sdkCipherView.sshKey = this.sshKey.toSdkSshKeyView();
|
||||
sdkCipherView.sshKey = this.sshKey?.toSdkSshKeyView();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import {
|
||||
@@ -10,21 +8,55 @@ import {
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class Fido2CredentialView extends ItemView {
|
||||
credentialId: string;
|
||||
keyType: "public-key";
|
||||
keyAlgorithm: "ECDSA";
|
||||
keyCurve: "P-256";
|
||||
keyValue: string;
|
||||
rpId: string;
|
||||
userHandle: string;
|
||||
userName: string;
|
||||
counter: number;
|
||||
rpName: string;
|
||||
userDisplayName: string;
|
||||
discoverable: boolean;
|
||||
creationDate: Date = null;
|
||||
credentialId!: string;
|
||||
keyType!: "public-key";
|
||||
keyAlgorithm!: "ECDSA";
|
||||
keyCurve!: "P-256";
|
||||
keyValue!: string;
|
||||
rpId!: string;
|
||||
userHandle?: string;
|
||||
userName?: string;
|
||||
counter!: number;
|
||||
rpName?: string;
|
||||
userDisplayName?: string;
|
||||
discoverable: boolean = false;
|
||||
creationDate!: Date;
|
||||
|
||||
get subTitle(): string {
|
||||
constructor(f?: {
|
||||
credentialId: string;
|
||||
keyType: "public-key";
|
||||
keyAlgorithm: "ECDSA";
|
||||
keyCurve: "P-256";
|
||||
keyValue: string;
|
||||
rpId: string;
|
||||
userHandle?: string;
|
||||
userName?: string;
|
||||
counter: number;
|
||||
rpName?: string;
|
||||
userDisplayName?: string;
|
||||
discoverable?: boolean;
|
||||
creationDate: Date;
|
||||
}) {
|
||||
super();
|
||||
if (f == null) {
|
||||
return;
|
||||
}
|
||||
this.credentialId = f.credentialId;
|
||||
this.keyType = f.keyType;
|
||||
this.keyAlgorithm = f.keyAlgorithm;
|
||||
this.keyCurve = f.keyCurve;
|
||||
this.keyValue = f.keyValue;
|
||||
this.rpId = f.rpId;
|
||||
this.userHandle = f.userHandle;
|
||||
this.userName = f.userName;
|
||||
this.counter = f.counter;
|
||||
this.rpName = f.rpName;
|
||||
this.userDisplayName = f.userDisplayName;
|
||||
this.discoverable = f.discoverable ?? false;
|
||||
this.creationDate = f.creationDate;
|
||||
}
|
||||
|
||||
get subTitle(): string | undefined {
|
||||
return this.userDisplayName;
|
||||
}
|
||||
|
||||
@@ -43,21 +75,21 @@ export class Fido2CredentialView extends ItemView {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const view = new Fido2CredentialView();
|
||||
view.credentialId = obj.credentialId;
|
||||
view.keyType = obj.keyType as "public-key";
|
||||
view.keyAlgorithm = obj.keyAlgorithm as "ECDSA";
|
||||
view.keyCurve = obj.keyCurve as "P-256";
|
||||
view.rpId = obj.rpId;
|
||||
view.userHandle = obj.userHandle;
|
||||
view.userName = obj.userName;
|
||||
view.counter = parseInt(obj.counter);
|
||||
view.rpName = obj.rpName;
|
||||
view.userDisplayName = obj.userDisplayName;
|
||||
view.discoverable = obj.discoverable?.toLowerCase() === "true" ? true : false;
|
||||
view.creationDate = obj.creationDate ? new Date(obj.creationDate) : null;
|
||||
|
||||
return view;
|
||||
return new Fido2CredentialView({
|
||||
credentialId: obj.credentialId,
|
||||
keyType: obj.keyType as "public-key",
|
||||
keyAlgorithm: obj.keyAlgorithm as "ECDSA",
|
||||
keyCurve: obj.keyCurve as "P-256",
|
||||
keyValue: obj.keyValue,
|
||||
rpId: obj.rpId,
|
||||
userHandle: obj.userHandle,
|
||||
userName: obj.userName,
|
||||
counter: parseInt(obj.counter),
|
||||
rpName: obj.rpName,
|
||||
userDisplayName: obj.userDisplayName,
|
||||
discoverable: obj.discoverable?.toLowerCase() === "true",
|
||||
creationDate: new Date(obj.creationDate),
|
||||
});
|
||||
}
|
||||
|
||||
toSdkFido2CredentialFullView(): Fido2CredentialFullView {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { FieldView as SdkFieldView, FieldType as SdkFieldType } from "@bitwarden/sdk-internal";
|
||||
@@ -9,13 +7,13 @@ import { FieldType, LinkedIdType } from "../../enums";
|
||||
import { Field } from "../domain/field";
|
||||
|
||||
export class FieldView implements View {
|
||||
name: string = null;
|
||||
value: string = null;
|
||||
type: FieldType = null;
|
||||
name?: string;
|
||||
value?: string;
|
||||
type: FieldType = FieldType.Text;
|
||||
newField = false; // Marks if the field is new and hasn't been saved
|
||||
showValue = false;
|
||||
showCount = false;
|
||||
linkedId: LinkedIdType = null;
|
||||
linkedId?: LinkedIdType;
|
||||
|
||||
constructor(f?: Field) {
|
||||
if (!f) {
|
||||
@@ -26,8 +24,8 @@ export class FieldView implements View {
|
||||
this.linkedId = f.linkedId;
|
||||
}
|
||||
|
||||
get maskedValue(): string {
|
||||
return this.value != null ? "••••••••" : null;
|
||||
get maskedValue(): string | undefined {
|
||||
return this.value != null ? "••••••••" : undefined;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<FieldView>>): FieldView {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { IdentityView as SdkIdentityView } from "@bitwarden/sdk-internal";
|
||||
@@ -12,65 +10,65 @@ import { ItemView } from "./item.view";
|
||||
|
||||
export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
@linkedFieldOption(LinkedId.Title, { sortPosition: 0 })
|
||||
title: string = null;
|
||||
title: string | undefined;
|
||||
@linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 })
|
||||
middleName: string = null;
|
||||
middleName: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Address1, { sortPosition: 12 })
|
||||
address1: string = null;
|
||||
address1: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Address2, { sortPosition: 13 })
|
||||
address2: string = null;
|
||||
address2: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Address3, { sortPosition: 14 })
|
||||
address3: string = null;
|
||||
address3: string | undefined;
|
||||
@linkedFieldOption(LinkedId.City, { sortPosition: 15, i18nKey: "cityTown" })
|
||||
city: string = null;
|
||||
city: string | undefined;
|
||||
@linkedFieldOption(LinkedId.State, { sortPosition: 16, i18nKey: "stateProvince" })
|
||||
state: string = null;
|
||||
state: string | undefined;
|
||||
@linkedFieldOption(LinkedId.PostalCode, { sortPosition: 17, i18nKey: "zipPostalCode" })
|
||||
postalCode: string = null;
|
||||
postalCode: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Country, { sortPosition: 18 })
|
||||
country: string = null;
|
||||
country: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Company, { sortPosition: 6 })
|
||||
company: string = null;
|
||||
company: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Email, { sortPosition: 10 })
|
||||
email: string = null;
|
||||
email: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Phone, { sortPosition: 11 })
|
||||
phone: string = null;
|
||||
phone: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Ssn, { sortPosition: 7 })
|
||||
ssn: string = null;
|
||||
ssn: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Username, { sortPosition: 5 })
|
||||
username: string = null;
|
||||
username: string | undefined;
|
||||
@linkedFieldOption(LinkedId.PassportNumber, { sortPosition: 8 })
|
||||
passportNumber: string = null;
|
||||
passportNumber: string | undefined;
|
||||
@linkedFieldOption(LinkedId.LicenseNumber, { sortPosition: 9 })
|
||||
licenseNumber: string = null;
|
||||
licenseNumber: string | undefined;
|
||||
|
||||
private _firstName: string = null;
|
||||
private _lastName: string = null;
|
||||
private _subTitle: string = null;
|
||||
private _firstName: string | undefined;
|
||||
private _lastName: string | undefined;
|
||||
private _subTitle: string | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.FirstName, { sortPosition: 1 })
|
||||
get firstName(): string {
|
||||
get firstName(): string | undefined {
|
||||
return this._firstName;
|
||||
}
|
||||
set firstName(value: string) {
|
||||
set firstName(value: string | undefined) {
|
||||
this._firstName = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.LastName, { sortPosition: 4 })
|
||||
get lastName(): string {
|
||||
get lastName(): string | undefined {
|
||||
return this._lastName;
|
||||
}
|
||||
set lastName(value: string) {
|
||||
set lastName(value: string | undefined) {
|
||||
this._lastName = value;
|
||||
this._subTitle = null;
|
||||
this._subTitle = undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
if (this._subTitle == null && (this.firstName != null || this.lastName != null)) {
|
||||
this._subTitle = "";
|
||||
if (this.firstName != null) {
|
||||
@@ -88,7 +86,7 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.FullName, { sortPosition: 3 })
|
||||
get fullName(): string {
|
||||
get fullName(): string | undefined {
|
||||
if (
|
||||
this.title != null ||
|
||||
this.firstName != null ||
|
||||
@@ -111,11 +109,11 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get fullAddress(): string {
|
||||
let address = this.address1;
|
||||
get fullAddress(): string | undefined {
|
||||
let address = this.address1 ?? "";
|
||||
if (!Utils.isNullOrWhitespace(this.address2)) {
|
||||
if (!Utils.isNullOrWhitespace(address)) {
|
||||
address += ", ";
|
||||
@@ -131,9 +129,9 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return address;
|
||||
}
|
||||
|
||||
get fullAddressPart2(): string {
|
||||
get fullAddressPart2(): string | undefined {
|
||||
if (this.city == null && this.state == null && this.postalCode == null) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
const city = this.city || "-";
|
||||
const state = this.state;
|
||||
@@ -146,7 +144,7 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return addressPart2;
|
||||
}
|
||||
|
||||
get fullAddressForCopy(): string {
|
||||
get fullAddressForCopy(): string | undefined {
|
||||
let address = this.fullAddress;
|
||||
if (this.city != null || this.state != null || this.postalCode != null) {
|
||||
address += "\n" + this.fullAddressPart2;
|
||||
@@ -157,38 +155,34 @@ export class IdentityView extends ItemView implements SdkIdentityView {
|
||||
return address;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<IdentityView>>): IdentityView {
|
||||
static fromJSON(obj: Partial<Jsonify<IdentityView>> | undefined): IdentityView {
|
||||
return Object.assign(new IdentityView(), obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the SDK IdentityView to an IdentityView.
|
||||
*/
|
||||
static fromSdkIdentityView(obj: SdkIdentityView): IdentityView | undefined {
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkIdentityView(obj: SdkIdentityView): IdentityView {
|
||||
const identityView = new IdentityView();
|
||||
|
||||
identityView.title = obj.title ?? null;
|
||||
identityView.firstName = obj.firstName ?? null;
|
||||
identityView.middleName = obj.middleName ?? null;
|
||||
identityView.lastName = obj.lastName ?? null;
|
||||
identityView.address1 = obj.address1 ?? null;
|
||||
identityView.address2 = obj.address2 ?? null;
|
||||
identityView.address3 = obj.address3 ?? null;
|
||||
identityView.city = obj.city ?? null;
|
||||
identityView.state = obj.state ?? null;
|
||||
identityView.postalCode = obj.postalCode ?? null;
|
||||
identityView.country = obj.country ?? null;
|
||||
identityView.company = obj.company ?? null;
|
||||
identityView.email = obj.email ?? null;
|
||||
identityView.phone = obj.phone ?? null;
|
||||
identityView.ssn = obj.ssn ?? null;
|
||||
identityView.username = obj.username ?? null;
|
||||
identityView.passportNumber = obj.passportNumber ?? null;
|
||||
identityView.licenseNumber = obj.licenseNumber ?? null;
|
||||
identityView.title = obj.title;
|
||||
identityView.firstName = obj.firstName;
|
||||
identityView.middleName = obj.middleName;
|
||||
identityView.lastName = obj.lastName;
|
||||
identityView.address1 = obj.address1;
|
||||
identityView.address2 = obj.address2;
|
||||
identityView.address3 = obj.address3;
|
||||
identityView.city = obj.city;
|
||||
identityView.state = obj.state;
|
||||
identityView.postalCode = obj.postalCode;
|
||||
identityView.country = obj.country;
|
||||
identityView.company = obj.company;
|
||||
identityView.email = obj.email;
|
||||
identityView.phone = obj.phone;
|
||||
identityView.ssn = obj.ssn;
|
||||
identityView.username = obj.username;
|
||||
identityView.passportNumber = obj.passportNumber;
|
||||
identityView.licenseNumber = obj.licenseNumber;
|
||||
|
||||
return identityView;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { View } from "../../../models/view/view";
|
||||
import { LinkedMetadata } from "../../linked-field-option.decorator";
|
||||
|
||||
export abstract class ItemView implements View {
|
||||
linkedFieldOptions: Map<number, LinkedMetadata>;
|
||||
abstract get subTitle(): string;
|
||||
linkedFieldOptions?: Map<number, LinkedMetadata>;
|
||||
abstract get subTitle(): string | undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { LoginUriView as SdkLoginUriView } from "@bitwarden/sdk-internal";
|
||||
@@ -11,13 +9,13 @@ import { Utils } from "../../../platform/misc/utils";
|
||||
import { LoginUri } from "../domain/login-uri";
|
||||
|
||||
export class LoginUriView implements View {
|
||||
match: UriMatchStrategySetting = null;
|
||||
match?: UriMatchStrategySetting;
|
||||
|
||||
private _uri: string = null;
|
||||
private _domain: string = null;
|
||||
private _hostname: string = null;
|
||||
private _host: string = null;
|
||||
private _canLaunch: boolean = null;
|
||||
private _uri?: string;
|
||||
private _domain?: string;
|
||||
private _hostname?: string;
|
||||
private _host?: string;
|
||||
private _canLaunch?: boolean;
|
||||
|
||||
constructor(u?: LoginUri) {
|
||||
if (!u) {
|
||||
@@ -27,59 +25,59 @@ export class LoginUriView implements View {
|
||||
this.match = u.match;
|
||||
}
|
||||
|
||||
get uri(): string {
|
||||
get uri(): string | undefined {
|
||||
return this._uri;
|
||||
}
|
||||
set uri(value: string) {
|
||||
set uri(value: string | undefined) {
|
||||
this._uri = value;
|
||||
this._domain = null;
|
||||
this._canLaunch = null;
|
||||
this._domain = undefined;
|
||||
this._canLaunch = undefined;
|
||||
}
|
||||
|
||||
get domain(): string {
|
||||
get domain(): string | undefined {
|
||||
if (this._domain == null && this.uri != null) {
|
||||
this._domain = Utils.getDomain(this.uri);
|
||||
if (this._domain === "") {
|
||||
this._domain = null;
|
||||
this._domain = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this._domain;
|
||||
}
|
||||
|
||||
get hostname(): string {
|
||||
get hostname(): string | undefined {
|
||||
if (this.match === UriMatchStrategy.RegularExpression) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
if (this._hostname == null && this.uri != null) {
|
||||
this._hostname = Utils.getHostname(this.uri);
|
||||
if (this._hostname === "") {
|
||||
this._hostname = null;
|
||||
this._hostname = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this._hostname;
|
||||
}
|
||||
|
||||
get host(): string {
|
||||
get host(): string | undefined {
|
||||
if (this.match === UriMatchStrategy.RegularExpression) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
if (this._host == null && this.uri != null) {
|
||||
this._host = Utils.getHost(this.uri);
|
||||
if (this._host === "") {
|
||||
this._host = null;
|
||||
this._host = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this._host;
|
||||
}
|
||||
|
||||
get hostnameOrUri(): string {
|
||||
get hostnameOrUri(): string | undefined {
|
||||
return this.hostname != null ? this.hostname : this.uri;
|
||||
}
|
||||
|
||||
get hostOrUri(): string {
|
||||
get hostOrUri(): string | undefined {
|
||||
return this.host != null ? this.host : this.uri;
|
||||
}
|
||||
|
||||
@@ -104,7 +102,10 @@ export class LoginUriView implements View {
|
||||
return this._canLaunch;
|
||||
}
|
||||
|
||||
get launchUri(): string {
|
||||
get launchUri(): string | undefined {
|
||||
if (this.uri == null) {
|
||||
return undefined;
|
||||
}
|
||||
return this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri))
|
||||
? "http://" + this.uri
|
||||
: this.uri;
|
||||
@@ -141,7 +142,7 @@ export class LoginUriView implements View {
|
||||
matchesUri(
|
||||
targetUri: string,
|
||||
equivalentDomains: Set<string>,
|
||||
defaultUriMatch: UriMatchStrategySetting = null,
|
||||
defaultUriMatch?: UriMatchStrategySetting,
|
||||
/** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */
|
||||
overrideNeverMatchStrategy?: true,
|
||||
): boolean {
|
||||
@@ -198,7 +199,7 @@ export class LoginUriView implements View {
|
||||
|
||||
if (Utils.DomainMatchBlacklist.has(this.domain)) {
|
||||
const domainUrlHost = Utils.getHost(targetUri);
|
||||
return !Utils.DomainMatchBlacklist.get(this.domain).has(domainUrlHost);
|
||||
return !Utils.DomainMatchBlacklist.get(this.domain)!.has(domainUrlHost);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -29,11 +29,6 @@ describe("LoginView", () => {
|
||||
});
|
||||
|
||||
describe("fromSdkLoginView", () => {
|
||||
it("should return undefined when the input is null", () => {
|
||||
const result = LoginView.fromSdkLoginView(null as unknown as SdkLoginView);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return a LoginView from an SdkLoginView", () => {
|
||||
jest.spyOn(LoginUriView, "fromSdkLoginUriView").mockImplementation(mockFromSdk);
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { LoginView as SdkLoginView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||
@@ -15,15 +13,15 @@ import { LoginUriView } from "./login-uri.view";
|
||||
|
||||
export class LoginView extends ItemView {
|
||||
@linkedFieldOption(LinkedId.Username, { sortPosition: 0 })
|
||||
username: string = null;
|
||||
username: string | undefined;
|
||||
@linkedFieldOption(LinkedId.Password, { sortPosition: 1 })
|
||||
password: string = null;
|
||||
password: string | undefined;
|
||||
|
||||
passwordRevisionDate?: Date = null;
|
||||
totp: string = null;
|
||||
passwordRevisionDate?: Date;
|
||||
totp: string | undefined;
|
||||
uris: LoginUriView[] = [];
|
||||
autofillOnPageLoad: boolean = null;
|
||||
fido2Credentials: Fido2CredentialView[] = null;
|
||||
autofillOnPageLoad: boolean | undefined;
|
||||
fido2Credentials: Fido2CredentialView[] = [];
|
||||
|
||||
constructor(l?: Login) {
|
||||
super();
|
||||
@@ -35,15 +33,15 @@ export class LoginView extends ItemView {
|
||||
this.autofillOnPageLoad = l.autofillOnPageLoad;
|
||||
}
|
||||
|
||||
get uri(): string {
|
||||
return this.hasUris ? this.uris[0].uri : null;
|
||||
get uri(): string | undefined {
|
||||
return this.hasUris ? this.uris[0].uri : undefined;
|
||||
}
|
||||
|
||||
get maskedPassword(): string {
|
||||
return this.password != null ? "••••••••" : null;
|
||||
get maskedPassword(): string | undefined {
|
||||
return this.password != null ? "••••••••" : undefined;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
get subTitle(): string | undefined {
|
||||
// if there's a passkey available, use that as a fallback
|
||||
if (Utils.isNullOrEmpty(this.username) && this.fido2Credentials?.length > 0) {
|
||||
return this.fido2Credentials[0].userName;
|
||||
@@ -60,14 +58,14 @@ export class LoginView extends ItemView {
|
||||
return !Utils.isNullOrWhitespace(this.totp);
|
||||
}
|
||||
|
||||
get launchUri(): string {
|
||||
get launchUri(): string | undefined {
|
||||
if (this.hasUris) {
|
||||
const uri = this.uris.find((u) => u.canLaunch);
|
||||
if (uri != null) {
|
||||
return uri.launchUri;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get hasUris(): boolean {
|
||||
@@ -81,7 +79,7 @@ export class LoginView extends ItemView {
|
||||
matchesUri(
|
||||
targetUri: string,
|
||||
equivalentDomains: Set<string>,
|
||||
defaultUriMatch: UriMatchStrategySetting = null,
|
||||
defaultUriMatch?: UriMatchStrategySetting,
|
||||
/** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */
|
||||
overrideNeverMatchStrategy?: true,
|
||||
): boolean {
|
||||
@@ -94,17 +92,20 @@ export class LoginView extends ItemView {
|
||||
);
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<DeepJsonify<LoginView>>): LoginView {
|
||||
const passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
const uris = obj.uris.map((uri) => LoginUriView.fromJSON(uri));
|
||||
const fido2Credentials = obj.fido2Credentials?.map((key) => Fido2CredentialView.fromJSON(key));
|
||||
static fromJSON(obj: Partial<DeepJsonify<LoginView>> | undefined): LoginView {
|
||||
if (obj == undefined) {
|
||||
return new LoginView();
|
||||
}
|
||||
|
||||
return Object.assign(new LoginView(), obj, {
|
||||
passwordRevisionDate,
|
||||
uris,
|
||||
fido2Credentials,
|
||||
});
|
||||
const loginView = Object.assign(new LoginView(), obj) as LoginView;
|
||||
|
||||
loginView.passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? undefined : new Date(obj.passwordRevisionDate);
|
||||
loginView.uris = obj.uris?.map((uri) => LoginUriView.fromJSON(uri)) ?? [];
|
||||
loginView.fido2Credentials =
|
||||
obj.fido2Credentials?.map((key) => Fido2CredentialView.fromJSON(key)) ?? [];
|
||||
|
||||
return loginView;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,25 +116,21 @@ export class LoginView extends ItemView {
|
||||
* the FIDO2 credentials in encrypted form. We can decrypt them later using a separate
|
||||
* call to client.vault().ciphers().decrypt_fido2_credentials().
|
||||
*/
|
||||
static fromSdkLoginView(obj: SdkLoginView): LoginView | undefined {
|
||||
if (obj == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkLoginView(obj: SdkLoginView): LoginView {
|
||||
const loginView = new LoginView();
|
||||
|
||||
loginView.username = obj.username ?? null;
|
||||
loginView.password = obj.password ?? null;
|
||||
loginView.username = obj.username;
|
||||
loginView.password = obj.password;
|
||||
loginView.passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
loginView.totp = obj.totp ?? null;
|
||||
loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null;
|
||||
obj.passwordRevisionDate == null ? undefined : new Date(obj.passwordRevisionDate);
|
||||
loginView.totp = obj.totp;
|
||||
loginView.autofillOnPageLoad = obj.autofillOnPageLoad;
|
||||
loginView.uris =
|
||||
obj.uris
|
||||
?.filter((uri) => uri.uri != null && uri.uri !== "")
|
||||
.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
|
||||
.map((uri) => LoginUriView.fromSdkLoginUriView(uri)!) || [];
|
||||
// FIDO2 credentials are not decrypted here, they remain encrypted
|
||||
loginView.fido2Credentials = null;
|
||||
loginView.fido2Credentials = [];
|
||||
|
||||
return loginView;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SecureNoteView as SdkSecureNoteView } from "@bitwarden/sdk-internal";
|
||||
@@ -10,7 +8,7 @@ import { SecureNote } from "../domain/secure-note";
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class SecureNoteView extends ItemView implements SdkSecureNoteView {
|
||||
type: SecureNoteType = null;
|
||||
type: SecureNoteType = SecureNoteType.Generic;
|
||||
|
||||
constructor(n?: SecureNote) {
|
||||
super();
|
||||
@@ -21,24 +19,20 @@ export class SecureNoteView extends ItemView implements SdkSecureNoteView {
|
||||
this.type = n.type;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
return null;
|
||||
get subTitle(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<SecureNoteView>>): SecureNoteView {
|
||||
static fromJSON(obj: Partial<Jsonify<SecureNoteView>> | undefined): SecureNoteView {
|
||||
return Object.assign(new SecureNoteView(), obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the SDK SecureNoteView to a SecureNoteView.
|
||||
*/
|
||||
static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView | undefined {
|
||||
if (!obj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkSecureNoteView(obj: SdkSecureNoteView): SecureNoteView {
|
||||
const secureNoteView = new SecureNoteView();
|
||||
secureNoteView.type = obj.type ?? null;
|
||||
secureNoteView.type = obj.type;
|
||||
|
||||
return secureNoteView;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SshKeyView as SdkSshKeyView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { SshKey } from "../domain/ssh-key";
|
||||
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class SshKeyView extends ItemView {
|
||||
privateKey: string = null;
|
||||
publicKey: string = null;
|
||||
keyFingerprint: string = null;
|
||||
|
||||
constructor(n?: SshKey) {
|
||||
super();
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
privateKey!: string;
|
||||
publicKey!: string;
|
||||
keyFingerprint!: string;
|
||||
|
||||
get maskedPrivateKey(): string {
|
||||
if (!this.privateKey || this.privateKey.length === 0) {
|
||||
@@ -43,23 +32,19 @@ export class SshKeyView extends ItemView {
|
||||
return this.keyFingerprint;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
|
||||
static fromJSON(obj: Partial<Jsonify<SshKeyView>> | undefined): SshKeyView {
|
||||
return Object.assign(new SshKeyView(), obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the SDK SshKeyView to a SshKeyView.
|
||||
*/
|
||||
static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView | undefined {
|
||||
if (!obj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static fromSdkSshKeyView(obj: SdkSshKeyView): SshKeyView {
|
||||
const sshKeyView = new SshKeyView();
|
||||
|
||||
sshKeyView.privateKey = obj.privateKey ?? null;
|
||||
sshKeyView.publicKey = obj.publicKey ?? null;
|
||||
sshKeyView.keyFingerprint = obj.fingerprint ?? null;
|
||||
sshKeyView.privateKey = obj.privateKey;
|
||||
sshKeyView.publicKey = obj.publicKey;
|
||||
sshKeyView.keyFingerprint = obj.fingerprint;
|
||||
|
||||
return sshKeyView;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of, firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
CipherBulkArchiveRequest,
|
||||
CipherBulkUnarchiveRequest,
|
||||
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { DefaultCipherArchiveService } from "./default-cipher-archive.service";
|
||||
|
||||
describe("DefaultCipherArchiveService", () => {
|
||||
let service: DefaultCipherArchiveService;
|
||||
let mockCipherService: jest.Mocked<CipherService>;
|
||||
let mockApiService: jest.Mocked<ApiService>;
|
||||
let mockBillingAccountProfileStateService: jest.Mocked<BillingAccountProfileStateService>;
|
||||
let mockConfigService: jest.Mocked<ConfigService>;
|
||||
|
||||
const userId = "user-id" as UserId;
|
||||
const cipherId = "123" as CipherId;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCipherService = mock<CipherService>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
|
||||
service = new DefaultCipherArchiveService(
|
||||
mockCipherService,
|
||||
mockApiService,
|
||||
mockBillingAccountProfileStateService,
|
||||
mockConfigService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("archivedCiphers$", () => {
|
||||
it("should return only archived ciphers", async () => {
|
||||
const mockCiphers: CipherListView[] = [
|
||||
{
|
||||
id: "1",
|
||||
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||
type: "identity",
|
||||
} as unknown as CipherListView,
|
||||
{
|
||||
id: "2",
|
||||
type: "secureNote",
|
||||
} as unknown as CipherListView,
|
||||
{
|
||||
id: "3",
|
||||
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||
deletedDate: "2024-01-16T10:30:00.000Z",
|
||||
type: "sshKey",
|
||||
} as unknown as CipherListView,
|
||||
];
|
||||
|
||||
mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
|
||||
|
||||
const result = await firstValueFrom(service.archivedCiphers$(userId));
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual("1");
|
||||
});
|
||||
|
||||
it("should return empty array when no archived ciphers exist", async () => {
|
||||
const mockCiphers: CipherListView[] = [
|
||||
{
|
||||
id: "1",
|
||||
type: "identity",
|
||||
} as unknown as CipherListView,
|
||||
];
|
||||
|
||||
mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
|
||||
|
||||
const result = await firstValueFrom(service.archivedCiphers$(userId));
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("userCanArchive$", () => {
|
||||
it("should return true when user has premium and feature flag is enabled", async () => {
|
||||
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
|
||||
const result = await firstValueFrom(service.userCanArchive$(userId));
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockBillingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
|
||||
userId,
|
||||
);
|
||||
expect(mockConfigService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM19148_InnovationArchive,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false when feature flag is disabled", async () => {
|
||||
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
|
||||
const result = await firstValueFrom(service.userCanArchive$(userId));
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("archiveWithServer", () => {
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{
|
||||
id: cipherId,
|
||||
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiService.send.mockResolvedValue(mockResponse);
|
||||
mockCipherService.ciphers$.mockReturnValue(
|
||||
of({
|
||||
[cipherId]: {
|
||||
id: cipherId,
|
||||
revisionDate: "2024-01-15T10:00:00.000Z",
|
||||
} as any,
|
||||
}),
|
||||
);
|
||||
mockCipherService.replace.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("should archive single cipher", async () => {
|
||||
await service.archiveWithServer(cipherId, userId);
|
||||
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/ciphers/archive",
|
||||
expect.any(CipherBulkArchiveRequest),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCipherService.replace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[cipherId]: expect.objectContaining({
|
||||
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||
}),
|
||||
}),
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should archive multiple ciphers", async () => {
|
||||
const cipherIds = [cipherId, "cipher-id-2" as CipherId];
|
||||
|
||||
await service.archiveWithServer(cipherIds, userId);
|
||||
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/ciphers/archive",
|
||||
expect.objectContaining({
|
||||
ids: cipherIds,
|
||||
}),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unarchiveWithServer", () => {
|
||||
const mockResponse = {
|
||||
data: [
|
||||
{
|
||||
id: cipherId,
|
||||
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiService.send.mockResolvedValue(mockResponse);
|
||||
mockCipherService.ciphers$.mockReturnValue(
|
||||
of({
|
||||
[cipherId]: {
|
||||
id: cipherId,
|
||||
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||
revisionDate: "2024-01-15T10:00:00.000Z",
|
||||
} as any,
|
||||
}),
|
||||
);
|
||||
mockCipherService.replace.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("should unarchive single cipher", async () => {
|
||||
await service.unarchiveWithServer(cipherId, userId);
|
||||
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/ciphers/unarchive",
|
||||
expect.any(CipherBulkUnarchiveRequest),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCipherService.replace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[cipherId]: expect.objectContaining({
|
||||
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||
}),
|
||||
}),
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("should unarchive multiple ciphers", async () => {
|
||||
const cipherIds = [cipherId, "cipher-id-2" as CipherId];
|
||||
|
||||
await service.unarchiveWithServer(cipherIds, userId);
|
||||
|
||||
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/ciphers/unarchive",
|
||||
expect.objectContaining({
|
||||
ids: cipherIds,
|
||||
}),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
122
libs/common/src/vault/services/default-cipher-archive.service.ts
Normal file
122
libs/common/src/vault/services/default-cipher-archive.service.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { filter, map, Observable, shareReplay, combineLatest, firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
CipherBulkArchiveRequest,
|
||||
CipherBulkUnarchiveRequest,
|
||||
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
|
||||
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
|
||||
import {
|
||||
CipherViewLike,
|
||||
CipherViewLikeUtils,
|
||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
import { CipherArchiveService } from "../abstractions/cipher-archive.service";
|
||||
|
||||
export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Observable that contains the list of ciphers that have been archived.
|
||||
*/
|
||||
archivedCiphers$(userId: UserId): Observable<CipherViewLike[]> {
|
||||
return this.cipherService.cipherListViews$(userId).pipe(
|
||||
filter((cipher) => cipher != null),
|
||||
map((ciphers) =>
|
||||
ciphers.filter(
|
||||
(cipher) =>
|
||||
CipherViewLikeUtils.isArchived(cipher) && !CipherViewLikeUtils.isDeleted(cipher),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User can archive items if:
|
||||
* Feature Flag is enabled
|
||||
* User has premium from any source (personal or organization)
|
||||
*/
|
||||
userCanArchive$(userId: UserId): Observable<boolean> {
|
||||
return combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive),
|
||||
]).pipe(
|
||||
map(([hasPremium, archiveFlagEnabled]) => hasPremium && archiveFlagEnabled),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* User can access the archive vault if:
|
||||
* Feature Flag is enabled
|
||||
* There is at least one archived item
|
||||
* ///////////// NOTE /////////////
|
||||
* This is separated from userCanArchive because a user that loses premium status, but has archived items,
|
||||
* should still be able to access their archive vault. The items will be read-only, and can be restored.
|
||||
*/
|
||||
showArchiveVault$(userId: UserId): Observable<boolean> {
|
||||
return combineLatest([
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive),
|
||||
this.archivedCiphers$(userId),
|
||||
]).pipe(
|
||||
map(
|
||||
([archiveFlagEnabled, hasArchivedItems]) =>
|
||||
archiveFlagEnabled && hasArchivedItems.length > 0,
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
}
|
||||
|
||||
async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
|
||||
const request = new CipherBulkArchiveRequest(Array.isArray(ids) ? ids : [ids]);
|
||||
const r = await this.apiService.send("PUT", "/ciphers/archive", request, true, true);
|
||||
const response = new ListResponse(r, CipherResponse);
|
||||
|
||||
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
|
||||
for (const cipher of response.data) {
|
||||
const localCipher = currentCiphers[cipher.id as CipherId];
|
||||
|
||||
if (localCipher == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
localCipher.archivedDate = cipher.archivedDate;
|
||||
localCipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
await this.cipherService.replace(currentCiphers, userId);
|
||||
}
|
||||
|
||||
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
|
||||
const request = new CipherBulkUnarchiveRequest(Array.isArray(ids) ? ids : [ids]);
|
||||
const r = await this.apiService.send("PUT", "/ciphers/unarchive", request, true, true);
|
||||
const response = new ListResponse(r, CipherResponse);
|
||||
|
||||
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||
|
||||
for (const cipher of response.data) {
|
||||
const localCipher = currentCiphers[cipher.id as CipherId];
|
||||
|
||||
if (localCipher == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
localCipher.archivedDate = cipher.archivedDate;
|
||||
localCipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
await this.cipherService.replace(currentCiphers, userId);
|
||||
}
|
||||
}
|
||||
@@ -296,12 +296,20 @@ export class SearchService implements SearchServiceAbstraction {
|
||||
return results;
|
||||
}
|
||||
|
||||
searchCiphersBasic<C extends CipherViewLike>(ciphers: C[], query: string, deleted = false) {
|
||||
searchCiphersBasic<C extends CipherViewLike>(
|
||||
ciphers: C[],
|
||||
query: string,
|
||||
deleted = false,
|
||||
archived = false,
|
||||
) {
|
||||
query = SearchService.normalizeSearchQuery(query.trim().toLowerCase());
|
||||
return ciphers.filter((c) => {
|
||||
if (deleted !== CipherViewLikeUtils.isDeleted(c)) {
|
||||
return false;
|
||||
}
|
||||
if (archived !== CipherViewLikeUtils.isArchived(c)) {
|
||||
return false;
|
||||
}
|
||||
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -298,6 +298,10 @@ describe("CipherViewLikeUtils", () => {
|
||||
(cipherView.attachments as any) = null;
|
||||
|
||||
expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(false);
|
||||
|
||||
cipherView.attachments = [];
|
||||
|
||||
expect(CipherViewLikeUtils.hasAttachments(cipherView)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export function perUserCache$<TValue>(
|
||||
create(userId),
|
||||
clearBuffer$.pipe(
|
||||
filter((clearId) => clearId === userId || clearId === null),
|
||||
map(() => null),
|
||||
map((): any => null),
|
||||
),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: false }));
|
||||
cache.set(userId, observable);
|
||||
|
||||
Reference in New Issue
Block a user