mirror of
https://github.com/bitwarden/browser
synced 2026-02-16 08:34:39 +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:
@@ -0,0 +1,20 @@
|
||||
<div class="tw-flex tw-flex-col tw-items-center">
|
||||
<ng-container *ngIf="currentState === 'assert'">
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "readingPasskeyLoading" | i18n }}</p>
|
||||
<button type="button" bitButton block [loading]="true" buttonType="primary" class="tw-mb-4">
|
||||
{{ "loading" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="currentState === 'assertFailed'">
|
||||
<p bitTypography="body1" class="tw-text-center">{{ "passkeyAuthenticationFailed" | i18n }}</p>
|
||||
<button type="button" bitButton block buttonType="primary" class="tw-mb-4" (click)="retry()">
|
||||
{{ "tryAgain" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
<p bitTypography="body1" class="tw-mb-0 tw-text-center">
|
||||
{{ "troubleLoggingIn" | i18n }}<br />
|
||||
<a bitLink routerLink="/login">{{ "useADifferentLogInMethod" | i18n }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1,27 +1,69 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
TwoFactorAuthSecurityKeyIcon,
|
||||
TwoFactorAuthSecurityKeyFailedIcon,
|
||||
} from "@bitwarden/assets/svg";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { LoginSuccessHandlerService } from "@bitwarden/auth/common";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export type State = "assert" | "assertFailed";
|
||||
|
||||
@Directive()
|
||||
export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
@Component({
|
||||
selector: "app-login-via-webauthn",
|
||||
templateUrl: "login-via-webauthn.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
JslibModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class LoginViaWebAuthnComponent implements OnInit {
|
||||
protected currentState: State = "assert";
|
||||
|
||||
protected successRoute = "/vault";
|
||||
protected readonly Icons = {
|
||||
TwoFactorAuthSecurityKeyIcon,
|
||||
TwoFactorAuthSecurityKeyFailedIcon,
|
||||
};
|
||||
|
||||
private readonly successRoutes: Record<ClientType, string> = {
|
||||
[ClientType.Web]: "/vault",
|
||||
[ClientType.Browser]: "/tabs/vault",
|
||||
[ClientType.Desktop]: "/vault",
|
||||
[ClientType.Cli]: "/vault",
|
||||
};
|
||||
|
||||
protected get successRoute(): string {
|
||||
const clientType = this.platformUtilsService.getClientType();
|
||||
return this.successRoutes[clientType] || "/vault";
|
||||
}
|
||||
|
||||
constructor(
|
||||
private webAuthnLoginService: WebAuthnLoginServiceAbstraction,
|
||||
@@ -31,6 +73,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
private i18nService: I18nService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private keyService: KeyService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -41,6 +85,8 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
|
||||
protected retry() {
|
||||
this.currentState = "assert";
|
||||
// Reset to default icon on retry
|
||||
this.setDefaultIcon();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.authenticate();
|
||||
@@ -54,6 +100,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
this.currentState = "assertFailed";
|
||||
this.setFailureIcon();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -64,6 +111,7 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
this.i18nService.t("twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn"),
|
||||
);
|
||||
this.currentState = "assertFailed";
|
||||
this.setFailureIcon();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,6 +128,19 @@ export class BaseLoginViaWebAuthnComponent implements OnInit {
|
||||
}
|
||||
this.logService.error(error);
|
||||
this.currentState = "assertFailed";
|
||||
this.setFailureIcon();
|
||||
}
|
||||
}
|
||||
|
||||
private setDefaultIcon(): void {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageIcon: this.Icons.TwoFactorAuthSecurityKeyIcon,
|
||||
});
|
||||
}
|
||||
|
||||
private setFailureIcon(): void {
|
||||
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
|
||||
pageIcon: this.Icons.TwoFactorAuthSecurityKeyFailedIcon,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [title]="'addCredit' | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ "creditDelayed" | i18n }}</p>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-radio-group formControlName="paymentMethod">
|
||||
<bit-radio-button [value]="paymentMethodType.PayPal">
|
||||
<bit-label> <i class="bwi bwi-paypal"></i>{{ "payPal" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button [value]="paymentMethodType.BitPay">
|
||||
<bit-label> <i class="bwi bwi-bitcoin"></i>{{ "bitcoin" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<div class="tw-grid tw-grid-cols-2">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "amount" | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="creditAmount" step="0.01" required />
|
||||
<span bitPrefix>$USD</span>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="ResultType.Closed"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<form #payPalForm action="{{ payPalConfig.buttonAction }}" method="post" target="_top">
|
||||
<input type="hidden" name="cmd" value="_xclick" />
|
||||
<input type="hidden" name="business" value="{{ payPalConfig.businessId }}" />
|
||||
<input type="hidden" name="button_subtype" value="services" />
|
||||
<input type="hidden" name="no_note" value="1" />
|
||||
<input type="hidden" name="no_shipping" value="1" />
|
||||
<input type="hidden" name="rm" value="1" />
|
||||
<input type="hidden" name="return" value="{{ payPalConfig.returnUrl }}" />
|
||||
<input type="hidden" name="cancel_return" value="{{ payPalConfig.returnUrl }}" />
|
||||
<input type="hidden" name="currency_code" value="USD" />
|
||||
<input type="hidden" name="image_url" value="https://bitwarden.com/images/paypal-banner.png" />
|
||||
<input type="hidden" name="bn" value="PP-BuyNowBF:btn_buynow_LG.gif:NonHosted" />
|
||||
<input type="hidden" name="amount" value="{{ formGroup.get('creditAmount').value }}" />
|
||||
<input type="hidden" name="custom" value="{{ payPalConfig.customField }}" />
|
||||
<input type="hidden" name="item_name" value="Bitwarden Account Credit" />
|
||||
<input type="hidden" name="item_number" value="{{ payPalConfig.subject }}" />
|
||||
</form>
|
||||
@@ -1,167 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, ElementRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AccountService, AccountInfo } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
|
||||
export type AddAccountCreditDialogParams = {
|
||||
organizationId?: string;
|
||||
providerId?: string;
|
||||
};
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum AddAccountCreditDialogResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
}
|
||||
|
||||
export const openAddAccountCreditDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AddAccountCreditDialogParams>,
|
||||
) =>
|
||||
dialogService.open<AddAccountCreditDialogResultType, AddAccountCreditDialogParams>(
|
||||
AddAccountCreditDialogComponent,
|
||||
dialogConfig,
|
||||
);
|
||||
|
||||
type PayPalConfig = {
|
||||
businessId?: string;
|
||||
buttonAction?: string;
|
||||
returnUrl?: string;
|
||||
customField?: string;
|
||||
subject?: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
templateUrl: "./add-account-credit-dialog.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class AddAccountCreditDialogComponent implements OnInit {
|
||||
@ViewChild("payPalForm", { read: ElementRef, static: true }) payPalForm: ElementRef;
|
||||
protected formGroup = new FormGroup({
|
||||
paymentMethod: new FormControl<PaymentMethodType>(PaymentMethodType.PayPal),
|
||||
creditAmount: new FormControl<number>(null, [Validators.required, Validators.min(0.01)]),
|
||||
});
|
||||
protected payPalConfig: PayPalConfig;
|
||||
protected ResultType = AddAccountCreditDialogResultType;
|
||||
|
||||
private organization?: Organization;
|
||||
private provider?: Provider;
|
||||
private user?: { id: UserId } & AccountInfo;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private configService: ConfigService,
|
||||
@Inject(DIALOG_DATA) private dialogParams: AddAccountCreditDialogParams,
|
||||
private dialogRef: DialogRef<AddAccountCreditDialogResultType>,
|
||||
private organizationService: OrganizationService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private providerService: ProviderService,
|
||||
) {
|
||||
this.payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig;
|
||||
}
|
||||
|
||||
protected readonly paymentMethodType = PaymentMethodType;
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.value.paymentMethod === PaymentMethodType.PayPal) {
|
||||
this.payPalForm.nativeElement.submit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formGroup.value.paymentMethod === PaymentMethodType.BitPay) {
|
||||
const request = this.getBitPayInvoiceRequest();
|
||||
const bitPayUrl = await this.apiService.postBitPayInvoice(request);
|
||||
this.platformUtilsService.launchUri(bitPayUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(AddAccountCreditDialogResultType.Submitted);
|
||||
};
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
let payPalCustomField: string;
|
||||
|
||||
if (this.dialogParams.organizationId) {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 20.0,
|
||||
});
|
||||
this.user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(this.user.id)
|
||||
.pipe(
|
||||
map((organizations) =>
|
||||
organizations.find((org) => org.id === this.dialogParams.organizationId),
|
||||
),
|
||||
),
|
||||
);
|
||||
payPalCustomField = "organization_id:" + this.organization.id;
|
||||
this.payPalConfig.subject = this.organization.name;
|
||||
} else if (this.dialogParams.providerId) {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 20.0,
|
||||
});
|
||||
this.provider = await firstValueFrom(
|
||||
this.providerService.get$(this.dialogParams.providerId, this.user.id),
|
||||
);
|
||||
payPalCustomField = "provider_id:" + this.provider.id;
|
||||
this.payPalConfig.subject = this.provider.name;
|
||||
} else {
|
||||
this.formGroup.patchValue({
|
||||
creditAmount: 10.0,
|
||||
});
|
||||
payPalCustomField = "user_id:" + this.user.id;
|
||||
this.payPalConfig.subject = this.user.email;
|
||||
}
|
||||
|
||||
const region = await firstValueFrom(this.configService.cloudRegion$);
|
||||
|
||||
payPalCustomField += ",account_credit:1";
|
||||
payPalCustomField += `,region:${region}`;
|
||||
|
||||
this.payPalConfig.customField = payPalCustomField;
|
||||
this.payPalConfig.returnUrl = window.location.href;
|
||||
}
|
||||
|
||||
getBitPayInvoiceRequest(): BitPayInvoiceRequest {
|
||||
const request = new BitPayInvoiceRequest();
|
||||
if (this.organization) {
|
||||
request.name = this.organization.name;
|
||||
request.organizationId = this.organization.id;
|
||||
} else if (this.provider) {
|
||||
request.name = this.provider.name;
|
||||
request.providerId = this.provider.id;
|
||||
} else {
|
||||
request.email = this.user.email;
|
||||
request.userId = this.user.id;
|
||||
}
|
||||
|
||||
request.credit = true;
|
||||
request.amount = this.formGroup.value.creditAmount;
|
||||
request.returnUrl = window.location.href;
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
export * from "./add-account-credit-dialog/add-account-credit-dialog.component";
|
||||
export * from "./invoices/invoices.component";
|
||||
export * from "./invoices/no-invoices.component";
|
||||
export * from "./manage-tax-information/manage-tax-information.component";
|
||||
export * from "./premium.component";
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-table *ngIf="!loading">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ "date" | i18n }}</th>
|
||||
<th bitCell>{{ "invoice" | i18n }}</th>
|
||||
<th bitCell>{{ "total" | i18n }}</th>
|
||||
<th bitCell>{{ "status" | i18n }}</th>
|
||||
<th bitCell>{{ "clientDetails" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let invoice of invoices">
|
||||
<td bitCell>{{ invoice.date | date: "mediumDate" }}</td>
|
||||
<td bitCell>
|
||||
<a
|
||||
href="{{ invoice.url }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="{{ 'viewInvoice' | i18n }}"
|
||||
>
|
||||
{{ invoice.number }}
|
||||
</a>
|
||||
</td>
|
||||
<td bitCell>{{ invoice.total | currency: "$" }}</td>
|
||||
<td bitCell *ngIf="expandInvoiceStatus(invoice) as expandedInvoiceStatus">
|
||||
<span *ngIf="expandedInvoiceStatus === 'open'">
|
||||
{{ "open" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'unpaid'">
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "unpaid" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'paid'">
|
||||
<i class="bwi bwi-check tw-text-success" aria-hidden="true"></i>
|
||||
{{ "paid" | i18n | titlecase }}
|
||||
</span>
|
||||
<span *ngIf="expandedInvoiceStatus === 'uncollectible'">
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "uncollectible" | i18n | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button type="button" bitLink (click)="runExport(invoice.id)">
|
||||
<span class="tw-font-normal">{{ "downloadCSV" | i18n }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
<div *ngIf="!invoices || invoices.length === 0" class="tw-mt-10">
|
||||
<app-no-invoices></app-no-invoices>
|
||||
</div>
|
||||
@@ -1,67 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import {
|
||||
InvoiceResponse,
|
||||
InvoicesResponse,
|
||||
} from "@bitwarden/common/billing/models/response/invoices.response";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-invoices",
|
||||
templateUrl: "./invoices.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class InvoicesComponent implements OnInit {
|
||||
@Input() startWith?: InvoicesResponse;
|
||||
@Input() getInvoices?: () => Promise<InvoicesResponse>;
|
||||
@Input() getClientInvoiceReport?: (invoiceId: string) => Promise<string>;
|
||||
@Input() getClientInvoiceReportName?: (invoiceResponse: InvoiceResponse) => string;
|
||||
|
||||
protected invoices: InvoiceResponse[] = [];
|
||||
protected loading = true;
|
||||
|
||||
constructor(private fileDownloadService: FileDownloadService) {}
|
||||
|
||||
runExport = async (invoiceId: string): Promise<void> => {
|
||||
const blobData = await this.getClientInvoiceReport(invoiceId);
|
||||
let fileName = "report.csv";
|
||||
if (this.getClientInvoiceReportName) {
|
||||
const invoice = this.invoices.find((invoice) => invoice.id === invoiceId);
|
||||
fileName = this.getClientInvoiceReportName(invoice);
|
||||
}
|
||||
this.fileDownloadService.download({
|
||||
fileName,
|
||||
blobData,
|
||||
blobOptions: {
|
||||
type: "text/csv",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (this.startWith) {
|
||||
this.invoices = this.startWith.invoices;
|
||||
} else if (this.getInvoices) {
|
||||
const response = await this.getInvoices();
|
||||
this.invoices = response.invoices;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
expandInvoiceStatus = (
|
||||
invoice: InvoiceResponse,
|
||||
): "open" | "unpaid" | "paid" | "uncollectible" => {
|
||||
switch (invoice.status) {
|
||||
case "open": {
|
||||
const dueDate = new Date(invoice.dueDate);
|
||||
return dueDate < new Date() ? "unpaid" : invoice.status;
|
||||
}
|
||||
case "paid":
|
||||
case "uncollectible": {
|
||||
return invoice.status;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { CreditCardIcon } from "@bitwarden/assets/svg";
|
||||
|
||||
@Component({
|
||||
selector: "app-no-invoices",
|
||||
template: `<bit-no-items [icon]="icon">
|
||||
<div slot="title">{{ "noInvoicesToList" | i18n }}</div>
|
||||
</bit-no-items>`,
|
||||
standalone: false,
|
||||
})
|
||||
export class NoInvoicesComponent {
|
||||
icon = CreditCardIcon;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "country" | i18n }}</bit-label>
|
||||
<bit-select formControlName="country" data-testid="country">
|
||||
<bit-option
|
||||
*ngFor="let country of countries"
|
||||
[value]="country.value"
|
||||
[disabled]="country.disabled"
|
||||
[label]="country.name"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "zipPostalCode" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="postalCode"
|
||||
autocomplete="postal-code"
|
||||
data-testid="postal-code"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<ng-container *ngIf="isTaxSupported">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "address1" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="line1"
|
||||
autocomplete="address-line1"
|
||||
data-testid="address-line1"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "address2" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="line2"
|
||||
autocomplete="address-line2"
|
||||
data-testid="address-line2"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "cityTown" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="city"
|
||||
autocomplete="address-level2"
|
||||
data-testid="city"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "stateProvince" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="state"
|
||||
autocomplete="address-level1"
|
||||
data-testid="state"
|
||||
/>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<div class="tw-col-span-6" *ngIf="showTaxIdField">
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label>{{ "taxIdNumber" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="taxId" data-testid="tax-id" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="tw-col-span-12" *ngIf="!!onSubmit">
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,262 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { SimpleChange } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SelectModule, FormFieldModule, BitSubmitDirective } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { ManageTaxInformationComponent } from "./manage-tax-information.component";
|
||||
|
||||
describe("ManageTaxInformationComponent", () => {
|
||||
let sut: ManageTaxInformationComponent;
|
||||
let fixture: ComponentFixture<ManageTaxInformationComponent>;
|
||||
let mockTaxService: MockProxy<TaxServiceAbstraction>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockTaxService = mock();
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ManageTaxInformationComponent],
|
||||
providers: [
|
||||
{ provide: TaxServiceAbstraction, useValue: mockTaxService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
SelectModule,
|
||||
FormFieldModule,
|
||||
BitSubmitDirective,
|
||||
I18nPipe,
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ManageTaxInformationComponent);
|
||||
sut = fixture.componentInstance;
|
||||
fixture.autoDetectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("creates successfully", () => {
|
||||
expect(sut).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize with all values empty in startWith", async () => {
|
||||
// Arrange
|
||||
sut.startWith = {
|
||||
country: "",
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
|
||||
// Act
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const startWithValue = sut.startWith;
|
||||
expect(startWithValue.line1).toHaveLength(0);
|
||||
expect(startWithValue.line2).toHaveLength(0);
|
||||
expect(startWithValue.city).toHaveLength(0);
|
||||
expect(startWithValue.state).toHaveLength(0);
|
||||
expect(startWithValue.postalCode).toHaveLength(0);
|
||||
expect(startWithValue.country).toHaveLength(0);
|
||||
expect(startWithValue.taxId).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should update the tax information protected state when form is updated", async () => {
|
||||
// Arrange
|
||||
const line1Value = "123 Street";
|
||||
const line2Value = "Apt. 5";
|
||||
const cityValue = "New York";
|
||||
const stateValue = "NY";
|
||||
const countryValue = "USA";
|
||||
const postalCodeValue = "123 Street";
|
||||
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const line1: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line1']",
|
||||
);
|
||||
const line2: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line2']",
|
||||
);
|
||||
const city: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='city']",
|
||||
);
|
||||
const state: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='state']",
|
||||
);
|
||||
const postalCode: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='postalCode']",
|
||||
);
|
||||
|
||||
line1.value = line1Value;
|
||||
line2.value = line2Value;
|
||||
city.value = cityValue;
|
||||
state.value = stateValue;
|
||||
postalCode.value = postalCodeValue;
|
||||
|
||||
line1.dispatchEvent(new Event("input"));
|
||||
line2.dispatchEvent(new Event("input"));
|
||||
city.dispatchEvent(new Event("input"));
|
||||
state.dispatchEvent(new Event("input"));
|
||||
postalCode.dispatchEvent(new Event("input"));
|
||||
await fixture.whenStable();
|
||||
|
||||
// Assert
|
||||
|
||||
// Assert that the internal tax information reflects the form
|
||||
const taxInformation = sut.getTaxInformation();
|
||||
expect(taxInformation.line1).toBe(line1Value);
|
||||
expect(taxInformation.line2).toBe(line2Value);
|
||||
expect(taxInformation.city).toBe(cityValue);
|
||||
expect(taxInformation.state).toBe(stateValue);
|
||||
expect(taxInformation.postalCode).toBe(postalCodeValue);
|
||||
expect(taxInformation.country).toBe(countryValue);
|
||||
expect(taxInformation.taxId).toHaveLength(0);
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should not show address fields except postal code if country is not supported for taxes", async () => {
|
||||
// Arrange
|
||||
const countryValue = "UNKNOWN";
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(false);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const line1: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line1']",
|
||||
);
|
||||
const line2: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='line2']",
|
||||
);
|
||||
const city: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='city']",
|
||||
);
|
||||
const state: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='state']",
|
||||
);
|
||||
const postalCode: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='postalCode']",
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(line1).toBeNull();
|
||||
expect(line2).toBeNull();
|
||||
expect(city).toBeNull();
|
||||
expect(state).toBeNull();
|
||||
//Should be visible
|
||||
expect(postalCode).toBeTruthy();
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should not show the tax id field if showTaxIdField is set to false", async () => {
|
||||
// Arrange
|
||||
const countryValue = "USA";
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
|
||||
sut.showTaxIdField = false;
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const taxId: HTMLInputElement = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='taxId']",
|
||||
);
|
||||
expect(taxId).toBeNull();
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should clear the tax id field if showTaxIdField is set to false after being true", async () => {
|
||||
// Arrange
|
||||
const countryValue = "USA";
|
||||
const taxIdValue = "A12345678";
|
||||
|
||||
sut.startWith = {
|
||||
country: countryValue,
|
||||
postalCode: "",
|
||||
taxId: taxIdValue,
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
};
|
||||
sut.showTaxIdField = true;
|
||||
|
||||
mockTaxService.isCountrySupported.mockResolvedValue(true);
|
||||
await sut.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
const initialTaxIdValue = fixture.nativeElement.querySelector(
|
||||
"input[formControlName='taxId']",
|
||||
).value;
|
||||
|
||||
// Act
|
||||
sut.showTaxIdField = false;
|
||||
sut.ngOnChanges({ showTaxIdField: new SimpleChange(true, false, false) });
|
||||
fixture.detectChanges();
|
||||
|
||||
// Assert
|
||||
const taxId = fixture.nativeElement.querySelector("input[formControlName='taxId']");
|
||||
expect(taxId).toBeNull();
|
||||
|
||||
const taxInformation = sut.getTaxInformation();
|
||||
expect(taxInformation.taxId).toBeNull();
|
||||
expect(initialTaxIdValue).toEqual(taxIdValue);
|
||||
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledWith(countryValue);
|
||||
expect(mockTaxService.isCountrySupported).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,166 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
|
||||
@Component({
|
||||
selector: "app-manage-tax-information",
|
||||
templateUrl: "./manage-tax-information.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class ManageTaxInformationComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() startWith: TaxInformation;
|
||||
@Input() onSubmit?: (taxInformation: TaxInformation) => Promise<void>;
|
||||
@Input() showTaxIdField: boolean = true;
|
||||
|
||||
/**
|
||||
* Emits when the tax information has changed.
|
||||
*/
|
||||
@Output() taxInformationChanged = new EventEmitter<TaxInformation>();
|
||||
|
||||
/**
|
||||
* Emits when the tax information has been updated.
|
||||
*/
|
||||
@Output() taxInformationUpdated = new EventEmitter();
|
||||
|
||||
private taxInformation: TaxInformation;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
country: ["", Validators.required],
|
||||
postalCode: ["", Validators.required],
|
||||
taxId: "",
|
||||
line1: "",
|
||||
line2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
});
|
||||
|
||||
protected isTaxSupported: boolean;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected readonly countries: CountryListItem[] = this.taxService.getCountries();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
) {}
|
||||
|
||||
getTaxInformation(): TaxInformation {
|
||||
return this.taxInformation;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.markAllAsTouched();
|
||||
if (this.formGroup.invalid) {
|
||||
return;
|
||||
}
|
||||
await this.onSubmit?.(this.taxInformation);
|
||||
this.taxInformationUpdated.emit();
|
||||
};
|
||||
|
||||
validate(): boolean {
|
||||
this.markAllAsTouched();
|
||||
return this.formGroup.valid;
|
||||
}
|
||||
|
||||
markAllAsTouched() {
|
||||
this.formGroup.markAllAsTouched();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => {
|
||||
this.taxInformation = {
|
||||
country: values.country,
|
||||
postalCode: values.postalCode,
|
||||
taxId: values.taxId,
|
||||
line1: values.line1,
|
||||
line2: values.line2,
|
||||
city: values.city,
|
||||
state: values.state,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.startWith) {
|
||||
this.formGroup.controls.country.setValue(this.startWith.country);
|
||||
this.formGroup.controls.postalCode.setValue(this.startWith.postalCode);
|
||||
|
||||
this.isTaxSupported =
|
||||
this.startWith && this.startWith.country
|
||||
? await this.taxService.isCountrySupported(this.startWith.country)
|
||||
: false;
|
||||
|
||||
if (this.isTaxSupported) {
|
||||
this.formGroup.controls.taxId.setValue(this.startWith.taxId);
|
||||
this.formGroup.controls.line1.setValue(this.startWith.line1);
|
||||
this.formGroup.controls.line2.setValue(this.startWith.line2);
|
||||
this.formGroup.controls.city.setValue(this.startWith.city);
|
||||
this.formGroup.controls.state.setValue(this.startWith.state);
|
||||
}
|
||||
}
|
||||
|
||||
this.formGroup.controls.country.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe((country: string) => {
|
||||
this.taxService
|
||||
.isCountrySupported(country)
|
||||
.then((isSupported) => (this.isTaxSupported = isSupported))
|
||||
.catch(() => (this.isTaxSupported = false))
|
||||
.finally(() => {
|
||||
if (!this.isTaxSupported) {
|
||||
this.formGroup.controls.taxId.setValue(null);
|
||||
this.formGroup.controls.line1.setValue(null);
|
||||
this.formGroup.controls.line2.setValue(null);
|
||||
this.formGroup.controls.city.setValue(null);
|
||||
this.formGroup.controls.state.setValue(null);
|
||||
}
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.formGroup.controls.postalCode.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
|
||||
this.formGroup.controls.taxId.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
if (this.taxInformationChanged) {
|
||||
this.taxInformationChanged.emit(this.taxInformation);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
// Clear the value of the tax-id if states have been changed in the parent component
|
||||
const showTaxIdField = changes["showTaxIdField"];
|
||||
if (showTaxIdField && !showTaxIdField.currentValue) {
|
||||
this.formGroup.controls.taxId.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,6 @@ import { CommonModule, DatePipe } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import {
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
} from "@bitwarden/angular/billing/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
AutofocusDirective,
|
||||
@@ -112,10 +106,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
TwoFactorIconComponent,
|
||||
],
|
||||
exports: [
|
||||
@@ -146,10 +136,6 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserTypePipe,
|
||||
IfFeatureDirective,
|
||||
FingerprintPipe,
|
||||
AddAccountCreditDialogComponent,
|
||||
InvoicesComponent,
|
||||
NoInvoicesComponent,
|
||||
ManageTaxInformationComponent,
|
||||
TwoFactorIconComponent,
|
||||
TextDragDirective,
|
||||
],
|
||||
|
||||
@@ -20,11 +20,13 @@ import {
|
||||
import {
|
||||
DefaultLoginComponentService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
DefaultNewDeviceVerificationComponentService,
|
||||
DefaultRegistrationFinishService,
|
||||
DefaultTwoFactorAuthComponentService,
|
||||
DefaultTwoFactorAuthWebAuthnComponentService,
|
||||
LoginComponentService,
|
||||
LoginDecryptionOptionsService,
|
||||
NewDeviceVerificationComponentService,
|
||||
RegistrationFinishService as RegistrationFinishServiceAbstraction,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
@@ -102,6 +104,7 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@
|
||||
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
|
||||
import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/auth/send-access";
|
||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||
@@ -144,14 +147,12 @@ import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/a
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||
import { OrganizationSponsorshipApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-sponsorship-api.service.abstraction";
|
||||
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
|
||||
import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service";
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service";
|
||||
import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service";
|
||||
import { OrganizationSponsorshipApiService } from "@bitwarden/common/billing/services/organization/organization-sponsorship-api.service";
|
||||
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
|
||||
import { TaxService } from "@bitwarden/common/billing/services/tax.service";
|
||||
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
@@ -264,6 +265,7 @@ import {
|
||||
InternalSendService,
|
||||
SendService as SendServiceAbstraction,
|
||||
} from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
@@ -284,6 +286,7 @@ import {
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
@@ -296,7 +299,6 @@ import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
@@ -345,11 +347,7 @@ import {
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
CipherArchiveService,
|
||||
DefaultCipherArchiveService,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import {
|
||||
IndividualVaultExportService,
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
@@ -1401,11 +1399,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: BillingApiService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaxServiceAbstraction,
|
||||
useClass: TaxService,
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BillingAccountProfileStateService,
|
||||
useClass: DefaultBillingAccountProfileStateService,
|
||||
@@ -1596,6 +1589,11 @@ const safeProviders: SafeProvider[] = [
|
||||
MessageListener,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SendTokenService,
|
||||
useClass: DefaultSendTokenService,
|
||||
deps: [GlobalStateProvider, SdkService, SendPasswordService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EndUserNotificationService,
|
||||
useClass: DefaultEndUserNotificationService,
|
||||
@@ -1652,12 +1650,15 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
DialogService,
|
||||
PasswordRepromptService,
|
||||
BillingAccountProfileStateService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NewDeviceVerificationComponentService,
|
||||
useClass: DefaultNewDeviceVerificationComponentService,
|
||||
deps: [],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Directive, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { CollectionTypes, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
|
||||
@@ -21,6 +21,7 @@ export class CollectionFilterComponent {
|
||||
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
|
||||
new EventEmitter<ITreeNodeObject>();
|
||||
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
|
||||
DefaultCollectionType = CollectionTypes.DefaultUserCollection;
|
||||
|
||||
readonly collectionsGrouping: TopLevelTreeNode = {
|
||||
id: "collections",
|
||||
|
||||
26
libs/assets/src/svg/svgs/favorites.icon.ts
Normal file
26
libs/assets/src/svg/svgs/favorites.icon.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { svgIcon } from "../icon-service";
|
||||
|
||||
export const FavoritesIcon = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="5.29 4.98 86.89 90.19">
|
||||
<g clip-path="url(#clip0_2211_2391)">
|
||||
<path class="tw-stroke-illustration-outline tw-fill-illustration-bg-primary" d="M45.7322 7.73645C46.8425 5.06767 50.6228 5.06767 51.7332 7.73645L60.8269 29.5929C61.511 31.2373 63.0575 32.3607 64.8328 32.5031L88.4343 34.3947C91.316 34.6257 92.4843 38.2221 90.2888 40.1027L72.3083 55.5001C70.9554 56.6589 70.3647 58.4773 70.7781 60.2101L76.2712 83.2335C76.9419 86.0452 73.8838 88.2672 71.4167 86.7609L51.2078 74.422C49.688 73.4941 47.7773 73.4941 46.2576 74.422L26.0486 86.7609C23.5815 88.2672 20.5234 86.0452 21.1941 83.2335L26.6873 60.2101C27.1007 58.4773 26.51 56.6589 25.157 55.5001L7.17651 40.1027C4.98107 38.2221 6.14942 34.6258 9.03101 34.3947L32.6326 32.5031C34.4079 32.3607 35.9543 31.2373 36.6384 29.5929L45.7322 7.73645Z" stroke-width="1.5"/>
|
||||
<path class="tw-stroke-illustration-outline tw-fill-illustration-bg-tertiary" d="M86.5363 75.5456L67.0229 86.8083L64.3035 82.0996C63.2768 82.3932 60.0282 82.2577 55.7085 78.0915C51.1997 73.7429 53.1 69.4322 54.8897 67.3528L51.1991 60.9624C51.1991 60.9624 49.0085 57.1701 47.5085 54.572C46.5871 52.9761 46.8445 50.4707 48.594 49.461C50.3435 48.4512 52.9064 49.1601 53.9008 50.8825C56.3862 55.1873 60.6993 62.6543 60.6993 62.6543L60.3136 61.9865C59.0344 59.7715 57.8733 56.6618 60.0887 55.3831C60.1803 55.3302 60.2715 55.2816 60.3624 55.2371C62.6556 54.1121 64.7529 56.4687 66.0303 58.6805C65.81 57.9107 65.7395 56.1576 67.2199 55.3032C69.5661 53.949 71.7744 56.9755 73.1292 59.3214L72.6308 58.4584C72.1365 57.4731 71.6772 55.4212 73.4319 54.4084C75.2982 53.3313 76.7992 54.9314 77.3409 55.7398C78.3682 57.3892 80.6222 61.1108 81.42 62.8029C82.2178 64.495 82.7029 67.7429 82.8457 69.1553L86.5363 75.5456Z"/>
|
||||
<path class="tw-stroke-illustration-outline" d="M66.0303 58.6805C65.81 57.9107 65.7395 56.1576 67.2199 55.3032V55.3032C69.5661 53.949 71.7744 56.9755 73.1292 59.3214L73.9905 60.8128L72.6308 58.4584C72.1365 57.4731 71.6772 55.4212 73.4319 54.4084C75.2982 53.3313 76.7992 54.9314 77.3409 55.7398C78.3682 57.3892 80.6222 61.1108 81.42 62.8029C82.2178 64.495 82.7029 67.7429 82.8457 69.1553L86.5363 75.5456L67.0229 86.8083L64.3035 82.0996C63.2768 82.3932 60.0282 82.2577 55.7085 78.0915C51.1997 73.7429 53.1 69.4322 54.8897 67.3528M66.0303 58.6805L67.9727 62.0439M66.0303 58.6805V58.6805C64.7529 56.4687 62.6556 54.1121 60.3624 55.2371C60.2715 55.2816 60.1803 55.3302 60.0887 55.3831V55.3831C57.8733 56.6618 59.0344 59.7715 60.3136 61.9865L60.6993 62.6543C60.6993 62.6543 56.3862 55.1873 53.9008 50.8825C52.9064 49.1601 50.3435 48.4512 48.594 49.461C46.8445 50.4707 46.5871 52.9761 47.5085 54.572C49.0085 57.1701 51.1991 60.9624 51.1991 60.9624L54.8897 67.3528M57.6091 72.0616L54.8897 67.3528" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path class="tw-stroke-illustration-outline tw-fill-illustration-bg-secondary" d="M70.9905 93.6786L67.5232 87.6748C67.247 87.1965 67.4108 86.585 67.8892 86.309L85.6704 76.046C86.1487 75.7699 86.7604 75.9338 87.0366 76.4121L90.5039 82.4159C90.7802 82.8942 90.6163 83.5057 90.138 83.7818L72.3567 94.0447C71.8784 94.3208 71.2667 94.1569 70.9905 93.6786Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g clip-path="url(#clip1_2211_2391)">
|
||||
<path class="tw-stroke-illustration-tertiary" d="M41.5571 37.1704L45.0496 43.2177" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M54.9774 35.4178L53.187 42.1557" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M65.7288 43.6858L59.6986 47.1663" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M35.1155 61.3549L41.1457 57.8744" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-illustration-tertiary" d="M33.329 47.9126L40.0662 49.7286" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2211_2391">
|
||||
<rect width="96" height="96" class="tw-fill-bg-tertiary" transform="translate(0.5 0.400024)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_2211_2391">
|
||||
<rect width="37.6674" height="20.0537" class="tw-fill-bg-tertiary" transform="matrix(0.86609 -0.499888 0.500112 0.86596 25.2179 45.5771)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>`;
|
||||
@@ -12,6 +12,7 @@ export * from "./deactivated-org";
|
||||
export * from "./devices.icon";
|
||||
export * from "./domain.icon";
|
||||
export * from "./empty-trash";
|
||||
export * from "./favorites.icon";
|
||||
export * from "./gear";
|
||||
export * from "./generator";
|
||||
export * from "./item-types";
|
||||
|
||||
@@ -59,6 +59,8 @@ export * from "./two-factor-auth";
|
||||
|
||||
// device verification
|
||||
export * from "./new-device-verification/new-device-verification.component";
|
||||
export * from "./new-device-verification/new-device-verification-component.service";
|
||||
export * from "./new-device-verification/default-new-device-verification-component.service";
|
||||
|
||||
// validators
|
||||
export * from "./validators/compare-inputs.validator";
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ErrorResponse } from "@bitwarden/common/models/response/error.response"
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -138,6 +139,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private configService: ConfigService,
|
||||
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private environmentService: EnvironmentService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
@@ -307,7 +309,7 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
await this.handleAuthResult(authResult);
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
this.handleSubmitError(error);
|
||||
await this.handleSubmitError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -316,15 +318,18 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
*
|
||||
* @param error The error object.
|
||||
*/
|
||||
private handleSubmitError(error: unknown) {
|
||||
private async handleSubmitError(error: unknown) {
|
||||
// Handle error responses
|
||||
if (error instanceof ErrorResponse) {
|
||||
switch (error.statusCode) {
|
||||
case HttpStatusCode.BadRequest: {
|
||||
if (error.message?.toLowerCase().includes("username or password is incorrect")) {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const host = Utils.getHost(env.getWebVaultUrl());
|
||||
|
||||
this.formGroup.controls.masterPassword.setErrors({
|
||||
error: {
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
message: this.i18nService.t("invalidMasterPasswordConfirmEmailAndHost", host),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DefaultNewDeviceVerificationComponentService } from "./default-new-device-verification-component.service";
|
||||
|
||||
describe("DefaultNewDeviceVerificationComponentService", () => {
|
||||
let sut: DefaultNewDeviceVerificationComponentService;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new DefaultNewDeviceVerificationComponentService();
|
||||
});
|
||||
|
||||
it("should instantiate the service", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("showBackButton()", () => {
|
||||
it("should return true", () => {
|
||||
const result = sut.showBackButton();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NewDeviceVerificationComponentService } from "./new-device-verification-component.service";
|
||||
|
||||
export class DefaultNewDeviceVerificationComponentService
|
||||
implements NewDeviceVerificationComponentService
|
||||
{
|
||||
showBackButton() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export abstract class NewDeviceVerificationComponentService {
|
||||
/**
|
||||
* States whether component should show a back button. Can be overridden by client-specific component services.
|
||||
* - Default = `true`
|
||||
* - Extension = `false` (because Extension shows a back button in the header instead)
|
||||
*/
|
||||
abstract showBackButton: () => boolean;
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
{{ "resendCode" | i18n }}
|
||||
</button>
|
||||
|
||||
<div class="tw-flex tw-mt-4">
|
||||
<div class="tw-grid tw-gap-3 tw-mt-4">
|
||||
<button
|
||||
bitButton
|
||||
bitFormButton
|
||||
@@ -33,5 +33,13 @@
|
||||
>
|
||||
{{ "continueLoggingIn" | i18n }}
|
||||
</button>
|
||||
|
||||
@if (showBackButton) {
|
||||
<div class="tw-text-center">{{ "or" | i18n }}</div>
|
||||
|
||||
<button type="button" bitButton block buttonType="secondary" (click)="goBack()">
|
||||
{{ "back" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { CommonModule, Location } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
@@ -11,7 +11,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
@@ -26,6 +25,8 @@ import {
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "../../common/abstractions/login-strategy.service";
|
||||
|
||||
import { NewDeviceVerificationComponentService } from "./new-device-verification-component.service";
|
||||
|
||||
/**
|
||||
* Component for verifying a new device via a one-time password (OTP).
|
||||
*/
|
||||
@@ -57,6 +58,7 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
protected disableRequestOTP = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
protected authenticationSessionTimeoutRoute = "/authentication-timeout";
|
||||
protected showBackButton = true;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@@ -66,12 +68,15 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
private newDeviceVerificationComponentService: NewDeviceVerificationComponentService,
|
||||
private location: Location,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.showBackButton = this.newDeviceVerificationComponentService.showBackButton();
|
||||
|
||||
// Redirect to timeout route if session expires
|
||||
this.loginStrategyService.authenticationSessionTimeout$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -179,4 +184,8 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
codeControl.markAsTouched();
|
||||
}
|
||||
};
|
||||
|
||||
protected goBack() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
@@ -397,4 +397,8 @@ export class MockPublicKeyCredential implements PublicKeyCredential {
|
||||
static isUserVerifyingPlatformAuthenticatorAvailable(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
export abstract class CipherArchiveService {
|
||||
@@ -10,5 +9,4 @@ export abstract class CipherArchiveService {
|
||||
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
||||
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||
abstract canInteract(cipher: CipherView): Promise<boolean>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user