mirror of
https://github.com/bitwarden/browser
synced 2026-02-18 10:23:52 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
@@ -1309,6 +1309,13 @@ export default class MainBackground {
|
||||
// Only the "true" background should run migrations
|
||||
await this.stateService.init({ runMigrations: true });
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
// This is here instead of in in the InitService b/c we don't plan for
|
||||
// side effects to run in the Browser InitService.
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
|
||||
@@ -3,6 +3,9 @@ import { inject, Inject, Injectable } from "@angular/core";
|
||||
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -27,6 +30,9 @@ export class InitService {
|
||||
private themingService: AbstractThemingService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private viewCacheService: PopupViewCacheService,
|
||||
private configService: ConfigService,
|
||||
private encryptService: EncryptService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -34,6 +40,12 @@ export class InitService {
|
||||
return async () => {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
await this.viewCacheService.init();
|
||||
|
||||
@@ -4,8 +4,9 @@ import { switchMap, tap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { filterOutNullish, TaskService } from "@bitwarden/vault";
|
||||
|
||||
export const canAccessAtRiskPasswords: CanActivateFn = () => {
|
||||
const accountService = inject(AccountService);
|
||||
|
||||
@@ -4,9 +4,10 @@ import { RouterModule } from "@angular/router";
|
||||
import { map, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { AnchorLinkDirective, CalloutModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { filterOutNullish, SecurityTaskType, TaskService } from "@bitwarden/vault";
|
||||
|
||||
// TODO: This component will need to be reworked to use the new EndUserNotificationService in PM-10609
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
DefaultChangeLoginPasswordService,
|
||||
PasswordRepromptService,
|
||||
SecurityTask,
|
||||
SecurityTaskType,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||
|
||||
@@ -15,6 +15,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
@@ -28,10 +30,7 @@ import {
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
DefaultChangeLoginPasswordService,
|
||||
filterOutNullish,
|
||||
PasswordRepromptService,
|
||||
SecurityTaskType,
|
||||
TaskService,
|
||||
VaultCarouselModule,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.cipherService
|
||||
.failedToDecryptCiphers$(activeUserId)
|
||||
.pipe(
|
||||
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
|
||||
map((ciphers) => (ciphers ? ciphers.filter((c) => !c.isDeleted) : [])),
|
||||
filter((ciphers) => ciphers.length > 0),
|
||||
take(1),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
|
||||
@@ -33,19 +33,17 @@ import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cip
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DialogService,
|
||||
IconButtonModule,
|
||||
SearchModule,
|
||||
ToastService,
|
||||
CalloutModule,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
CopyCipherFieldService,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
@@ -95,7 +93,6 @@ type LoadAction =
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: BrowserViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ export class VaultPopupItemsService {
|
||||
this.cipherService.failedToDecryptCiphers$(userId),
|
||||
]),
|
||||
),
|
||||
map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]),
|
||||
map(([ciphers, failedToDecryptCiphers]) => [...(failedToDecryptCiphers || []), ...ciphers]),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
@@ -360,10 +359,10 @@ export class VaultPopupListFiltersService {
|
||||
switchMap((userId) => {
|
||||
// Observable of cipher views
|
||||
const cipherViews$ = this.cipherService.cipherViews$(userId).pipe(
|
||||
tap((cipherViews) => {
|
||||
this.cipherViews = Object.values(cipherViews);
|
||||
map((ciphers) => {
|
||||
this.cipherViews = ciphers ? Object.values(ciphers) : [];
|
||||
return this.cipherViews;
|
||||
}),
|
||||
map((ciphers) => Object.values(ciphers)),
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
|
||||
@@ -283,6 +283,7 @@ export class ServiceContainer {
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
ssoUrlService: SsoUrlService;
|
||||
masterPasswordApiService: MasterPasswordApiServiceAbstraction;
|
||||
bulkEncryptService: FallbackBulkEncryptService;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@@ -314,6 +315,7 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
true,
|
||||
);
|
||||
this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService);
|
||||
this.storageService = new LowdbStorageService(this.logService, null, p, false, true);
|
||||
this.secureStorageService = new NodeEnvSecureStorageService(
|
||||
this.storageService,
|
||||
@@ -686,7 +688,7 @@ export class ServiceContainer {
|
||||
this.stateService,
|
||||
this.autofillSettingsService,
|
||||
this.encryptService,
|
||||
new FallbackBulkEncryptService(this.encryptService),
|
||||
this.bulkEncryptService,
|
||||
this.cipherFileUploadService,
|
||||
this.configService,
|
||||
this.stateProvider,
|
||||
@@ -885,6 +887,12 @@ export class ServiceContainer {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.storageService.init();
|
||||
await this.stateService.init();
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
this.containerService.attachToGlobal(global);
|
||||
await this.i18nService.init();
|
||||
this.twoFactorService.init();
|
||||
|
||||
@@ -7,8 +7,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
@@ -49,6 +51,8 @@ export class InitService {
|
||||
private sshAgentService: SshAgentService,
|
||||
private autofillService: DesktopAutofillService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -59,6 +63,13 @@ export class InitService {
|
||||
this.nativeMessagingService.init();
|
||||
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
const setUserKeyInMemoryPromises = [];
|
||||
for (const userId of Object.keys(accounts) as UserId[]) {
|
||||
|
||||
@@ -282,14 +282,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary btn-submit"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
*ngIf="!disableSend"
|
||||
>
|
||||
<button type="submit" class="primary btn-submit" *ngIf="!disableSend">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span><i class="bwi bwi-save-changes bwi-lg bwi-fw" aria-hidden="true"></i></span>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.maxAccessCountReached">
|
||||
<i
|
||||
class="bwi bwi-ban"
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'maxAccessCountReached' | i18n }}"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -3561,5 +3561,8 @@
|
||||
},
|
||||
"changeAtRiskPassword": {
|
||||
"message": "Change at-risk password"
|
||||
},
|
||||
"move": {
|
||||
"message": "Move"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,17 +773,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="$any(form).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="$any(form).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="$any(form).loading">
|
||||
<span [hidden]="$any(form).loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(form).loading"
|
||||
@@ -797,10 +788,9 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="share()"
|
||||
appA11yTitle="{{ 'moveToOrganization' | i18n }}"
|
||||
*ngIf="editMode && cipher && !cipher.organizationId && !cloneMode"
|
||||
>
|
||||
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
{{ "move" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
#deleteBtn
|
||||
|
||||
@@ -54,17 +54,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -28,17 +28,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -21,17 +21,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<button type="submit" class="primary" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -58,15 +58,10 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="primary"
|
||||
appA11yTitle="{{ 'save' | i18n }}"
|
||||
[disabled]="form.loading || !canSave"
|
||||
*ngIf="organizations && organizations.length"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-save-changes bwi-lg bwi-fw"
|
||||
[hidden]="form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!form.loading"
|
||||
|
||||
@@ -3,16 +3,23 @@
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngFor="let field of cipher.fields; index as i"
|
||||
>
|
||||
<div class="row-main">
|
||||
<span
|
||||
*ngIf="field.type != fieldType.Linked"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, field.value)"
|
||||
>{{ field.name }}</span
|
||||
[id]="'customField-' + i"
|
||||
>
|
||||
<span *ngIf="field.type === fieldType.Linked" class="row-label">{{ field.name }}</span>
|
||||
{{ field.name }}
|
||||
</span>
|
||||
<span *ngIf="field.type === fieldType.Linked" class="row-label">
|
||||
{{ "cfTypeLinked" | i18n }}: {{ field.name }}
|
||||
</span>
|
||||
<div *ngIf="field.type === fieldType.Text">
|
||||
{{ field.value || " " }}
|
||||
</div>
|
||||
@@ -29,19 +36,14 @@
|
||||
></span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Boolean">
|
||||
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-square" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ field.value }}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="field.value === 'true'"
|
||||
disabled="true"
|
||||
[attr.aria-labelledby]="'customField-' + i"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
|
||||
<div class="icon icon-small">
|
||||
<i
|
||||
class="bwi bwi-link"
|
||||
aria-hidden="true"
|
||||
appA11yTitle="{{ 'linkedValue' | i18n }}"
|
||||
></i>
|
||||
<span class="sr-only">{{ "linkedValue" | i18n }}</span>
|
||||
</div>
|
||||
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -535,7 +535,7 @@
|
||||
*ngIf="u.canLaunch"
|
||||
(click)="launch(u)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -59,7 +59,7 @@ export function isEnterpriseOrgGuard(showError: boolean = true): CanActivateFn {
|
||||
content: { key: "onlyAvailableForEnterpriseOrganization" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
icon: "bwi-arrow-circle-up",
|
||||
icon: "bwi-plus-circle",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
|
||||
@@ -58,7 +58,7 @@ export function isPaidOrgGuard(): CanActivateFn {
|
||||
content: { key: "upgradeOrganizationCloseSecurityGapsDesc" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
icon: "bwi-arrow-circle-up",
|
||||
icon: "bwi-plus-circle",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</ng-container>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
icon="bwi-providers"
|
||||
icon="bwi-msp"
|
||||
[text]="'integrations' | i18n"
|
||||
route="integrations"
|
||||
*ngIf="integrationPageEnabled$ | async"
|
||||
|
||||
@@ -50,7 +50,7 @@ export class DeleteManagedMemberWarningService {
|
||||
key: "deleteManagedUserWarningDesc",
|
||||
},
|
||||
type: "danger",
|
||||
icon: "bwi-exclamation-circle",
|
||||
icon: "bwi-exclamation-triangle",
|
||||
acceptButtonText: { key: "continue" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{{ "keyConnectorPolicyRestriction" | i18n }}
|
||||
</bit-callout>
|
||||
|
||||
<bit-callout type="success" [title]="'prerequisite' | i18n" icon="bwi-lightbulb">
|
||||
<bit-callout type="info" [title]="'prerequisite' | i18n">
|
||||
{{ "accountRecoverySingleOrgRequirementDesc" | i18n }}
|
||||
</bit-callout>
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import mock, { MockProxy } from "jest-mock-extended/lib/Mock";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { ChangeEmailComponent } from "@bitwarden/web-vault/app/auth/settings/account/change-email.component";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
describe("ChangeEmailComponent", () => {
|
||||
let component: ChangeEmailComponent;
|
||||
let fixture: ComponentFixture<ChangeEmailComponent>;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let accountService: FakeAccountService;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
apiService = mock<ApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
accountService = mockAccountServiceWith("UserId" as UserId);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ChangeEmailComponent],
|
||||
imports: [ReactiveFormsModule, SharedModule],
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: MessagingService, useValue: mock<MessagingService>() },
|
||||
{ provide: KdfConfigService, useValue: kdfConfigService },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: FormBuilder, useClass: FormBuilder },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChangeEmailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("creates component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
beforeEach(() => {
|
||||
apiService.getTwoFactorProviders.mockResolvedValue({
|
||||
data: [{ type: TwoFactorProviderType.Email, enabled: true } as TwoFactorProviderResponse],
|
||||
} as ListResponse<TwoFactorProviderResponse>);
|
||||
});
|
||||
|
||||
it("initializes userId", async () => {
|
||||
await component.ngOnInit();
|
||||
expect(component.userId).toBe("UserId");
|
||||
});
|
||||
|
||||
it("errors if there is no active user", async () => {
|
||||
// clear active account
|
||||
await firstValueFrom(accountService.activeAccount$);
|
||||
accountService.activeAccountSubject.next(null);
|
||||
|
||||
await expect(() => component.ngOnInit()).rejects.toThrow("Null or undefined account");
|
||||
});
|
||||
|
||||
it("initializes showTwoFactorEmailWarning", async () => {
|
||||
await component.ngOnInit();
|
||||
expect(component.showTwoFactorEmailWarning).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("submit", () => {
|
||||
beforeEach(() => {
|
||||
component.formGroup.controls.step1.setValue({
|
||||
masterPassword: "password",
|
||||
newEmail: "test@example.com",
|
||||
});
|
||||
|
||||
keyService.getOrDeriveMasterKey
|
||||
.calledWith("password", "UserId")
|
||||
.mockResolvedValue("getOrDeriveMasterKey" as any);
|
||||
keyService.hashMasterKey
|
||||
.calledWith("password", "getOrDeriveMasterKey" as any)
|
||||
.mockResolvedValue("existingHash");
|
||||
});
|
||||
|
||||
it("throws if userId is null on submit", async () => {
|
||||
component.userId = undefined;
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Can't find user");
|
||||
});
|
||||
|
||||
describe("step 1", () => {
|
||||
it("does not submit if step 1 is invalid", async () => {
|
||||
component.formGroup.controls.step1.setValue({
|
||||
masterPassword: "",
|
||||
newEmail: "",
|
||||
});
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmailToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends email token in step 1 if tokenSent is false", async () => {
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmailToken).toHaveBeenCalledWith({
|
||||
newEmail: "test@example.com",
|
||||
masterPasswordHash: "existingHash",
|
||||
});
|
||||
// should activate step 2
|
||||
expect(component.tokenSent).toBe(true);
|
||||
expect(component.formGroup.controls.step1.disabled).toBe(true);
|
||||
expect(component.formGroup.controls.token.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("step 2", () => {
|
||||
beforeEach(() => {
|
||||
component.tokenSent = true;
|
||||
component.formGroup.controls.step1.disable();
|
||||
component.formGroup.controls.token.enable();
|
||||
component.formGroup.controls.token.setValue("token");
|
||||
|
||||
kdfConfigService.getKdfConfig$
|
||||
.calledWith("UserId" as any)
|
||||
.mockReturnValue(of("kdfConfig" as any));
|
||||
keyService.userKey$.calledWith("UserId" as any).mockReturnValue(of("userKey" as any));
|
||||
|
||||
keyService.makeMasterKey
|
||||
.calledWith("password", "test@example.com", "kdfConfig" as any)
|
||||
.mockResolvedValue("newMasterKey" as any);
|
||||
keyService.hashMasterKey
|
||||
.calledWith("password", "newMasterKey" as any)
|
||||
.mockResolvedValue("newMasterKeyHash");
|
||||
|
||||
// Important: make sure this is called with new master key, not existing
|
||||
keyService.encryptUserKeyWithMasterKey
|
||||
.calledWith("newMasterKey" as any, "userKey" as any)
|
||||
.mockResolvedValue(["userKey" as any, { encryptedString: "newEncryptedUserKey" } as any]);
|
||||
});
|
||||
|
||||
it("does not post email if token is missing on submit", async () => {
|
||||
component.formGroup.controls.token.setValue("");
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if kdfConfig is missing on submit", async () => {
|
||||
kdfConfigService.getKdfConfig$.mockReturnValue(of(null));
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Missing kdf config");
|
||||
});
|
||||
|
||||
it("throws if userKey can't be found", async () => {
|
||||
keyService.userKey$.mockReturnValue(of(null));
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Can't find UserKey");
|
||||
});
|
||||
|
||||
it("throws if encryptedUserKey is missing", async () => {
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(["userKey" as any, null as any]);
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Missing Encrypted User Key");
|
||||
});
|
||||
|
||||
it("submits if step 2 is valid", async () => {
|
||||
await component.submit();
|
||||
|
||||
// validate that hashes are correct
|
||||
expect(apiService.postEmail).toHaveBeenCalledWith({
|
||||
masterPasswordHash: "existingHash",
|
||||
newMasterPasswordHash: "newMasterKeyHash",
|
||||
token: "token",
|
||||
newEmail: "test@example.com",
|
||||
key: "newEncryptedUserKey",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.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";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -22,8 +21,9 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
export class ChangeEmailComponent implements OnInit {
|
||||
tokenSent = false;
|
||||
showTwoFactorEmailWarning = false;
|
||||
userId: UserId | undefined;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
formGroup = this.formBuilder.group({
|
||||
step1: this.formBuilder.group({
|
||||
masterPassword: ["", [Validators.required]],
|
||||
newEmail: ["", [Validators.required, Validators.email]],
|
||||
@@ -32,26 +32,30 @@ export class ChangeEmailComponent implements OnInit {
|
||||
});
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private keyService: KeyService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
|
||||
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(
|
||||
(p) => p.type === TwoFactorProviderType.Email && p.enabled,
|
||||
);
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
submit = async () => {
|
||||
if (this.userId == null) {
|
||||
throw new Error("Can't find user");
|
||||
}
|
||||
|
||||
// This form has multiple steps, so we need to mark all the groups as touched.
|
||||
this.formGroup.controls.step1.markAllAsTouched();
|
||||
|
||||
@@ -65,37 +69,54 @@ export class ChangeEmailComponent implements OnInit {
|
||||
}
|
||||
|
||||
const step1Value = this.formGroup.controls.step1.value;
|
||||
const newEmail = step1Value.newEmail.trim().toLowerCase();
|
||||
const newEmail = step1Value.newEmail?.trim().toLowerCase();
|
||||
const masterPassword = step1Value.masterPassword;
|
||||
|
||||
if (newEmail == null || masterPassword == null) {
|
||||
throw new Error("Missing email or password");
|
||||
}
|
||||
|
||||
const existingHash = await this.keyService.hashMasterKey(
|
||||
masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(masterPassword, this.userId),
|
||||
);
|
||||
|
||||
if (!this.tokenSent) {
|
||||
const request = new EmailTokenRequest();
|
||||
request.newEmail = newEmail;
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(step1Value.masterPassword),
|
||||
);
|
||||
request.masterPasswordHash = existingHash;
|
||||
await this.apiService.postEmailToken(request);
|
||||
this.activateStep2();
|
||||
} else {
|
||||
const token = this.formGroup.value.token;
|
||||
if (token == null) {
|
||||
throw new Error("Missing token");
|
||||
}
|
||||
const request = new EmailRequest();
|
||||
request.token = this.formGroup.value.token;
|
||||
request.token = token;
|
||||
request.newEmail = newEmail;
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(step1Value.masterPassword),
|
||||
);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
step1Value.masterPassword,
|
||||
newEmail,
|
||||
kdfConfig,
|
||||
);
|
||||
request.masterPasswordHash = existingHash;
|
||||
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId));
|
||||
if (kdfConfig == null) {
|
||||
throw new Error("Missing kdf config");
|
||||
}
|
||||
const newMasterKey = await this.keyService.makeMasterKey(masterPassword, newEmail, kdfConfig);
|
||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(this.userId));
|
||||
if (userKey == null) {
|
||||
throw new Error("Can't find UserKey");
|
||||
}
|
||||
const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey, userKey);
|
||||
const encryptedUserKey = newUserKey[1]?.encryptedString;
|
||||
if (encryptedUserKey == null) {
|
||||
throw new Error("Missing Encrypted User Key");
|
||||
}
|
||||
request.key = encryptedUserKey;
|
||||
|
||||
await this.apiService.postEmail(request);
|
||||
this.reset();
|
||||
|
||||
@@ -17,8 +17,9 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { ChangeLoginPasswordService, TaskService } from "@bitwarden/vault";
|
||||
import { ChangeLoginPasswordService } from "@bitwarden/vault";
|
||||
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
@@ -55,6 +56,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
{ provide: DialogRef, useValue: { close } },
|
||||
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: TaskService, useValue: mock<TaskService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(EmergencyViewDialogComponent, {
|
||||
@@ -71,10 +73,6 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
},
|
||||
add: {
|
||||
providers: [
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mock<TaskService>(),
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
|
||||
@@ -39,7 +37,6 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -965,9 +965,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
|
||||
@@ -222,9 +222,8 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
class="tw-mr-2"
|
||||
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
||||
<i class="bwi bwi-file-text" aria-hidden="true"></i
|
||||
></a>
|
||||
<a
|
||||
bitLink
|
||||
@@ -34,7 +34,7 @@
|
||||
{{ "paid" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="!i.paid">
|
||||
<i class="bwi bwi-exclamation-circle tw-text-muted" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "unpaid" | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
@@ -59,7 +59,7 @@
|
||||
class="tw-mr-2"
|
||||
appA11yTitle="{{ 'downloadInvoice' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-file-pdf" aria-hidden="true"></i
|
||||
<i class="bwi bwi-file-text" aria-hidden="true"></i
|
||||
></a>
|
||||
<a
|
||||
bitLink
|
||||
@@ -78,7 +78,7 @@
|
||||
{{ "paid" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="!i.paid">
|
||||
<i class="bwi bwi-exclamation-circle tw-text-muted" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-error tw-text-muted" aria-hidden="true"></i>
|
||||
{{ "unpaid" | i18n }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -31,7 +31,7 @@ export class BillingHistoryComponent {
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
case PaymentMethodType.WireTransfer:
|
||||
return ["bwi-bank"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.BitPay:
|
||||
return ["bwi-bitcoin text-warning"];
|
||||
case PaymentMethodType.PayPal:
|
||||
|
||||
@@ -237,9 +237,8 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
return ["bwi-billing"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*ngIf="showBankAccount"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-billing" aria-hidden="true"></i>
|
||||
{{ "bankAccount" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
|
||||
@@ -499,45 +499,45 @@ export class EventService {
|
||||
|
||||
switch (ev.deviceType) {
|
||||
case DeviceType.Android:
|
||||
return ["bwi-android", this.i18nService.t("mobile") + " - Android"];
|
||||
return ["bwi-mobile", this.i18nService.t("mobile") + " - Android"];
|
||||
case DeviceType.iOS:
|
||||
return ["bwi-apple", this.i18nService.t("mobile") + " - iOS"];
|
||||
return ["bwi-mobile", this.i18nService.t("mobile") + " - iOS"];
|
||||
case DeviceType.UWP:
|
||||
return ["bwi-windows", this.i18nService.t("mobile") + " - Windows"];
|
||||
return ["bwi-mobile", this.i18nService.t("mobile") + " - Windows"];
|
||||
case DeviceType.ChromeExtension:
|
||||
return ["bwi-chrome", this.i18nService.t("extension") + " - Chrome"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Chrome"];
|
||||
case DeviceType.FirefoxExtension:
|
||||
return ["bwi-firefox", this.i18nService.t("extension") + " - Firefox"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Firefox"];
|
||||
case DeviceType.OperaExtension:
|
||||
return ["bwi-opera", this.i18nService.t("extension") + " - Opera"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Opera"];
|
||||
case DeviceType.EdgeExtension:
|
||||
return ["bwi-edge", this.i18nService.t("extension") + " - Edge"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Edge"];
|
||||
case DeviceType.VivaldiExtension:
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Vivaldi"];
|
||||
case DeviceType.SafariExtension:
|
||||
return ["bwi-safari", this.i18nService.t("extension") + " - Safari"];
|
||||
return ["bwi-puzzle", this.i18nService.t("extension") + " - Safari"];
|
||||
case DeviceType.WindowsDesktop:
|
||||
return ["bwi-windows", this.i18nService.t("desktop") + " - Windows"];
|
||||
return ["bwi-desktop", this.i18nService.t("desktop") + " - Windows"];
|
||||
case DeviceType.MacOsDesktop:
|
||||
return ["bwi-apple", this.i18nService.t("desktop") + " - macOS"];
|
||||
return ["bwi-desktop", this.i18nService.t("desktop") + " - macOS"];
|
||||
case DeviceType.LinuxDesktop:
|
||||
return ["bwi-linux", this.i18nService.t("desktop") + " - Linux"];
|
||||
return ["bwi-desktop", this.i18nService.t("desktop") + " - Linux"];
|
||||
case DeviceType.ChromeBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Chrome"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Chrome"];
|
||||
case DeviceType.FirefoxBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Firefox"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Firefox"];
|
||||
case DeviceType.OperaBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Opera"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Opera"];
|
||||
case DeviceType.SafariBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Safari"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Safari"];
|
||||
case DeviceType.VivaldiBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Vivaldi"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Vivaldi"];
|
||||
case DeviceType.EdgeBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - Edge"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - Edge"];
|
||||
case DeviceType.IEBrowser:
|
||||
return ["bwi-globe", this.i18nService.t("webVault") + " - IE"];
|
||||
return ["bwi-browser", this.i18nService.t("webVault") + " - IE"];
|
||||
case DeviceType.Server:
|
||||
return ["bwi-server", this.i18nService.t("server")];
|
||||
return ["bwi-user-monitor", this.i18nService.t("server")];
|
||||
case DeviceType.WindowsCLI:
|
||||
return ["bwi-cli", this.i18nService.t("cli") + " - Windows"];
|
||||
case DeviceType.MacOsCLI:
|
||||
@@ -546,7 +546,7 @@ export class EventService {
|
||||
return ["bwi-cli", this.i18nService.t("cli") + " - Linux"];
|
||||
case DeviceType.UnknownBrowser:
|
||||
return [
|
||||
"bwi-globe",
|
||||
"bwi-browser",
|
||||
this.i18nService.t("webVault") + " - " + this.i18nService.t("unknown"),
|
||||
];
|
||||
default:
|
||||
|
||||
@@ -7,8 +7,10 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abstractions/bulk-encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
@@ -37,6 +39,8 @@ export class InitService {
|
||||
private accountService: AccountService,
|
||||
private versionService: VersionService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@@ -45,6 +49,13 @@ export class InitService {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.stateService.init();
|
||||
|
||||
this.configService.serverConfig$.subscribe((newConfig) => {
|
||||
if (newConfig != null) {
|
||||
this.encryptService.onServerConfigChange(newConfig);
|
||||
this.bulkEncryptService.onServerConfigChange(newConfig);
|
||||
}
|
||||
});
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (activeAccount) {
|
||||
// If there is an active account, we must await the process of setting the user key in memory
|
||||
|
||||
@@ -59,22 +59,22 @@
|
||||
<td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-cog"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="secondary"
|
||||
[bitMenuTriggerFor]="appListDropdown"
|
||||
class="tw-border-0 tw-bg-transparent tw-p-0"
|
||||
></button>
|
||||
<bit-menu #appListDropdown>
|
||||
<a href="#" bitMenuItem appStopClick (click)="toggleExcluded(d)" *ngIf="!d.excluded">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||
{{ "exclude" | i18n }}
|
||||
</a>
|
||||
<a href="#" bitMenuItem appStopClick (click)="toggleExcluded(d)" *ngIf="d.excluded">
|
||||
<i class="bwi bwi-fw bwi-plus" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
{{ "include" | i18n }}
|
||||
</a>
|
||||
<a href="#" bitMenuItem appStopClick (click)="customize(d)">
|
||||
<i class="bwi bwi-fw bwi-cut" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "customize" | i18n }}
|
||||
</a>
|
||||
</bit-menu>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<details #details class="tw-rounded-sm tw-bg-background-alt tw-text-main" (toggle)="toggle()" open>
|
||||
<summary class="tw-list-none tw-p-2 tw-px-4">
|
||||
<div class="tw-flex tw-select-none tw-items-center tw-gap-4">
|
||||
<i class="bwi bwi-dashboard tw-text-3xl tw-text-primary-600" aria-hidden="true"></i>
|
||||
<div class="tw-text-lg">{{ title }}</div>
|
||||
<bit-progress class="tw-flex-1" [showText]="false" [barWidth]="barWidth"></bit-progress>
|
||||
<span *ngIf="tasks.length > 0; else spinner">
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="s.maxAccessCountReached">
|
||||
<i
|
||||
class="bwi bwi-ban"
|
||||
class="bwi bwi-exclamation-triangle"
|
||||
appStopProp
|
||||
title="{{ 'maxAccessCountReached' | i18n }}"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -15,12 +15,14 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
let fixture: ComponentFixture<BrowserExtensionPromptComponent>;
|
||||
let component: BrowserExtensionPromptComponent;
|
||||
const start = jest.fn();
|
||||
const openExtension = jest.fn();
|
||||
const pageState$ = new BehaviorSubject(BrowserPromptState.Loading);
|
||||
const setAttribute = jest.fn();
|
||||
const getAttribute = jest.fn().mockReturnValue("width=1010");
|
||||
|
||||
beforeEach(async () => {
|
||||
start.mockClear();
|
||||
openExtension.mockClear();
|
||||
setAttribute.mockClear();
|
||||
getAttribute.mockClear();
|
||||
|
||||
@@ -39,7 +41,7 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
providers: [
|
||||
{
|
||||
provide: BrowserExtensionPromptService,
|
||||
useValue: { start, pageState$ },
|
||||
useValue: { start, openExtension, pageState$ },
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
@@ -83,6 +85,15 @@ describe("BrowserExtensionPromptComponent", () => {
|
||||
const errorText = fixture.debugElement.query(By.css("p")).nativeElement;
|
||||
expect(errorText.textContent.trim()).toBe("openingExtensionError");
|
||||
});
|
||||
|
||||
it("opens extension on button click", () => {
|
||||
const button = fixture.debugElement.query(By.css("button")).nativeElement;
|
||||
|
||||
button.click();
|
||||
|
||||
expect(openExtension).toHaveBeenCalledTimes(1);
|
||||
expect(openExtension).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("success state", () => {
|
||||
|
||||
@@ -61,6 +61,6 @@ export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
openExtension(): void {
|
||||
this.browserExtensionPromptService.openExtension();
|
||||
this.browserExtensionPromptService.openExtension(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherAttachmentsComponent,
|
||||
CipherFormComponent,
|
||||
CipherFormConfig,
|
||||
CipherFormGenerationService,
|
||||
@@ -47,8 +46,6 @@ import {
|
||||
CipherViewComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
@@ -132,17 +129,14 @@ export enum VaultItemDialogResult {
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CipherFormModule,
|
||||
CipherAttachmentsComponent,
|
||||
AsyncActionsModule,
|
||||
ItemModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService },
|
||||
RoutedVaultFilterService,
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-share-square" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-fw bwi-external-link" aria-hidden="true"></i>
|
||||
{{ "launch" | i18n }}
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
@@ -52,10 +52,12 @@
|
||||
{{ "permission" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-12 tw-text-right">
|
||||
@let featureFlaggedDisable =
|
||||
(limitItemDeletion$ | async) ? (disableMenu$ | async) : disableMenu;
|
||||
<button
|
||||
[disabled]="disabled || isEmpty || disableMenu"
|
||||
[disabled]="disabled || isEmpty || featureFlaggedDisable"
|
||||
[bitMenuTriggerFor]="headerMenu"
|
||||
[attr.title]="disableMenu ? ('missingPermissions' | i18n) : ''"
|
||||
[attr.title]="featureFlaggedDisable ? ('missingPermissions' | i18n) : ''"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
type="button"
|
||||
|
||||
@@ -85,6 +85,7 @@ export class VaultItemsComponent {
|
||||
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
||||
protected canDeleteSelected$: Observable<boolean>;
|
||||
protected canRestoreSelected$: Observable<boolean>;
|
||||
protected disableMenu$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
@@ -140,6 +141,20 @@ export class VaultItemsComponent {
|
||||
}),
|
||||
map((canRestore) => canRestore && this.showBulkTrashOptions),
|
||||
);
|
||||
|
||||
this.disableMenu$ = combineLatest([this.limitItemDeletion$, this.canDeleteSelected$]).pipe(
|
||||
map(([enabled, canDelete]) => {
|
||||
if (enabled) {
|
||||
return (
|
||||
!this.bulkMoveAllowed &&
|
||||
!this.showAssignToCollections() &&
|
||||
!canDelete &&
|
||||
!this.showBulkEditCollectionAccess
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
get showExtraColumn() {
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
(click)="launch(u)"
|
||||
[disabled]="!u.canLaunch"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -46,12 +46,10 @@
|
||||
bitMenuItem
|
||||
(click)="unlinkSso(organization)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-chain-broken" aria-hidden="true"></i>
|
||||
{{ "unlinkSso" | i18n }}
|
||||
</button>
|
||||
<ng-template #linkSso>
|
||||
<button type="button" bitMenuItem (click)="handleLinkSso(organization)">
|
||||
<i class="bwi bwi-fw bwi-link" aria-hidden="true"></i>
|
||||
{{ "linkSso" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@@ -20,9 +20,10 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { ChangeLoginPasswordService, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
import { ChangeLoginPasswordService } from "@bitwarden/vault";
|
||||
|
||||
import { ViewCipherDialogParams, ViewCipherDialogResult, ViewComponent } from "./view.component";
|
||||
|
||||
@@ -83,12 +84,12 @@ describe("ViewComponent", () => {
|
||||
canDeleteCipher$: jest.fn().mockReturnValue(true),
|
||||
},
|
||||
},
|
||||
{ provide: TaskService, useValue: mock<TaskService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(ViewComponent, {
|
||||
remove: {
|
||||
providers: [
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: PlatformUtilsService, useValue: PlatformUtilsService },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
@@ -98,10 +99,6 @@ describe("ViewComponent", () => {
|
||||
},
|
||||
add: {
|
||||
providers: [
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mock<TaskService>(),
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit } from "@angular/core";
|
||||
import { Observable, firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { CipherViewComponent, DefaultTaskService, TaskService } from "@bitwarden/vault";
|
||||
import { CipherViewComponent } from "@bitwarden/vault";
|
||||
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
import { WebVaultPremiumUpgradePromptService } from "../services/web-premium-upgrade-prompt.service";
|
||||
@@ -74,7 +74,6 @@ export interface ViewCipherDialogCloseResult {
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
],
|
||||
})
|
||||
export class ViewComponent implements OnInit {
|
||||
|
||||
@@ -169,5 +169,32 @@ describe("BrowserExtensionPromptService", () => {
|
||||
pageTitle: { key: "somethingWentWrong" },
|
||||
});
|
||||
});
|
||||
|
||||
it("sets manual open state when open extension is called", (done) => {
|
||||
service.openExtension(true);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
service.pageState$.subscribe((state) => {
|
||||
expect(state).toBe(BrowserPromptState.ManualOpen);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows success state when extension auto opens", (done) => {
|
||||
service.openExtension(true);
|
||||
|
||||
jest.advanceTimersByTime(500); // don't let timeout occur
|
||||
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", { data: { command: VaultMessages.PopupOpened } }),
|
||||
);
|
||||
|
||||
service.pageState$.subscribe((state) => {
|
||||
expect(state).toBe(BrowserPromptState.Success);
|
||||
expect(service["extensionCheckTimeout"]).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,8 +54,18 @@ export class BrowserExtensionPromptService {
|
||||
}
|
||||
|
||||
/** Post a message to the extension to open */
|
||||
openExtension() {
|
||||
openExtension(setManualErrorTimeout = false) {
|
||||
window.postMessage({ command: VaultMessages.OpenPopup });
|
||||
|
||||
// Optionally, configure timeout to show the manual open error state if
|
||||
// the extension does not open within one second.
|
||||
if (setManualErrorTimeout) {
|
||||
this.clearExtensionCheckTimeout();
|
||||
|
||||
this.extensionCheckTimeout = window.setTimeout(() => {
|
||||
this.setErrorState(BrowserPromptState.ManualOpen);
|
||||
}, 750);
|
||||
}
|
||||
}
|
||||
|
||||
/** Send message checking for the browser extension */
|
||||
|
||||
Reference in New Issue
Block a user