mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 10:13:31 +00:00
[PM-5189] Merging work done for pm-8518
This commit is contained in:
@@ -136,6 +136,9 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement
|
||||
import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state";
|
||||
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||
/* eslint-enable import/no-restricted-paths */
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
@@ -166,8 +169,6 @@ import { CollectionService as CollectionServiceAbstraction } from "@bitwarden/co
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { InternalFolderService as InternalFolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync-notifier.service.abstraction";
|
||||
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { VaultSettingsService as VaultSettingsServiceAbstraction } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
@@ -176,8 +177,6 @@ import { CollectionService } from "@bitwarden/common/vault/services/collection.s
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
|
||||
import {
|
||||
@@ -268,7 +267,7 @@ export default class MainBackground {
|
||||
collectionService: CollectionServiceAbstraction;
|
||||
vaultTimeoutService: VaultTimeoutService;
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction;
|
||||
syncService: SyncServiceAbstraction;
|
||||
syncService: SyncService;
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction;
|
||||
passwordStrengthService: PasswordStrengthServiceAbstraction;
|
||||
totpService: TotpServiceAbstraction;
|
||||
@@ -306,7 +305,6 @@ export default class MainBackground {
|
||||
policyApiService: PolicyApiServiceAbstraction;
|
||||
sendApiService: SendApiServiceAbstraction;
|
||||
userVerificationApiService: UserVerificationApiServiceAbstraction;
|
||||
syncNotifierService: SyncNotifierServiceAbstraction;
|
||||
fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction;
|
||||
fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction;
|
||||
fido2ClientService: Fido2ClientServiceAbstraction;
|
||||
@@ -638,7 +636,6 @@ export default class MainBackground {
|
||||
this.i18nService,
|
||||
this.stateProvider,
|
||||
);
|
||||
this.syncNotifierService = new SyncNotifierService();
|
||||
|
||||
this.autofillSettingsService = new AutofillSettingsService(
|
||||
this.stateProvider,
|
||||
@@ -827,7 +824,7 @@ export default class MainBackground {
|
||||
messageListener,
|
||||
);
|
||||
} else {
|
||||
this.syncService = new SyncService(
|
||||
this.syncService = new DefaultSyncService(
|
||||
this.masterPasswordService,
|
||||
this.accountService,
|
||||
this.apiService,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<div class="tw-flex tw-justify-between tw-items-end tw-gap-1 tw-px-1 tw-pb-1">
|
||||
<div class="tw-flex tw-items-center tw-gap-1">
|
||||
<h2 bitTypography="h6" noMargin class="tw-mb-0 tw-text-headers">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<ng-content select="[slot=title-suffix]"></ng-content>
|
||||
</div>
|
||||
<div class="tw-text-muted has-[button]:-tw-mb-1">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { TypographyModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "popup-section-header",
|
||||
templateUrl: "./popup-section-header.component.html",
|
||||
imports: [TypographyModule],
|
||||
})
|
||||
export class PopupSectionHeaderComponent {
|
||||
@Input() title: string;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
|
||||
import {
|
||||
CardComponent,
|
||||
IconButtonModule,
|
||||
SectionComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PopupSectionHeaderComponent } from "./popup-section-header.component";
|
||||
|
||||
export default {
|
||||
title: "Browser/Popup Section Header",
|
||||
component: PopupSectionHeaderComponent,
|
||||
args: {
|
||||
title: "Title",
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [SectionComponent, CardComponent, TypographyModule, IconButtonModule],
|
||||
}),
|
||||
],
|
||||
} as Meta<PopupSectionHeaderComponent>;
|
||||
|
||||
type Story = StoryObj<PopupSectionHeaderComponent>;
|
||||
|
||||
export const OnlyTitle: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<popup-section-header [title]="title"></popup-section-header>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
title: "Only Title",
|
||||
},
|
||||
};
|
||||
|
||||
export const TrailingText: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<popup-section-header [title]="title">
|
||||
<span bitTypography="body2" slot="end">13</span>
|
||||
</popup-section-header>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
title: "Trailing Text",
|
||||
},
|
||||
};
|
||||
|
||||
export const TailingIcon: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<popup-section-header [title]="title">
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</popup-section-header>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
title: "Trailing Icon",
|
||||
},
|
||||
};
|
||||
|
||||
export const TitleSuffix: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<popup-section-header [title]="title">
|
||||
<button bitIconButton="bwi-refresh" size="small" slot="title-suffix"></button>
|
||||
</popup-section-header>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
title: "Title Suffix",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSections: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<bit-section>
|
||||
<popup-section-header title="Section 1">
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</popup-section-header>
|
||||
<bit-card>
|
||||
<h3 bitTypography="h3">Card 1 Content</h3>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<popup-section-header title="Section 2">
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</popup-section-header>
|
||||
<bit-card>
|
||||
<h3 bitTypography="h3">Card 2 Content</h3>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -46,7 +46,6 @@ import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.comp
|
||||
import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
|
||||
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
import { PopupSectionHeaderComponent } from "../platform/popup/popup-section-header/popup-section-header.component";
|
||||
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
|
||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||
import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component";
|
||||
@@ -119,7 +118,6 @@ import "../platform/popup/locales";
|
||||
PopupFooterComponent,
|
||||
PopupHeaderComponent,
|
||||
UserVerificationDialogComponent,
|
||||
PopupSectionHeaderComponent,
|
||||
CurrentAccountComponent,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
@@ -80,11 +80,11 @@ import {
|
||||
} from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Used for dependency injection
|
||||
import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
@@ -8,17 +8,19 @@
|
||||
></app-vault-list-items-container>
|
||||
<ng-container *ngIf="showEmptyAutofillTip$ | async">
|
||||
<bit-section>
|
||||
<popup-section-header [title]="'autofillSuggestions' | i18n">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ "autofillSuggestions" | i18n }}
|
||||
</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
size="small"
|
||||
slot="title-suffix"
|
||||
type="button"
|
||||
[appA11yTitle]="'refresh' | i18n"
|
||||
(click)="refreshCurrentTab()"
|
||||
></button>
|
||||
</popup-section-header>
|
||||
</bit-section-header>
|
||||
<span class="tw-text-muted tw-px-1" bitTypography="body2">{{
|
||||
"autofillSuggestionsTip" | i18n
|
||||
}}</span>
|
||||
|
||||
@@ -3,10 +3,14 @@ import { Component } from "@angular/core";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { IconButtonModule, SectionComponent, TypographyModule } from "@bitwarden/components";
|
||||
import {
|
||||
IconButtonModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component";
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||
import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component";
|
||||
@@ -19,7 +23,7 @@ import { VaultListItemsContainerComponent } from "../vault-list-items-container/
|
||||
TypographyModule,
|
||||
VaultListItemsContainerComponent,
|
||||
JslibModule,
|
||||
PopupSectionHeaderComponent,
|
||||
SectionHeaderComponent,
|
||||
IconButtonModule,
|
||||
],
|
||||
selector: "app-autofill-vault-list-items",
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<bit-section *ngIf="ciphers?.length > 0">
|
||||
<popup-section-header [title]="title">
|
||||
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<button
|
||||
*ngIf="showRefresh"
|
||||
bitIconButton="bwi-refresh"
|
||||
type="button"
|
||||
size="small"
|
||||
slot="title-suffix"
|
||||
(click)="onRefresh.emit()"
|
||||
[appA11yTitle]="'refresh' | i18n"
|
||||
></button>
|
||||
</popup-section-header>
|
||||
<span bitTypography="body2" slot="end">{{ ciphers.length }}</span>
|
||||
</bit-section-header>
|
||||
<bit-item-group>
|
||||
<bit-item *ngFor="let cipher of ciphers">
|
||||
<a
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component";
|
||||
import { PopupCipherView } from "../../../views/popup-cipher.view";
|
||||
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
|
||||
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
|
||||
@@ -28,7 +28,7 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
|
||||
SectionComponent,
|
||||
TypographyModule,
|
||||
JslibModule,
|
||||
PopupSectionHeaderComponent,
|
||||
SectionHeaderComponent,
|
||||
RouterLink,
|
||||
ItemCopyActionsComponent,
|
||||
ItemMoreOptionsComponent,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Output, EventEmitter } from "@angular/core";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { Subject, debounceTime } from "rxjs";
|
||||
import { Subject, Subscription, debounceTime, filter } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
|
||||
const SearchTextDebounceInterval = 200;
|
||||
|
||||
@Component({
|
||||
@@ -17,19 +19,34 @@ const SearchTextDebounceInterval = 200;
|
||||
})
|
||||
export class VaultV2SearchComponent {
|
||||
searchText: string;
|
||||
@Output() searchTextChanged = new EventEmitter<string>();
|
||||
|
||||
private searchText$ = new Subject<string>();
|
||||
|
||||
constructor() {
|
||||
this.searchText$
|
||||
.pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed())
|
||||
.subscribe((data) => {
|
||||
this.searchTextChanged.emit(data);
|
||||
});
|
||||
constructor(private vaultPopupItemsService: VaultPopupItemsService) {
|
||||
this.subscribeToLatestSearchText();
|
||||
this.subscribeToApplyFilter();
|
||||
}
|
||||
|
||||
onSearchTextChanged() {
|
||||
this.searchText$.next(this.searchText);
|
||||
}
|
||||
|
||||
subscribeToLatestSearchText(): Subscription {
|
||||
return this.vaultPopupItemsService.latestSearchText$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
filter((data) => !!data),
|
||||
)
|
||||
.subscribe((text) => {
|
||||
this.searchText = text;
|
||||
});
|
||||
}
|
||||
|
||||
subscribeToApplyFilter(): Subscription {
|
||||
return this.searchText$
|
||||
.pipe(debounceTime(SearchTextDebounceInterval), takeUntilDestroyed())
|
||||
.subscribe((data) => {
|
||||
this.vaultPopupItemsService.applyFilter(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
|
||||
@@ -9,8 +9,8 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@@ -22,18 +22,15 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!(showEmptyState$ | async)">
|
||||
<div class="tw-fixed">
|
||||
<app-vault-v2-search (searchTextChanged)="handleSearchTextChange($event)">
|
||||
</app-vault-v2-search>
|
||||
<app-vault-v2-search> </app-vault-v2-search>
|
||||
|
||||
<app-vault-list-filters></app-vault-list-filters>
|
||||
</div>
|
||||
<app-vault-list-filters></app-vault-list-filters>
|
||||
|
||||
<div
|
||||
*ngIf="(showNoResultsState$ | async) && !(showDeactivatedOrg$ | async)"
|
||||
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
|
||||
class="tw-flex tw-flex-col tw-justify-center tw-h-auto tw-pt-12"
|
||||
>
|
||||
<bit-no-items>
|
||||
<bit-no-items [icon]="noResultsIcon">
|
||||
<ng-container slot="title">{{ "noItemsMatchSearch" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "clearFiltersOrTryAnother" | i18n }}</ng-container>
|
||||
</bit-no-items>
|
||||
@@ -41,7 +38,7 @@
|
||||
|
||||
<div
|
||||
*ngIf="showDeactivatedOrg$ | async"
|
||||
class="tw-flex tw-flex-col tw-h-full tw-justify-center"
|
||||
class="tw-flex tw-flex-col tw-justify-center tw-h-auto tw-pt-12"
|
||||
>
|
||||
<bit-no-items [icon]="deactivatedIcon">
|
||||
<ng-container slot="title">{{ "organizationIsDeactivated" | i18n }}</ng-container>
|
||||
|
||||
@@ -44,6 +44,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
|
||||
protected vaultIcon = Icons.Vault;
|
||||
protected deactivatedIcon = Icons.DeactivatedOrg;
|
||||
protected noResultsIcon = Icons.NoResults;
|
||||
|
||||
constructor(
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
@@ -54,10 +55,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
handleSearchTextChange(searchText: string) {
|
||||
this.vaultPopupItemsService.applyFilter(searchText);
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
// TODO: Add currently filtered organization to query params if available
|
||||
void this.router.navigate(["/add-cipher"], {});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductType } from "@bitwarden/common/enums";
|
||||
import { ObservableTracker } from "@bitwarden/common/spec";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
@@ -50,7 +51,8 @@ describe("VaultPopupItemsService", () => {
|
||||
cipherList[3].favorite = true;
|
||||
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList);
|
||||
cipherServiceMock.ciphers$ = new BehaviorSubject(null).asObservable();
|
||||
cipherServiceMock.ciphers$ = new BehaviorSubject(null);
|
||||
cipherServiceMock.localData$ = new BehaviorSubject(null);
|
||||
searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers);
|
||||
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
|
||||
ciphers.filter((c) => ["0", "1"].includes(c.id)),
|
||||
@@ -123,6 +125,34 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should update cipher list when cipherService.ciphers$ emits", async () => {
|
||||
const tracker = new ObservableTracker(service.autoFillCiphers$);
|
||||
|
||||
await tracker.expectEmission();
|
||||
|
||||
(cipherServiceMock.ciphers$ as BehaviorSubject<any>).next(null);
|
||||
|
||||
await tracker.expectEmission();
|
||||
|
||||
// Should only emit twice
|
||||
expect(tracker.emissions.length).toBe(2);
|
||||
await expect(tracker.pauseUntilReceived(3)).rejects.toThrow("Timeout exceeded");
|
||||
});
|
||||
|
||||
it("should update cipher list when cipherService.localData$ emits", async () => {
|
||||
const tracker = new ObservableTracker(service.autoFillCiphers$);
|
||||
|
||||
await tracker.expectEmission();
|
||||
|
||||
(cipherServiceMock.localData$ as BehaviorSubject<any>).next(null);
|
||||
|
||||
await tracker.expectEmission();
|
||||
|
||||
// Should only emit twice
|
||||
expect(tracker.emissions.length).toBe(2);
|
||||
await expect(tracker.pauseUntilReceived(3)).rejects.toThrow("Timeout exceeded");
|
||||
});
|
||||
|
||||
describe("autoFillCiphers$", () => {
|
||||
it("should return empty array if there is no current tab", (done) => {
|
||||
jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
distinctUntilKeyChanged,
|
||||
from,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
@@ -38,7 +39,8 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi
|
||||
})
|
||||
export class VaultPopupItemsService {
|
||||
private _refreshCurrentTab$ = new Subject<void>();
|
||||
private searchText$ = new BehaviorSubject<string>("");
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
latestSearchText$: Observable<string> = this._searchText$.asObservable();
|
||||
|
||||
/**
|
||||
* Observable that contains the list of other cipher types that should be shown
|
||||
@@ -77,10 +79,12 @@ export class VaultPopupItemsService {
|
||||
* Observable that contains the list of all decrypted ciphers.
|
||||
* @private
|
||||
*/
|
||||
private _cipherList$: Observable<PopupCipherView[]> = this.cipherService.ciphers$.pipe(
|
||||
private _cipherList$: Observable<PopupCipherView[]> = merge(
|
||||
this.cipherService.ciphers$,
|
||||
this.cipherService.localData$,
|
||||
).pipe(
|
||||
runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular
|
||||
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
|
||||
map((ciphers) => Object.values(ciphers)),
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([
|
||||
this.organizationService.organizations$,
|
||||
@@ -105,7 +109,7 @@ export class VaultPopupItemsService {
|
||||
|
||||
private _filteredCipherList$: Observable<PopupCipherView[]> = combineLatest([
|
||||
this._cipherList$,
|
||||
this.searchText$,
|
||||
this._searchText$,
|
||||
this.vaultPopupListFiltersService.filterFunction$,
|
||||
]).pipe(
|
||||
map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [
|
||||
@@ -179,7 +183,7 @@ export class VaultPopupItemsService {
|
||||
* Observable that indicates whether a filter is currently applied to the ciphers.
|
||||
*/
|
||||
hasFilterApplied$ = combineLatest([
|
||||
this.searchText$,
|
||||
this._searchText$,
|
||||
this.vaultPopupListFiltersService.filters$,
|
||||
]).pipe(
|
||||
switchMap(([searchText, filters]) => {
|
||||
@@ -242,7 +246,7 @@ export class VaultPopupItemsService {
|
||||
}
|
||||
|
||||
applyFilter(newSearchText: string) {
|
||||
this.searchText$.next(newSearchText);
|
||||
this._searchText$.next(newSearchText);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
|
||||
@Component({
|
||||
selector: "app-sync",
|
||||
|
||||
@@ -97,6 +97,9 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement
|
||||
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||
/* eslint-enable import/no-restricted-paths */
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service construction
|
||||
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
@@ -120,8 +123,6 @@ import { CollectionService } from "@bitwarden/common/vault/services/collection.s
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
|
||||
import { SyncNotifierService } from "@bitwarden/common/vault/services/sync/sync-notifier.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/services/sync/sync.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import {
|
||||
ImportApiService,
|
||||
@@ -216,7 +217,6 @@ export class ServiceContainer {
|
||||
folderApiService: FolderApiService;
|
||||
userVerificationApiService: UserVerificationApiService;
|
||||
organizationApiService: OrganizationApiServiceAbstraction;
|
||||
syncNotifierService: SyncNotifierService;
|
||||
sendApiService: SendApiService;
|
||||
devicesApiService: DevicesApiServiceAbstraction;
|
||||
deviceTrustService: DeviceTrustServiceAbstraction;
|
||||
@@ -440,8 +440,6 @@ export class ServiceContainer {
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.syncNotifierService = new SyncNotifierService();
|
||||
|
||||
this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService);
|
||||
|
||||
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||
@@ -648,7 +646,7 @@ export class ServiceContainer {
|
||||
|
||||
this.avatarService = new AvatarService(this.apiService, this.stateProvider);
|
||||
|
||||
this.syncService = new SyncService(
|
||||
this.syncService = new DefaultSyncService(
|
||||
this.masterPasswordService,
|
||||
this.accountService,
|
||||
this.apiService,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/message.response";
|
||||
|
||||
@@ -42,13 +42,13 @@ import { SystemService } from "@bitwarden/common/platform/abstractions/system.se
|
||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwar
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||
|
||||
@@ -23,7 +23,7 @@ import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broa
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
|
||||
@@ -216,31 +216,33 @@ export class AccountComponent {
|
||||
};
|
||||
|
||||
async viewApiKey() {
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.apiKeyModalRef, (comp) => {
|
||||
comp.keyType = "organization";
|
||||
comp.entityId = this.organizationId;
|
||||
comp.postKey = this.organizationApiService.getOrCreateApiKey.bind(
|
||||
this.organizationApiService,
|
||||
);
|
||||
comp.scope = "api.organization";
|
||||
comp.grantType = "client_credentials";
|
||||
comp.apiKeyTitle = "apiKey";
|
||||
comp.apiKeyWarning = "apiKeyWarning";
|
||||
comp.apiKeyDescription = "apiKeyDesc";
|
||||
await ApiKeyComponent.open(this.dialogService, {
|
||||
data: {
|
||||
keyType: "organization",
|
||||
entityId: this.organizationId,
|
||||
postKey: this.organizationApiService.getOrCreateApiKey.bind(this.organizationApiService),
|
||||
scope: "api.organization",
|
||||
grantType: "client_credentials",
|
||||
apiKeyTitle: "apiKey",
|
||||
apiKeyWarning: "apiKeyWarning",
|
||||
apiKeyDescription: "apiKeyDesc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async rotateApiKey() {
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.rotateApiKeyModalRef, (comp) => {
|
||||
comp.keyType = "organization";
|
||||
comp.isRotation = true;
|
||||
comp.entityId = this.organizationId;
|
||||
comp.postKey = this.organizationApiService.rotateApiKey.bind(this.organizationApiService);
|
||||
comp.scope = "api.organization";
|
||||
comp.grantType = "client_credentials";
|
||||
comp.apiKeyTitle = "apiKey";
|
||||
comp.apiKeyWarning = "apiKeyWarning";
|
||||
comp.apiKeyDescription = "apiKeyRotateDesc";
|
||||
await ApiKeyComponent.open(this.dialogService, {
|
||||
data: {
|
||||
keyType: "organization",
|
||||
isRotation: true,
|
||||
entityId: this.organizationId,
|
||||
postKey: this.organizationApiService.rotateApiKey.bind(this.organizationApiService),
|
||||
scope: "api.organization",
|
||||
grantType: "client_credentials",
|
||||
apiKeyTitle: "apiKey",
|
||||
apiKeyWarning: "apiKeyWarning",
|
||||
apiKeyDescription: "apiKeyRotateDesc",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PolicyListService } from "./admin-console/core/policy-list.service";
|
||||
|
||||
@@ -1,76 +1,40 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{ "recoverAccountTwoStep" | i18n }}</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>
|
||||
{{ "recoverAccountTwoStepDesc" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/lost-two-step-device/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="Email"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
appAutofocus
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="password"
|
||||
name="MasterPassword"
|
||||
class="form-control"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="recoveryCode">{{ "recoveryCodeTitle" | i18n }}</label>
|
||||
<input
|
||||
id="recoveryCode"
|
||||
class="text-monospace form-control"
|
||||
type="text"
|
||||
name="RecoveryCode"
|
||||
[(ngModel)]="recoveryCode"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<p bitTypography="body1">
|
||||
{{ "recoverAccountTwoStepDesc" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/lost-two-step-device/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>{{ "learnMore" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="email"
|
||||
appAutofocus
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input bitInput type="password" formControlName="masterPassword" appInputVerbatim />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "recoveryCodeTitle" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="recoveryCode" appInputVerbatim />
|
||||
</bit-form-field>
|
||||
<hr />
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [block]="true">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<a bitButton buttonType="secondary" routerLink="/login" [block]="true">
|
||||
{{ "cancel" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
@@ -6,7 +7,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/two-factor-recovery.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@Component({
|
||||
@@ -14,10 +14,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
templateUrl: "recover-two-factor.component.html",
|
||||
})
|
||||
export class RecoverTwoFactorComponent {
|
||||
email: string;
|
||||
masterPassword: string;
|
||||
recoveryCode: string;
|
||||
formPromise: Promise<any>;
|
||||
protected formGroup = new FormGroup({
|
||||
email: new FormControl(null, [Validators.required]),
|
||||
masterPassword: new FormControl(null, [Validators.required]),
|
||||
recoveryCode: new FormControl(null, [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@@ -26,31 +27,32 @@ export class RecoverTwoFactorComponent {
|
||||
private i18nService: I18nService,
|
||||
private cryptoService: CryptoService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new TwoFactorRecoveryRequest();
|
||||
request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase();
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
const key = await this.loginStrategyService.makePreloginKey(
|
||||
this.masterPassword,
|
||||
request.email,
|
||||
);
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key);
|
||||
this.formPromise = this.apiService.postTwoFactorRecover(request);
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("twoStepRecoverDisabled"),
|
||||
);
|
||||
// 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(["/"]);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
get email(): string {
|
||||
return this.formGroup.value.email;
|
||||
}
|
||||
|
||||
get masterPassword(): string {
|
||||
return this.formGroup.value.masterPassword;
|
||||
}
|
||||
|
||||
get recoveryCode(): string {
|
||||
return this.formGroup.value.recoveryCode;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const request = new TwoFactorRecoveryRequest();
|
||||
request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase();
|
||||
request.email = this.email.trim().toLowerCase();
|
||||
const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email);
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key);
|
||||
await this.apiService.postTwoFactorRecover(request);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("twoStepRecoverDisabled"),
|
||||
);
|
||||
await this.router.navigate(["/"]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,72 +1,42 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="apiKeyTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="apiKeyTitle">{{ apiKeyTitle | i18n }}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ apiKeyDescription | i18n }}</p>
|
||||
<app-user-verification
|
||||
[(ngModel)]="masterPassword"
|
||||
ngDefaultControl
|
||||
name="secret"
|
||||
*ngIf="!clientSecret"
|
||||
>
|
||||
</app-user-verification>
|
||||
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{ apiKeyWarning | i18n }}</app-callout>
|
||||
<app-callout
|
||||
type="info"
|
||||
title="{{ 'oauth2ClientCredentials' | i18n }}"
|
||||
icon="bwi bwi-key"
|
||||
*ngIf="clientSecret"
|
||||
>
|
||||
<p class="mb-1">
|
||||
<strong>client_id:</strong><br />
|
||||
<code>{{ clientId }}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>client_secret:</strong><br />
|
||||
<code>{{ clientSecret }}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>scope:</strong><br />
|
||||
<code>{{ scope }}</code>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>grant_type:</strong><br />
|
||||
<code>{{ grantType }}</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="!clientSecret"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ (isRotation ? "rotateApiKey" : "viewApiKey") | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ data.apiKeyTitle | i18n }}</span>
|
||||
<div bitDialogContent>
|
||||
<p bitTypography="body1">{{ data.apiKeyDescription | i18n }}</p>
|
||||
<app-user-verification-form-input formControlName="masterPassword" *ngIf="!clientSecret">
|
||||
</app-user-verification-form-input>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{ data.apiKeyWarning | i18n }}</app-callout>
|
||||
<app-callout
|
||||
type="info"
|
||||
title="{{ 'oauth2ClientCredentials' | i18n }}"
|
||||
icon="bwi bwi-key"
|
||||
*ngIf="clientSecret"
|
||||
>
|
||||
<p bitTypography="body1" class="tw-mb-1">
|
||||
<strong>client_id:</strong><br />
|
||||
<code>{{ clientId }}</code>
|
||||
</p>
|
||||
<p bitTypography="body1" class="tw-mb-1">
|
||||
<strong>client_secret:</strong><br />
|
||||
<code>{{ clientSecret }}</code>
|
||||
</p>
|
||||
<p bitTypography="body1" class="tw-mb-1">
|
||||
<strong>scope:</strong><br />
|
||||
<code>{{ data.scope }}</code>
|
||||
</p>
|
||||
<p bitTypography="body1" class="tw-mb-0">
|
||||
<strong>grant_type:</strong><br />
|
||||
<code>{{ data.grantType }}</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<button type="submit" buttonType="primary" *ngIf="!clientSecret" bitButton bitFormButton>
|
||||
<span>{{ (data.isRotation ? "rotateApiKey" : "viewApiKey") | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-api-key",
|
||||
templateUrl: "api-key.component.html",
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
export type ApiKeyDialogData = {
|
||||
keyType: string;
|
||||
isRotation: boolean;
|
||||
postKey: (entityId: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
isRotation?: boolean;
|
||||
entityId: string;
|
||||
postKey: (entityId: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
scope: string;
|
||||
grantType: string;
|
||||
apiKeyTitle: string;
|
||||
apiKeyWarning: string;
|
||||
apiKeyDescription: string;
|
||||
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
};
|
||||
@Component({
|
||||
selector: "app-api-key",
|
||||
templateUrl: "api-key.component.html",
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
masterPassword: [null as Verification, [Validators.required]],
|
||||
});
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: ApiKeyDialogData,
|
||||
private formBuilder: FormBuilder,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword)
|
||||
.then((request) => this.postKey(this.entityId, request));
|
||||
const response = await this.formPromise;
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = `${this.keyType}.${this.entityId}`;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
submit = async () => {
|
||||
if (this.formGroup.invalid) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const response = await this.userVerificationService
|
||||
.buildRequest(this.formGroup.value.masterPassword)
|
||||
.then((request) => this.data.postKey(this.data.entityId, request));
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = `${this.data.keyType}.${this.data.entityId}`;
|
||||
};
|
||||
/**
|
||||
* Strongly typed helper to open a ApiKeyComponent
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
static open = (dialogService: DialogService, config: DialogConfig<ApiKeyDialogData>) => {
|
||||
return dialogService.open(ApiKeyComponent, config);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ApiKeyComponent } from "./api-key.component";
|
||||
|
||||
@@ -22,8 +22,8 @@ export class SecurityKeysComponent implements OnInit {
|
||||
constructor(
|
||||
private userVerificationService: UserVerificationService,
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService,
|
||||
private apiService: ApiService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -32,30 +32,34 @@ export class SecurityKeysComponent implements OnInit {
|
||||
|
||||
async viewUserApiKey() {
|
||||
const entityId = await this.stateService.getUserId();
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.viewUserApiKeyModalRef, (comp) => {
|
||||
comp.keyType = "user";
|
||||
comp.entityId = entityId;
|
||||
comp.postKey = this.apiService.postUserApiKey.bind(this.apiService);
|
||||
comp.scope = "api";
|
||||
comp.grantType = "client_credentials";
|
||||
comp.apiKeyTitle = "apiKey";
|
||||
comp.apiKeyWarning = "userApiKeyWarning";
|
||||
comp.apiKeyDescription = "userApiKeyDesc";
|
||||
await ApiKeyComponent.open(this.dialogService, {
|
||||
data: {
|
||||
keyType: "user",
|
||||
entityId: entityId,
|
||||
postKey: this.apiService.postUserApiKey.bind(this.apiService),
|
||||
scope: "api",
|
||||
grantType: "client_credentials",
|
||||
apiKeyTitle: "apiKey",
|
||||
apiKeyWarning: "userApiKeyWarning",
|
||||
apiKeyDescription: "userApiKeyDesc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async rotateUserApiKey() {
|
||||
const entityId = await this.stateService.getUserId();
|
||||
await this.modalService.openViewRef(ApiKeyComponent, this.rotateUserApiKeyModalRef, (comp) => {
|
||||
comp.keyType = "user";
|
||||
comp.isRotation = true;
|
||||
comp.entityId = entityId;
|
||||
comp.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
|
||||
comp.scope = "api";
|
||||
comp.grantType = "client_credentials";
|
||||
comp.apiKeyTitle = "apiKey";
|
||||
comp.apiKeyWarning = "userApiKeyWarning";
|
||||
comp.apiKeyDescription = "apiKeyRotateDesc";
|
||||
await ApiKeyComponent.open(this.dialogService, {
|
||||
data: {
|
||||
keyType: "user",
|
||||
isRotation: true,
|
||||
entityId: entityId,
|
||||
postKey: this.apiService.postUserRotateApiKey.bind(this.apiService),
|
||||
scope: "api",
|
||||
grantType: "client_credentials",
|
||||
apiKeyTitle: "apiKey",
|
||||
apiKeyWarning: "userApiKeyWarning",
|
||||
apiKeyDescription: "apiKeyRotateDesc",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,53 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faEmailTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="2faEmailTitle">
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<small>{{ "emailTitle" | i18n }}</small>
|
||||
</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
*ngIf="authed"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<strong>{{ "email" | i18n }}:</strong> {{ email }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<p class="d-flex">
|
||||
<span class="mr-3">{{ "twoFactorEmailDesc" | i18n }}</span>
|
||||
<img class="float-right ml-auto mfaType1" alt="Email logo" />
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="email">1. {{ "twoFactorEmailEnterEmail" | i18n }}</label>
|
||||
<input
|
||||
id="email"
|
||||
type="text"
|
||||
name="Email"
|
||||
class="form-control"
|
||||
[(ngModel)]="email"
|
||||
required
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 d-flex">
|
||||
<button
|
||||
#sendBtn
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm btn-submit align-self-start"
|
||||
(click)="sendEmail()"
|
||||
[appApiAction]="emailPromise"
|
||||
[disabled]="$any(sendBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span>{{ "sendEmail" | i18n }}</span>
|
||||
</button>
|
||||
<span class="text-success ml-3" *ngIf="sentEmail">
|
||||
{{ "verificationCodeEmailSent" | i18n: sentEmail }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="token">2. {{ "twoFactorEmailEnterCode" | i18n }}</label>
|
||||
<input
|
||||
id="token"
|
||||
type="text"
|
||||
name="Token"
|
||||
class="form-control"
|
||||
[(ngModel)]="token"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
|
||||
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="authed">
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "twoStepLogin" | i18n }}
|
||||
<span bitTypography="body1">{{ "emailTitle" | i18n }}</span>
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
<strong>{{ "email" | i18n }}:</strong> {{ email }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!enabled">
|
||||
<p class="tw-flex">
|
||||
<span class="tw-mr-3">{{ "twoFactorEmailDesc" | i18n }}</span>
|
||||
<img class="tw-float-right tw-ml-auto mfaType1" alt="Email logo" />
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>1. {{ "twoFactorEmailEnterEmail" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="email"
|
||||
inputmode="email"
|
||||
appInputVerbatim="false"
|
||||
/>
|
||||
</bit-form-field>
|
||||
<div class="tw-mb-3 tw-flex">
|
||||
<button bitButton type="button" buttonType="primary" [bitAction]="sendEmail">
|
||||
{{ "sendEmail" | i18n }}
|
||||
</button>
|
||||
<span class="tw-text-success tw-ml-3" *ngIf="sentEmail">
|
||||
{{ "verificationCodeEmailSent" | i18n: sentEmail }}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
<bit-label>2. {{ "twoFactorEmailEnterCode" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="token" appInputVerbatim />
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton type="submit" buttonType="primary">
|
||||
<span *ngIf="!enabled">{{ "enable" | i18n }}</span>
|
||||
<span *ngIf="enabled">{{ "disable" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, EventEmitter, Inject, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -19,18 +21,22 @@ import { TwoFactorBaseComponent } from "./two-factor-base.component";
|
||||
@Component({
|
||||
selector: "app-two-factor-email",
|
||||
templateUrl: "two-factor-email.component.html",
|
||||
outputs: ["onUpdated"],
|
||||
})
|
||||
export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
@Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
|
||||
type = TwoFactorProviderType.Email;
|
||||
email: string;
|
||||
token: string;
|
||||
sentEmail: string;
|
||||
formPromise: Promise<TwoFactorEmailResponse>;
|
||||
emailPromise: Promise<unknown>;
|
||||
|
||||
override componentName = "app-two-factor-email";
|
||||
formGroup = this.formBuilder.group({
|
||||
token: [null],
|
||||
email: ["", [Validators.email, Validators.required]],
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorEmailResponse>,
|
||||
apiService: ApiService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
@@ -38,6 +44,8 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
userVerificationService: UserVerificationService,
|
||||
private accountService: AccountService,
|
||||
dialogService: DialogService,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -48,31 +56,49 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
dialogService,
|
||||
);
|
||||
}
|
||||
get token() {
|
||||
return this.formGroup.get("token").value;
|
||||
}
|
||||
set token(value: string) {
|
||||
this.formGroup.get("token").setValue(value);
|
||||
}
|
||||
get email() {
|
||||
return this.formGroup.get("email").value;
|
||||
}
|
||||
set email(value: string) {
|
||||
this.formGroup.get("email").setValue(value);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.auth(this.data);
|
||||
}
|
||||
|
||||
auth(authResponse: AuthResponse<TwoFactorEmailResponse>) {
|
||||
super.auth(authResponse);
|
||||
return this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
submit() {
|
||||
submit = async () => {
|
||||
if (this.enabled) {
|
||||
return super.disable(this.formPromise);
|
||||
await this.disableEmail();
|
||||
this.onChangeStatus.emit(false);
|
||||
} else {
|
||||
return this.enable();
|
||||
await this.enable();
|
||||
this.onChangeStatus.emit(true);
|
||||
}
|
||||
};
|
||||
|
||||
private disableEmail() {
|
||||
return super.disable(this.formPromise);
|
||||
}
|
||||
|
||||
async sendEmail() {
|
||||
try {
|
||||
const request = await this.buildRequestModel(TwoFactorEmailRequest);
|
||||
request.email = this.email;
|
||||
this.emailPromise = this.apiService.postTwoFactorEmailSetup(request);
|
||||
await this.emailPromise;
|
||||
this.sentEmail = this.email;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
sendEmail = async () => {
|
||||
const request = await this.buildRequestModel(TwoFactorEmailRequest);
|
||||
request.email = this.email;
|
||||
this.emailPromise = this.apiService.postTwoFactorEmailSetup(request);
|
||||
await this.emailPromise;
|
||||
this.sentEmail = this.email;
|
||||
};
|
||||
|
||||
protected async enable() {
|
||||
const request = await this.buildRequestModel(UpdateTwoFactorEmailRequest);
|
||||
@@ -86,6 +112,10 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
});
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.dialogRef.close(this.enabled);
|
||||
};
|
||||
|
||||
private async processResponse(response: TwoFactorEmailResponse) {
|
||||
this.token = null;
|
||||
this.email = response.email;
|
||||
@@ -96,4 +126,15 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Strongly typed helper to open a TwoFactorEmailComponentComponent
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AuthResponse<TwoFactorEmailResponse>>,
|
||||
) {
|
||||
return dialogService.open<boolean>(TwoFactorEmailComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
@@ -178,11 +179,14 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const emailComp = await this.openModal(this.emailModalRef, TwoFactorEmailComponent);
|
||||
await emailComp.auth(result);
|
||||
emailComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||
const authComp: DialogRef<boolean, any> = TwoFactorEmailComponent.open(this.dialogService, {
|
||||
data: result,
|
||||
});
|
||||
authComp.componentInstance.onChangeStatus
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.Email);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case TwoFactorProviderType.WebAuthn: {
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
<form #form class="card" (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" appA11yTitle="{{ 'cancel' | i18n }}" (click)="cancel()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h3 class="card-body-header">{{ "downloadLicense" | i18n }}</h3>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<div class="d-flex">
|
||||
<label for="installationId">{{ "enterInstallationId" | i18n }}</label>
|
||||
<a
|
||||
class="ml-auto"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/licensing-on-premise/#organization-account-sharing"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
<form [formGroup]="licenseForm" [bitSubmit]="submit">
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>{{ "downloadLicense" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-8">
|
||||
<bit-form-field>
|
||||
<bit-label
|
||||
>{{ "enterInstallationId" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
class="tw-ml-auto"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/licensing-on-premise/#organization-account-sharing"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
<input type="text" bitInput formControlName="installationId" />
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<input
|
||||
id="installationId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="InstallationId"
|
||||
[(ngModel)]="installationId"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "submit" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button bitButton [bitAction]="cancel" bitFormButton buttonType="secondary" type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,50 +1,61 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { DialogConfig, DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export enum DownloadLicenseDialogResult {
|
||||
Cancelled = "cancelled",
|
||||
Downloaded = "downloaded",
|
||||
}
|
||||
type DownloadLicenseDialogData = {
|
||||
/** current organization id */
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: "app-download-license",
|
||||
templateUrl: "download-license.component.html",
|
||||
})
|
||||
export class DownloadLicenseComponent {
|
||||
@Input() organizationId: string;
|
||||
@Output() onDownloaded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
installationId: string;
|
||||
formPromise: Promise<unknown>;
|
||||
|
||||
export class DownloadLicenceDialogComponent {
|
||||
licenseForm = this.formBuilder.group({
|
||||
installationId: ["", [Validators.required]],
|
||||
});
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: DownloadLicenseDialogData,
|
||||
private dialogRef: DialogRef,
|
||||
private fileDownloadService: FileDownloadService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected formBuilder: FormBuilder,
|
||||
) {}
|
||||
|
||||
async submit() {
|
||||
if (this.installationId == null || this.installationId === "") {
|
||||
submit = async () => {
|
||||
this.licenseForm.markAllAsTouched();
|
||||
const installationId = this.licenseForm.get("installationId").value;
|
||||
if (installationId == null || installationId === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.organizationApiService.getLicense(
|
||||
this.organizationId,
|
||||
this.installationId,
|
||||
);
|
||||
const license = await this.formPromise;
|
||||
const licenseString = JSON.stringify(license, null, 2);
|
||||
this.fileDownloadService.download({
|
||||
fileName: "bitwarden_organization_license.json",
|
||||
blobData: licenseString,
|
||||
});
|
||||
this.onDownloaded.emit();
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
const license = await this.organizationApiService.getLicense(
|
||||
this.data.organizationId,
|
||||
installationId,
|
||||
);
|
||||
const licenseString = JSON.stringify(license, null, 2);
|
||||
this.fileDownloadService.download({
|
||||
fileName: "bitwarden_organization_license.json",
|
||||
blobData: licenseString,
|
||||
});
|
||||
this.dialogRef.close(DownloadLicenseDialogResult.Downloaded);
|
||||
};
|
||||
/**
|
||||
* Strongly typed helper to open a DownloadLicenceDialogComponent
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
static open(dialogService: DialogService, config: DialogConfig<DownloadLicenseDialogData>) {
|
||||
return dialogService.open<DownloadLicenseDialogResult>(DownloadLicenceDialogComponent, config);
|
||||
}
|
||||
cancel = () => {
|
||||
this.dialogRef.close(DownloadLicenseDialogResult.Cancelled);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AdjustSubscription } from "./adjust-subscription.component";
|
||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||
import { BillingSyncKeyComponent } from "./billing-sync-key.component";
|
||||
import { ChangePlanComponent } from "./change-plan.component";
|
||||
import { DownloadLicenseComponent } from "./download-license.component";
|
||||
import { DownloadLicenceDialogComponent } from "./download-license.component";
|
||||
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
|
||||
import { OrganizationBillingRoutingModule } from "./organization-billing-routing.module";
|
||||
import { OrganizationPlansComponent } from "./organization-plans.component";
|
||||
@@ -32,7 +32,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component";
|
||||
BillingSyncApiKeyComponent,
|
||||
BillingSyncKeyComponent,
|
||||
ChangePlanComponent,
|
||||
DownloadLicenseComponent,
|
||||
DownloadLicenceDialogComponent,
|
||||
OrganizationSubscriptionCloudComponent,
|
||||
OrganizationSubscriptionSelfhostComponent,
|
||||
OrgBillingHistoryViewComponent,
|
||||
|
||||
@@ -246,13 +246,6 @@
|
||||
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-mt-3" *ngIf="showDownloadLicense">
|
||||
<app-download-license
|
||||
[organizationId]="organizationId"
|
||||
(onDownloaded)="closeDownloadLicense()"
|
||||
(onCanceled)="closeDownloadLicense()"
|
||||
></app-download-license>
|
||||
</div>
|
||||
<ng-container *ngIf="userOrg.canEditSubscription">
|
||||
<h2 bitTypography="h2" class="tw-mt-7">{{ "additionalOptions" | i18n }}</h2>
|
||||
<p bitTypography="body1">
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "../shared/offboarding-survey.component";
|
||||
|
||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||
import { DownloadLicenceDialogComponent } from "./download-license.component";
|
||||
import { ManageBilling } from "./icons/manage-billing.icon";
|
||||
import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component";
|
||||
|
||||
@@ -354,8 +355,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
this.showChangePlan = false;
|
||||
}
|
||||
|
||||
downloadLicense() {
|
||||
this.showDownloadLicense = !this.showDownloadLicense;
|
||||
async downloadLicense() {
|
||||
DownloadLicenceDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async manageBillingSync() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
|
||||
import { PaymentMethodWarningsModule } from "../billing/shared";
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
redirectGuard,
|
||||
tdeDecryptionRequiredGuard,
|
||||
UnauthGuard,
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/auth/angular";
|
||||
|
||||
import { flagEnabled, Flags } from "../utils/flags";
|
||||
|
||||
@@ -40,6 +42,7 @@ import { UpdatePasswordComponent } from "./auth/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "./auth/update-temp-password.component";
|
||||
import { VerifyEmailTokenComponent } from "./auth/verify-email-token.component";
|
||||
import { VerifyRecoverDeleteComponent } from "./auth/verify-recover-delete.component";
|
||||
import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component";
|
||||
import { DataProperties } from "./core";
|
||||
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "./layouts/user-layout.component";
|
||||
@@ -141,12 +144,6 @@ const routes: Routes = [
|
||||
data: { titleId: "acceptFamilySponsorship", doNotSaveUrl: false } satisfies DataProperties,
|
||||
},
|
||||
{ path: "recover", pathMatch: "full", redirectTo: "recover-2fa" },
|
||||
{
|
||||
path: "recover-2fa",
|
||||
component: RecoverTwoFactorComponent,
|
||||
canActivate: [UnauthGuard],
|
||||
data: { titleId: "recoverAccountTwoStep" } satisfies DataProperties,
|
||||
},
|
||||
{
|
||||
path: "recover-delete",
|
||||
component: RecoverDeleteComponent,
|
||||
@@ -203,6 +200,31 @@ const routes: Routes = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "recover-2fa",
|
||||
canActivate: [unauthGuardFn()],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: RecoverTwoFactorComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: "recoverAccountTwoStep",
|
||||
titleId: "recoverAccountTwoStep",
|
||||
} satisfies DataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
component: UserLayoutComponent,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
|
||||
@@ -45,9 +45,9 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
|
||||
|
||||
@@ -48,10 +48,10 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
export interface PurgeVaultDialogData {
|
||||
|
||||
Reference in New Issue
Block a user