1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-05 18:13:26 +00:00

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

This commit is contained in:
Alec Rippberger
2024-09-30 12:58:04 -05:00
368 changed files with 5854 additions and 1954 deletions

View File

@@ -21,7 +21,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
@@ -30,6 +29,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BiometricStateService } from "@bitwarden/key-management";
import { PolicyListService } from "./admin-console/core/policy-list.service";
import {

View File

@@ -66,33 +66,39 @@
</bit-callout>
</bit-section>
<bit-section *ngIf="isSelfHost">
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
<bit-form-field>
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
<div>
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
}}
</div>
<input
bitInput
#fileSelector
type="file"
formControlName="file"
(change)="onLicenseFileSelected($event)"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" buttonType="primary" bitButton bitFormButton>
{{ "submit" | i18n }}
</button>
</form>
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
<bit-form-field>
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
<div>
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
}}
</div>
<input
bitInput
#fileSelector
type="file"
formControlName="file"
(change)="onLicenseFileSelected($event)"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" buttonType="primary" bitButton bitFormButton>
{{ "submit" | i18n }}
</button>
</form>
</ng-container>
<individual-self-hosting-license-uploader
*ngIf="useLicenseUploaderComponent$ | async"
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
/>
</bit-section>
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [bitSubmit]="submitPayment">
<bit-section>

View File

@@ -7,6 +7,8 @@ import { combineLatest, concatMap, from, Observable, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -36,6 +38,10 @@ export class PremiumV2Component {
protected cloudWebVaultURL: string;
protected isSelfHost = false;
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
);
protected readonly familyPlanMaxUserCount = 6;
protected readonly premiumPrice = 10;
protected readonly storageGBPrice = 4;
@@ -44,6 +50,7 @@ export class PremiumV2Component {
private activatedRoute: ActivatedRoute,
private apiService: ApiService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private configService: ConfigService,
private environmentService: EnvironmentService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
@@ -78,6 +85,9 @@ export class PremiumV2Component {
finalizeUpgrade = async () => {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
};
postFinalizeUpgrade = async () => {
this.toastService.showToast({
variant: "success",
title: null,
@@ -119,6 +129,7 @@ export class PremiumV2Component {
await this.apiService.postAccountLicense(formData);
await this.finalizeUpgrade();
await this.postFinalizeUpgrade();
};
submitPayment = async (): Promise<void> => {
@@ -138,6 +149,7 @@ export class PremiumV2Component {
await this.apiService.postPremium(formData);
await this.finalizeUpgrade();
await this.postFinalizeUpgrade();
};
protected get additionalStorageCost(): number {
@@ -161,4 +173,8 @@ export class PremiumV2Component {
protected get total(): number {
return this.subtotal + this.estimatedTax;
}
protected async onLicenseFileSelectedChanged(): Promise<void> {
await this.postFinalizeUpgrade();
}
}

View File

@@ -757,7 +757,7 @@
{{ "serviceAccounts" | i18n }}
&times;
{{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }}
/{{ "month" | i18n }}
/{{ "year" | i18n }}
</span>
<span>{{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }}</span>
</p>

View File

@@ -497,7 +497,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return 0;
}
const result = plan.PasswordManager.seatPrice * Math.abs(this.organization.seats || 0);
const result = plan.PasswordManager.seatPrice * Math.abs(this.sub?.seats || 0);
return result;
}

View File

@@ -7,32 +7,38 @@
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="createOrganization && selfHosted">
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
<bit-form-field>
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
<div class="tw-pt-2 tw-pb-1">
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
</div>
<input
#fileSelector
bitInput
type="file"
formControlName="file"
(change)="setSelectedFile($event)"
accept="application/JSON"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
</form>
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
<bit-form-field>
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
<div class="tw-pt-2 tw-pb-1">
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
</div>
<input
#fileSelector
bitInput
type="file"
formControlName="file"
(change)="setSelectedFile($event)"
accept="application/JSON"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
</form>
</ng-container>
<organization-self-hosting-license-uploader
*ngIf="useLicenseUploaderComponent$ | async"
(onLicenseFileUploaded)="onLicenseFileUploaded($event)"
/>
</ng-container>
<form
[formGroup]="formGroup"

View File

@@ -117,6 +117,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
discount = 0;
deprecateStripeSourcesAPI: boolean;
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
);
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
selfHostedForm = this.formBuilder.group({
@@ -855,4 +859,30 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
private planIsEnabled(plan: PlanResponse) {
return !plan.disabled && !plan.legacyYear;
}
protected async onLicenseFileUploaded(organizationId: string): Promise<void> {
this.toastService.showToast({
variant: "success",
title: this.i18nService.t("organizationCreated"),
message: this.i18nService.t("organizationReadyToGo"),
});
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
// 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.router.navigate(["/organizations/" + organizationId]);
}
if (this.isInTrialFlow) {
this.onTrialBillingSuccess.emit({
orgId: organizationId,
subLabelText: this.billingSubLabelText(),
});
}
this.onSuccess.emit({ organizationId: organizationId });
// TODO: No one actually listening to this message?
this.messagingService.send("organizationCreated", { organizationId: organizationId });
}
}

View File

@@ -13,6 +13,8 @@ import { OffboardingSurveyComponent } from "./offboarding-survey.component";
import { PaymentV2Component } from "./payment/payment-v2.component";
import { PaymentComponent } from "./payment/payment.component";
import { PaymentMethodComponent } from "./payment-method.component";
import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component";
import { OrganizationSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/organization-self-hosting-license-uploader.component";
import { SecretsManagerSubscribeComponent } from "./sm-subscribe.component";
import { TaxInfoComponent } from "./tax-info.component";
import { UpdateLicenseDialogComponent } from "./update-license-dialog.component";
@@ -40,6 +42,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
OffboardingSurveyComponent,
AdjustPaymentDialogV2Component,
AdjustStorageDialogV2Component,
IndividualSelfHostingLicenseUploaderComponent,
OrganizationSelfHostingLicenseUploaderComponent,
],
exports: [
SharedModule,
@@ -53,6 +57,8 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
OffboardingSurveyComponent,
VerifyBankAccountComponent,
PaymentV2Component,
IndividualSelfHostingLicenseUploaderComponent,
OrganizationSelfHostingLicenseUploaderComponent,
],
})
export class BillingSharedModule {}

View File

@@ -0,0 +1,81 @@
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
import { LicenseUploaderFormModel } from "./license-uploader-form-model";
/**
* Shared implementation for processing license file uploads.
* @remarks Requires self-hosting.
*/
export abstract class AbstractSelfHostingLicenseUploaderComponent {
protected form: FormGroup;
protected constructor(
protected readonly formBuilder: FormBuilder,
protected readonly i18nService: I18nService,
protected readonly platformUtilsService: PlatformUtilsService,
protected readonly toastService: ToastService,
protected readonly tokenService: TokenService,
) {
const isSelfHosted = this.platformUtilsService.isSelfHost();
if (!isSelfHosted) {
throw new Error("This component should only be used in self-hosted environments");
}
this.form = this.formBuilder.group({
file: [null, [Validators.required]],
});
this.submit = this.submit.bind(this);
}
/**
* Gets the submitted license upload form model.
* @protected
*/
protected get formValue(): LicenseUploaderFormModel {
return this.form.value as LicenseUploaderFormModel;
}
/**
* Triggered when a different license file is selected.
* @param event
*/
onLicenseFileSelectedChanged(event: Event): void {
const element = event.target as HTMLInputElement;
this.form.value.file = element.files.length > 0 ? element.files[0] : null;
}
/**
* Submits the license upload form.
* @protected
*/
protected async submit(): Promise<void> {
this.form.markAllAsTouched();
if (this.form.invalid) {
return this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("selectFile"),
});
}
const emailVerified = await this.tokenService.getEmailVerified();
if (!emailVerified) {
return this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("verifyEmailFirst"),
});
}
}
abstract get description(): string;
abstract get hintFileName(): string;
}

View File

@@ -0,0 +1,60 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { ToastService } from "@bitwarden/components";
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
/**
* Processes license file uploads for individual plans.
* @remarks Requires self-hosting.
*/
@Component({
selector: "individual-self-hosting-license-uploader",
templateUrl: "./self-hosting-license-uploader.component.html",
})
export class IndividualSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
/**
* Emitted when a license file has been successfully uploaded & processed.
*/
@Output() onLicenseFileUploaded: EventEmitter<void> = new EventEmitter<void>();
constructor(
protected readonly apiService: ApiService,
protected readonly formBuilder: FormBuilder,
protected readonly i18nService: I18nService,
protected readonly platformUtilsService: PlatformUtilsService,
protected readonly syncService: SyncService,
protected readonly toastService: ToastService,
protected readonly tokenService: TokenService,
) {
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
}
protected async submit(): Promise<void> {
await super.submit();
const formData = new FormData();
formData.append("license", this.formValue.file);
await this.apiService.postAccountLicense(formData);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.onLicenseFileUploaded.emit();
}
get description(): string {
return "uploadLicenseFilePremium";
}
get hintFileName(): string {
return "bitwarden_premium_license.json";
}
}

View File

@@ -0,0 +1,3 @@
export interface LicenseUploaderFormModel {
file: File;
}

View File

@@ -0,0 +1,85 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { OrgKey } from "@bitwarden/common/types/key";
import { ToastService } from "@bitwarden/components";
import { AbstractSelfHostingLicenseUploaderComponent } from "../../shared/self-hosting-license-uploader/abstract-self-hosting-license-uploader.component";
/**
* Processes license file uploads for organizations.
* @remarks Requires self-hosting.
*/
@Component({
selector: "organization-self-hosting-license-uploader",
templateUrl: "./self-hosting-license-uploader.component.html",
})
export class OrganizationSelfHostingLicenseUploaderComponent extends AbstractSelfHostingLicenseUploaderComponent {
/**
* Notifies the parent component of the `organizationId` the license was created for.
*/
@Output() onLicenseFileUploaded: EventEmitter<string> = new EventEmitter<string>();
constructor(
protected readonly formBuilder: FormBuilder,
protected readonly i18nService: I18nService,
protected readonly platformUtilsService: PlatformUtilsService,
protected readonly toastService: ToastService,
protected readonly tokenService: TokenService,
private readonly apiService: ApiService,
private readonly encryptService: EncryptService,
private readonly cryptoService: CryptoService,
private readonly organizationApiService: OrganizationApiServiceAbstraction,
private readonly syncService: SyncService,
) {
super(formBuilder, i18nService, platformUtilsService, toastService, tokenService);
}
protected async submit(): Promise<void> {
await super.submit();
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
const collection = await this.encryptService.encrypt(
this.i18nService.t("defaultCollection"),
orgKey[1],
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
const fd = new FormData();
fd.append("license", this.formValue.file);
fd.append("key", key);
fd.append("collectionName", collectionCt);
const response = await this.organizationApiService.createLicense(fd);
const orgId = response.id;
await this.apiService.refreshIdentityToken();
// Org Keys live outside of the OrganizationLicense - add the keys to the org here
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
await this.organizationApiService.updateKeys(orgId, request);
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
this.onLicenseFileUploaded.emit(orgId);
}
get description(): string {
return "uploadLicenseFileOrg";
}
get hintFileName(): string {
return "bitwarden_organization_license.json";
}
}

View File

@@ -0,0 +1,26 @@
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
<form [formGroup]="form" [bitSubmit]="submit">
<bit-form-field>
<bit-label>{{ description | i18n }}</bit-label>
<div>
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ form.value.file ? form.value.file.name : ("noFileChosen" | i18n) }}
</div>
<input
#fileSelector
bitInput
type="file"
formControlName="file"
(change)="onLicenseFileSelectedChanged($event)"
accept="application/JSON"
hidden
class="tw-hidden"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: hintFileName }}</bit-hint>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
</form>

View File

@@ -4,24 +4,24 @@ import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
SECURE_STORAGE,
LOCALES_DIRECTORY,
SYSTEM_LANGUAGE,
MEMORY_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_DISK_LOCAL_STORAGE,
WINDOW,
SafeInjectionToken,
DEFAULT_VAULT_TIMEOUT,
CLIENT_TYPE,
DEFAULT_VAULT_TIMEOUT,
LOCALES_DIRECTORY,
MEMORY_STORAGE,
OBSERVABLE_DISK_LOCAL_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
SECURE_STORAGE,
SYSTEM_LANGUAGE,
SafeInjectionToken,
WINDOW,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import {
SetPasswordJitService,
RegistrationFinishService as RegistrationFinishServiceAbstraction,
LoginComponentService,
SetPasswordJitService,
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
@@ -48,7 +48,6 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { AppIdService as DefaultAppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
@@ -67,6 +66,7 @@ import {
} from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BiometricsService } from "@bitwarden/key-management";
import { PolicyListService } from "../admin-console/core/policy-list.service";
import {
@@ -77,7 +77,7 @@ import {
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
import { WebBiometricsService } from "../platform/web-biometric.service";
import { WebBiometricsService } from "../key-management/web-biometric.service";
import { WebEnvironmentService } from "../platform/web-environment.service";
import { WebMigrationRunner } from "../platform/web-migration-runner";
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";

View File

@@ -1,4 +1,4 @@
import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service";
import { BiometricsService } from "@bitwarden/key-management";
export class WebBiometricsService extends BiometricsService {
async supportsBiometric(): Promise<boolean> {

View File

@@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@@ -81,7 +82,7 @@ export class CipherReportComponent implements OnDestroy {
if (filterId === 0) {
cipherCount = this.allCiphers.length;
} else if (filterId === 1) {
cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length;
cipherCount = this.allCiphers.filter((c) => c.organizationId === null).length;
} else {
this.organizations.filter((org: Organization) => {
if (org.id === filterId) {
@@ -89,22 +90,20 @@ export class CipherReportComponent implements OnDestroy {
return org;
}
});
cipherCount = this.allCiphers.filter(
(c: any) => c.orgFilterStatus === orgFilterStatus,
).length;
cipherCount = this.allCiphers.filter((c) => c.organizationId === orgFilterStatus).length;
}
return cipherCount;
}
async filterOrgToggle(status: any) {
this.currentFilterStatus = status;
if (status === 0) {
this.dataSource.filter = null;
} else if (status === 1) {
this.dataSource.filter = (c: any) => c.orgFilterStatus == null;
} else {
this.dataSource.filter = (c: any) => c.orgFilterStatus === status;
let filter = null;
if (typeof status === "number" && status === 1) {
filter = (c: CipherView) => c.organizationId == null;
} else if (typeof status === "string") {
const orgId = status as OrganizationId;
filter = (c: CipherView) => c.organizationId === orgId;
}
this.dataSource.filter = filter;
}
async load() {
@@ -183,9 +182,7 @@ export class CipherReportComponent implements OnDestroy {
protected filterCiphersByOrg(ciphersList: CipherView[]) {
this.allCiphers = [...ciphersList];
this.ciphers = ciphersList.map((ciph: any) => {
ciph.orgFilterStatus = ciph.organizationId;
this.ciphers = ciphersList.map((ciph) => {
if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) {
this.filterStatus.push(ciph.organizationId);
} else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) {
@@ -193,7 +190,6 @@ export class CipherReportComponent implements OnDestroy {
}
return ciph;
});
this.dataSource.data = this.ciphers;
if (this.filterStatus.length > 2) {

View File

@@ -34,7 +34,9 @@
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
{{ "owner" | i18n }}
</th>
<th bitCell class="tw-text-right" bitSortable="exposedXTimes"></th>
<th bitCell class="tw-text-right" bitSortable="exposedXTimes">
{{ "timesExposed" | i18n }}
</th>
</tr>
</ng-container>
<ng-template body let-rows$>

View File

@@ -39,7 +39,7 @@
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
<th bitCell></th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>

View File

@@ -32,12 +32,16 @@
</ng-container>
</bit-toggle-group>
<bit-table [dataSource]="dataSource">
<ng-container header *ngIf="!isAdminConsoleActive">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
{{ "owner" | i18n }}
</th>
<th bitCell class="tw-text-right" bitSortable="reportValue" default>
{{ "weakness" | i18n }}
</th>
</tr>
</ng-container>
<ng-template body let-rows$>
@@ -80,7 +84,7 @@
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell>
<td bitCell *ngIf="!isAdminConsoleActive">
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
@@ -91,8 +95,8 @@
</app-org-badge>
</td>
<td bitCell class="tw-text-right">
<span bitBadge [variant]="passwordStrengthMap.get(r.id)[1]">
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
<span bitBadge [variant]="r.reportValue.badgeVariant">
{{ r.reportValue.label | i18n }}
</span>
</td>
</tr>

View File

@@ -14,16 +14,17 @@ import { PasswordRepromptService } from "@bitwarden/vault";
import { CipherReportComponent } from "./cipher-report.component";
type ReportScore = { label: string; badgeVariant: BadgeVariant };
type ReportResult = CipherView & { reportValue: ReportScore };
@Component({
selector: "app-weak-passwords-report",
templateUrl: "weak-passwords-report.component.html",
})
export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
disabled = true;
private passwordStrengthCache = new Map<string, number>();
weakPasswordCiphers: CipherView[] = [];
weakPasswordCiphers: ReportResult[] = [];
constructor(
protected cipherService: CipherService,
@@ -49,16 +50,15 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
}
async setCiphers() {
const allCiphers: any = await this.getAllCiphers();
this.passwordStrengthCache = new Map<string, number>();
const allCiphers = await this.getAllCiphers();
this.weakPasswordCiphers = [];
this.filterStatus = [0];
this.findWeakPasswords(allCiphers);
}
protected findWeakPasswords(ciphers: any[]): void {
protected findWeakPasswords(ciphers: CipherView[]): void {
ciphers.forEach((ciph) => {
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
const { type, login, isDeleted, edit, viewPassword } = ciph;
if (
type !== CipherType.Login ||
login.password == null ||
@@ -71,50 +71,39 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
}
const hasUserName = this.isUserNameNotEmpty(ciph);
const cacheKey = this.getCacheKey(ciph);
if (!this.passwordStrengthCache.has(cacheKey)) {
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
if (atPosition > -1) {
userInput = userInput
.concat(
login.username
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/),
)
.filter((i) => i.length >= 3);
} else {
userInput = login.username
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
.filter((i: any) => i.length >= 3);
}
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
if (atPosition > -1) {
userInput = userInput
.concat(
login.username
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/),
)
.filter((i) => i.length >= 3);
} else {
userInput = login.username
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
.filter((i) => i.length >= 3);
}
const result = this.passwordStrengthService.getPasswordStrength(
login.password,
null,
userInput.length > 0 ? userInput : null,
);
this.passwordStrengthCache.set(cacheKey, result.score);
}
const score = this.passwordStrengthCache.get(cacheKey);
if (score != null && score <= 2) {
this.passwordStrengthMap.set(id, this.scoreKey(score));
this.weakPasswordCiphers.push(ciph);
}
});
this.weakPasswordCiphers.sort((a, b) => {
return (
this.passwordStrengthCache.get(this.getCacheKey(a)) -
this.passwordStrengthCache.get(this.getCacheKey(b))
const result = this.passwordStrengthService.getPasswordStrength(
login.password,
null,
userInput.length > 0 ? userInput : null,
);
});
if (result.score != null && result.score <= 2) {
const scoreValue = this.scoreKey(result.score);
const row = { ...ciph, reportValue: scoreValue } as ReportResult;
this.weakPasswordCiphers.push(row);
}
});
this.filterCiphersByOrg(this.weakPasswordCiphers);
}
@@ -127,20 +116,16 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
return !Utils.isNullOrWhitespace(c.login.username);
}
private getCacheKey(c: CipherView): string {
return c.login.password + "_____" + (this.isUserNameNotEmpty(c) ? c.login.username : "");
}
private scoreKey(score: number): [string, BadgeVariant] {
private scoreKey(score: number): ReportScore {
switch (score) {
case 4:
return ["strong", "success"];
return { label: "strong", badgeVariant: "success" };
case 3:
return ["good", "primary"];
return { label: "good", badgeVariant: "primary" };
case 2:
return ["weak", "warning"];
return { label: "weak", badgeVariant: "warning" };
default:
return ["veryWeak", "danger"];
return { label: "veryWeak", badgeVariant: "danger" };
}
}
}

View File

@@ -12,12 +12,10 @@
>
<bit-item slot="attachment-button">
<button bit-item-content type="button" (click)="openAttachmentsDialog()">
<p class="tw-m-0">
{{ "attachments" | i18n }}
<span *ngIf="!canAccessAttachments" bitBadge variant="success" class="tw-ml-2">
{{ "premium" | i18n }}
</span>
</p>
{{ "attachments" | i18n }}
<span *ngIf="!canAccessAttachments" bitBadge variant="success" slot="default-trailing">
{{ "premium" | i18n }}
</span>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</button>
</bit-item>

View File

@@ -27,6 +27,7 @@ import {
switchMap,
takeUntil,
tap,
withLatestFrom,
} from "rxjs/operators";
import {
@@ -476,15 +477,13 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$
.pipe(
switchMap(() =>
combineLatest([this.route.queryParams, allCipherMap$, allCollections$, organization$]),
),
switchMap(() => this.route.queryParams),
withLatestFrom(allCipherMap$, allCollections$, organization$),
switchMap(async ([qParams, allCiphersMap, allCollections]) => {
const cipherId = getCipherIdFromParams(qParams);
if (!cipherId) {
return;
}
const cipher = allCiphersMap[cipherId];
const cipherCollections = allCollections.filter((c) =>
cipher.collectionIds.includes(c.id),