mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-24 21:41:33 +01:00
[PM-5189] Merging work done for pm-8518
This commit is contained in:
commit
9fe7c2f643
@ -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 {
|
||||
|
@ -246,7 +246,10 @@ export class LoginViaAuthRequestComponent
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({
|
||||
type: "password",
|
||||
length: 25,
|
||||
});
|
||||
|
||||
this.fingerprintPhrase = (
|
||||
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair.publicKey)
|
||||
|
@ -187,6 +187,9 @@ import { DefaultStateProvider } from "@bitwarden/common/platform/state/implement
|
||||
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
|
||||
/* eslint-enable import/no-restricted-paths */
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||
import {
|
||||
DefaultThemeStateService,
|
||||
ThemeStateService,
|
||||
@ -226,8 +229,6 @@ import {
|
||||
FolderService as FolderServiceAbstraction,
|
||||
InternalFolderService,
|
||||
} 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 { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
@ -235,8 +236,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 { ToastService } from "@bitwarden/components";
|
||||
@ -644,8 +643,8 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [ApiServiceAbstraction, FileUploadServiceAbstraction, InternalSendService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncServiceAbstraction,
|
||||
useClass: SyncService,
|
||||
provide: SyncService,
|
||||
useClass: DefaultSyncService,
|
||||
deps: [
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
@ -796,7 +795,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: devFlagEnabled("noopNotifications") ? NoopNotificationsService : NotificationsService,
|
||||
deps: [
|
||||
LogService,
|
||||
SyncServiceAbstraction,
|
||||
SyncService,
|
||||
AppIdServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
EnvironmentService,
|
||||
@ -942,12 +941,7 @@ const safeProviders: SafeProvider[] = [
|
||||
// it depends on SyncService so that new data can be retrieved through the sync
|
||||
// rather than updating the OrganizationService directly. Instead OrganizationService
|
||||
// subscribes to sync notifications and will update itself based on that.
|
||||
deps: [ApiServiceAbstraction, SyncServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SyncNotifierServiceAbstraction,
|
||||
useClass: SyncNotifierService,
|
||||
deps: [],
|
||||
deps: [ApiServiceAbstraction, SyncService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DefaultConfigService,
|
||||
@ -1122,7 +1116,7 @@ const safeProviders: SafeProvider[] = [
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
OrganizationApiServiceAbstraction,
|
||||
SyncServiceAbstraction,
|
||||
SyncService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
@ -3,7 +3,6 @@ import { Observable } from "rxjs";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
|
||||
import {
|
||||
@ -72,12 +71,4 @@ export abstract class LoginStrategyServiceAbstraction {
|
||||
* Creates a master key from the provided master password and email.
|
||||
*/
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
||||
/**
|
||||
* Sends a response to an auth request.
|
||||
*/
|
||||
passwordlessLogin: (
|
||||
id: string,
|
||||
key: string,
|
||||
requestApproved: boolean,
|
||||
) => Promise<AuthRequestResponse>;
|
||||
}
|
||||
|
@ -24,8 +24,6 @@ import {
|
||||
PBKDF2KdfConfig,
|
||||
} from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
@ -39,7 +37,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
@ -263,47 +260,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig);
|
||||
}
|
||||
|
||||
// TODO: move to auth request service
|
||||
async passwordlessLogin(
|
||||
id: string,
|
||||
key: string,
|
||||
requestApproved: boolean,
|
||||
): Promise<AuthRequestResponse> {
|
||||
const pubKey = Utils.fromB64ToArray(key);
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
let keyToEncrypt;
|
||||
let encryptedMasterKeyHash = null;
|
||||
|
||||
if (masterKey) {
|
||||
keyToEncrypt = masterKey.encKey;
|
||||
|
||||
// Only encrypt the master password hash if masterKey exists as
|
||||
// we won't have a masterKeyHash without a masterKey
|
||||
const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId));
|
||||
if (masterKeyHash != null) {
|
||||
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
|
||||
Utils.fromUtf8ToArray(masterKeyHash),
|
||||
pubKey,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const userKey = await this.cryptoService.getUserKey();
|
||||
keyToEncrypt = userKey.key;
|
||||
}
|
||||
|
||||
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
|
||||
|
||||
const request = new PasswordlessAuthRequest(
|
||||
encryptedKey.encryptedString,
|
||||
encryptedMasterKeyHash?.encryptedString,
|
||||
await this.appIdService.getAppId(),
|
||||
requestApproved,
|
||||
);
|
||||
return await this.apiService.putAuthRequest(id, request);
|
||||
}
|
||||
|
||||
private async clearCache(): Promise<void> {
|
||||
await this.currentAuthnTypeState.update((_) => null);
|
||||
await this.loginStrategyCacheState.update((_) => null);
|
||||
|
@ -103,6 +103,7 @@ import { EventResponse } from "../models/response/event.response";
|
||||
import { ListResponse } from "../models/response/list.response";
|
||||
import { ProfileResponse } from "../models/response/profile.response";
|
||||
import { UserKeyResponse } from "../models/response/user-key.response";
|
||||
import { SyncResponse } from "../platform/sync";
|
||||
import { UserId } from "../types/guid";
|
||||
import { AttachmentRequest } from "../vault/models/request/attachment.request";
|
||||
import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request";
|
||||
@ -124,7 +125,6 @@ import {
|
||||
CollectionResponse,
|
||||
} from "../vault/models/response/collection.response";
|
||||
import { OptionalCipherResponse } from "../vault/models/response/optional-cipher.response";
|
||||
import { SyncResponse } from "../vault/models/response/sync.response";
|
||||
|
||||
/**
|
||||
* @deprecated The `ApiService` class is deprecated and calls should be extracted into individual
|
||||
|
@ -1,50 +1,53 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||
import { LogoutReason } from "../../../../auth/src/common/types";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "../../admin-console/abstractions/provider.service";
|
||||
import { OrganizationUserType } from "../../admin-console/enums";
|
||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../admin-console/models/data/provider.data";
|
||||
import { PolicyResponse } from "../../admin-console/models/response/policy.response";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
import { AvatarService } from "../../auth/abstractions/avatar.service";
|
||||
import { KeyConnectorService } from "../../auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "../../auth/abstractions/token.service";
|
||||
import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason";
|
||||
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
|
||||
import { BillingAccountProfileStateService } from "../../billing/abstractions";
|
||||
import { DomainsResponse } from "../../models/response/domains.response";
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
import { SendData } from "../../tools/send/models/data/send.data";
|
||||
import { SendResponse } from "../../tools/send/models/response/send.response";
|
||||
import { SendApiService } from "../../tools/send/services/send-api.service.abstraction";
|
||||
import { InternalSendService } from "../../tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { CipherService } from "../../vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "../../vault/abstractions/collection.service";
|
||||
import { FolderApiServiceAbstraction } from "../../vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { InternalFolderService } from "../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||
import { CollectionData } from "../../vault/models/data/collection.data";
|
||||
import { FolderData } from "../../vault/models/data/folder.data";
|
||||
import { CipherResponse } from "../../vault/models/response/cipher.response";
|
||||
import { CollectionDetailsResponse } from "../../vault/models/response/collection.response";
|
||||
import { FolderResponse } from "../../vault/models/response/folder.response";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { StateService } from "../abstractions/state.service";
|
||||
import { MessageSender } from "../messaging";
|
||||
import { sequentialize } from "../misc/sequentialize";
|
||||
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
|
||||
import { OrganizationUserType } from "../../../admin-console/enums";
|
||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||
import { PolicyData } from "../../../admin-console/models/data/policy.data";
|
||||
import { ProviderData } from "../../../admin-console/models/data/provider.data";
|
||||
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||
import { AvatarService } from "../../../auth/abstractions/avatar.service";
|
||||
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "../../../auth/abstractions/token.service";
|
||||
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
|
||||
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
|
||||
import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { DomainsResponse } from "../../../models/response/domains.response";
|
||||
import { ProfileResponse } from "../../../models/response/profile.response";
|
||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { MessageSender } from "../../../platform/messaging";
|
||||
import { sequentialize } from "../../../platform/misc/sequentialize";
|
||||
import { CoreSyncService } from "../../../platform/sync/core-sync.service";
|
||||
import { SendData } from "../../../tools/send/models/data/send.data";
|
||||
import { SendResponse } from "../../../tools/send/models/response/send.response";
|
||||
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
|
||||
import { InternalSendService } from "../../../tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { CipherService } from "../../../vault/abstractions/cipher.service";
|
||||
import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||
import { FolderData } from "../../../vault/models/data/folder.data";
|
||||
import { CipherResponse } from "../../../vault/models/response/cipher.response";
|
||||
import { FolderResponse } from "../../../vault/models/response/folder.response";
|
||||
import { CollectionService } from "../../abstractions/collection.service";
|
||||
import { CollectionData } from "../../models/data/collection.data";
|
||||
import { CollectionDetailsResponse } from "../../models/response/collection.response";
|
||||
import { CoreSyncService } from "./core-sync.service";
|
||||
|
||||
export class DefaultSyncService extends CoreSyncService {
|
||||
syncInProgress = false;
|
||||
|
||||
export class SyncService extends CoreSyncService {
|
||||
constructor(
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
2
libs/common/src/platform/sync/index.ts
Normal file
2
libs/common/src/platform/sync/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { SyncService } from "./sync.service";
|
||||
export { SyncResponse } from "./sync.response";
|
@ -1 +1,2 @@
|
||||
export { DefaultSyncService } from "./default-sync.service";
|
||||
export { CoreSyncService } from "./core-sync.service";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SyncResponse } from "../models/response/sync.response";
|
||||
import { SyncResponse } from "./sync.response";
|
||||
|
||||
type SyncStatus = "Started" | "Completed";
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { DomainsResponse } from "../../../models/response/domains.response";
|
||||
import { ProfileResponse } from "../../../models/response/profile.response";
|
||||
import { SendResponse } from "../../../tools/send/models/response/send.response";
|
||||
|
||||
import { CipherResponse } from "./cipher.response";
|
||||
import { CollectionDetailsResponse } from "./collection.response";
|
||||
import { FolderResponse } from "./folder.response";
|
||||
import { PolicyResponse } from "../../admin-console/models/response/policy.response";
|
||||
import { BaseResponse } from "../../models/response/base.response";
|
||||
import { DomainsResponse } from "../../models/response/domains.response";
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
import { SendResponse } from "../../tools/send/models/response/send.response";
|
||||
import { CipherResponse } from "../../vault/models/response/cipher.response";
|
||||
import { CollectionDetailsResponse } from "../../vault/models/response/collection.response";
|
||||
import { FolderResponse } from "../../vault/models/response/folder.response";
|
||||
|
||||
export class SyncResponse extends BaseResponse {
|
||||
profile?: ProfileResponse;
|
58
libs/common/src/platform/sync/sync.service.ts
Normal file
58
libs/common/src/platform/sync/sync.service.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
SyncCipherNotification,
|
||||
SyncFolderNotification,
|
||||
SyncSendNotification,
|
||||
} from "../../models/response/notification.response";
|
||||
|
||||
/**
|
||||
* A class encapsulating sync operations and data.
|
||||
*/
|
||||
export abstract class SyncService {
|
||||
/**
|
||||
* A boolean indicating if a sync is currently in progress via this instance and this instance only.
|
||||
*
|
||||
* @deprecated Trusting this property is not safe as it only tells if the current instance is currently
|
||||
* doing a sync operation but does not tell if another instance of SyncService is doing a sync operation.
|
||||
*/
|
||||
abstract syncInProgress: boolean;
|
||||
|
||||
/**
|
||||
* Gets the date of the last sync for the currently active user.
|
||||
*
|
||||
* @returns The date of the last sync or null if there is no active user or the active user has not synced before.
|
||||
*/
|
||||
abstract getLastSync(): Promise<Date>;
|
||||
|
||||
/**
|
||||
* Updates a users last sync date.
|
||||
* @param date The date to be set as the users last sync date.
|
||||
* @param userId The userId of the user to update the last sync date for.
|
||||
*/
|
||||
abstract setLastSync(date: Date, userId?: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Optionally does a full sync operation including going to the server to gather the source
|
||||
* of truth and set that data to state.
|
||||
* @param forceSync A boolean dictating if a sync should be forced. If `true` a sync will happen
|
||||
* as long as the current user is authenticated. If `false` it will only sync if either a sync
|
||||
* has not happened before or the last sync date for the active user is before their account
|
||||
* revision date. Try to always use `false` if possible.
|
||||
*
|
||||
* @param allowThrowOnError A boolean dictating whether or not caught errors should be rethrown.
|
||||
* `true` if they can be rethrown, `false` if they should not be rethrown.
|
||||
*/
|
||||
abstract fullSync(forceSync: boolean, allowThrowOnError?: boolean): Promise<boolean>;
|
||||
|
||||
abstract syncUpsertFolder(
|
||||
notification: SyncFolderNotification,
|
||||
isEdit: boolean,
|
||||
): Promise<boolean>;
|
||||
abstract syncDeleteFolder(notification: SyncFolderNotification): Promise<boolean>;
|
||||
abstract syncUpsertCipher(
|
||||
notification: SyncCipherNotification,
|
||||
isEdit: boolean,
|
||||
): Promise<boolean>;
|
||||
abstract syncDeleteCipher(notification: SyncFolderNotification): Promise<boolean>;
|
||||
abstract syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean>;
|
||||
abstract syncDeleteSend(notification: SyncSendNotification): Promise<boolean>;
|
||||
}
|
@ -121,6 +121,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "../platform/misc/utils";
|
||||
import { SyncResponse } from "../platform/sync";
|
||||
import { UserId } from "../types/guid";
|
||||
import { AttachmentRequest } from "../vault/models/request/attachment.request";
|
||||
import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request";
|
||||
@ -142,7 +143,6 @@ import {
|
||||
CollectionResponse,
|
||||
} from "../vault/models/response/collection.response";
|
||||
import { OptionalCipherResponse } from "../vault/models/response/optional-cipher.response";
|
||||
import { SyncResponse } from "../vault/models/response/sync.response";
|
||||
|
||||
/**
|
||||
* @deprecated The `ApiService` class is deprecated and calls should be extracted into individual
|
||||
|
@ -21,8 +21,8 @@ import { EnvironmentService } from "../platform/abstractions/environment.service
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../platform/abstractions/messaging.service";
|
||||
import { StateService } from "../platform/abstractions/state.service";
|
||||
import { SyncService } from "../platform/sync/sync.service";
|
||||
import { UserId } from "../types/guid";
|
||||
import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
private signalrConnection: signalR.HubConnection;
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { LocalData } from "@bitwarden/common/vault/models/data/local.data";
|
||||
|
||||
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CipherId, CollectionId, OrganizationId } from "../../types/guid";
|
||||
@ -14,6 +16,7 @@ import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
|
||||
export abstract class CipherService {
|
||||
cipherViews$: Observable<Record<CipherId, CipherView>>;
|
||||
ciphers$: Observable<Record<CipherId, CipherData>>;
|
||||
localData$: Observable<Record<CipherId, LocalData>>;
|
||||
/**
|
||||
* An observable monitoring the add/edit cipher info saved to memory.
|
||||
*/
|
||||
|
@ -1,8 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SyncEventArgs } from "../../types/sync-event-args";
|
||||
|
||||
export abstract class SyncNotifierService {
|
||||
sync$: Observable<SyncEventArgs>;
|
||||
next: (event: SyncEventArgs) => void;
|
||||
}
|
@ -1,19 +1,2 @@
|
||||
import {
|
||||
SyncCipherNotification,
|
||||
SyncFolderNotification,
|
||||
SyncSendNotification,
|
||||
} from "../../../models/response/notification.response";
|
||||
|
||||
export abstract class SyncService {
|
||||
syncInProgress: boolean;
|
||||
|
||||
getLastSync: () => Promise<Date>;
|
||||
setLastSync: (date: Date, userId?: string) => Promise<any>;
|
||||
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;
|
||||
syncUpsertFolder: (notification: SyncFolderNotification, isEdit: boolean) => Promise<boolean>;
|
||||
syncDeleteFolder: (notification: SyncFolderNotification) => Promise<boolean>;
|
||||
syncUpsertCipher: (notification: SyncCipherNotification, isEdit: boolean) => Promise<boolean>;
|
||||
syncDeleteCipher: (notification: SyncFolderNotification) => Promise<boolean>;
|
||||
syncUpsertSend: (notification: SyncSendNotification, isEdit: boolean) => Promise<boolean>;
|
||||
syncDeleteSend: (notification: SyncSendNotification) => Promise<boolean>;
|
||||
}
|
||||
// TEMP: Re-export of original SyncService location to allow for team specific PR's
|
||||
export { SyncService } from "../../../platform/sync";
|
||||
|
@ -1,18 +0,0 @@
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { SyncNotifierService as SyncNotifierServiceAbstraction } from "../../abstractions/sync/sync-notifier.service.abstraction";
|
||||
import { SyncEventArgs } from "../../types/sync-event-args";
|
||||
|
||||
/**
|
||||
* This class should most likely have 0 dependencies because it will hopefully
|
||||
* be rolled into SyncService once upon a time.
|
||||
*/
|
||||
export class SyncNotifierService implements SyncNotifierServiceAbstraction {
|
||||
private _sync = new Subject<SyncEventArgs>();
|
||||
|
||||
sync$ = this._sync.asObservable();
|
||||
|
||||
next(event: SyncEventArgs): void {
|
||||
this._sync.next(event);
|
||||
}
|
||||
}
|
@ -40,13 +40,13 @@ export class BitFormButtonDirective implements OnDestroy {
|
||||
if (this.type === "submit") {
|
||||
buttonComponent.loading = loading;
|
||||
} else {
|
||||
buttonComponent.disabled = loading;
|
||||
buttonComponent.disabled = this.disabled || loading;
|
||||
}
|
||||
});
|
||||
|
||||
submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
|
||||
if (this.disabled !== false) {
|
||||
buttonComponent.disabled = disabled;
|
||||
buttonComponent.disabled = this.disabled || disabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ const template = `
|
||||
<button class="tw-mr-2" type="submit" buttonType="primary" bitButton bitFormButton>Submit</button>
|
||||
<button class="tw-mr-2" type="button" buttonType="secondary" bitButton bitFormButton>Cancel</button>
|
||||
<button class="tw-mr-2" type="button" buttonType="danger" bitButton bitFormButton [bitAction]="delete">Delete</button>
|
||||
<button class="tw-mr-2" type="button" buttonType="secondary" bitButton bitFormButton [disabled]="true">Disabled</button>
|
||||
<button class="tw-mr-2" type="button" buttonType="secondary" bitIconButton="bwi-star" bitFormButton [bitAction]="delete">Delete</button>
|
||||
</form>`;
|
||||
|
||||
|
@ -2,3 +2,4 @@ export * from "./deactivated-org";
|
||||
export * from "./search";
|
||||
export * from "./no-access";
|
||||
export * from "./vault";
|
||||
export * from "./no-results";
|
||||
|
18
libs/components/src/icon/icons/no-results.ts
Normal file
18
libs/components/src/icon/icons/no-results.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { svgIcon } from "../icon";
|
||||
|
||||
export const NoResults = svgIcon`
|
||||
<svg width="98" height="96" viewBox="0 0 98 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class="tw-stroke-primary-700" d="M8.8545 86.7919L56.9901 86.7919C60.2321 86.7919 62.8603 84.1637 62.8603 80.9217L62.8603 32.2678C62.8603 30.7472 62.2702 29.2859 61.2143 28.1916L47.5536 14.0345C46.4473 12.8881 44.9225 12.2405 43.3293 12.2405L8.85451 12.2405C5.61249 12.2405 2.98431 14.8687 2.98431 18.1107L2.98431 80.9217C2.98431 84.1637 5.61248 86.7919 8.8545 86.7919Z" stroke-width="1.76106"/>
|
||||
<path class="tw-fill-background tw-stroke-primary-700" d="M18.8335 76.8125L66.9691 76.8125C70.2111 76.8125 72.8393 74.1844 72.8393 70.9423L72.8393 21.8271C72.8393 20.3144 72.2554 18.8601 71.2093 17.7675L57.5349 3.48471C56.4276 2.32814 54.8959 1.67408 53.2947 1.67408L18.8335 1.67407C15.5915 1.67407 12.9633 4.30225 12.9633 7.54427L12.9633 70.9423C12.9633 74.1844 15.5915 76.8125 18.8335 76.8125Z" stroke-width="1.76106"/>
|
||||
<path class="tw-stroke-primary-700" d="M54.3484 2.26123L54.3484 14.0016C54.3484 17.2436 56.9766 19.8718 60.2186 19.8718L72.546 19.8718" stroke-width="1.76106"/>
|
||||
<path class="tw-stroke-success-600" d="M20.0914 15.9861L43.5722 15.9861" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
|
||||
<path class="tw-stroke-success-600" d="M20.0914 30.8945L51.2034 30.8945" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
|
||||
<path class="tw-stroke-success-600" d="M20.0914 45.803L45.9203 45.803" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
|
||||
<path class="tw-stroke-success-600" d="M20.0914 60.7112L45.9203 60.7112" stroke-width="0.880529" stroke-linecap="round" stroke-dasharray="11.74 4.7"/>
|
||||
<path class="tw-fill-background tw-stroke-primary-700" d="M85.4233 53.9449C81.9863 66.772 68.6684 74.3484 55.6768 70.8674C42.6853 67.3863 34.9398 54.1659 38.3768 41.3388C41.8138 28.5117 55.1318 20.9353 68.1234 24.4163C81.1149 27.8974 88.8604 41.1178 85.4233 53.9449Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path class="tw-stroke-success-600" d="M55.1859 41.5395C55.1859 41.5395 55.2828 39.2314 57.5434 37.273C58.8998 36.084 60.5145 35.7692 61.9678 35.7343C63.2919 35.6993 64.4868 35.9441 65.1649 36.3288C66.3921 36.9583 68.7497 38.462 68.7497 41.7144C68.7497 45.1416 66.6828 46.6804 64.3576 48.394C62.0324 50.1076 62.3667 52.3385 62.3667 54.227" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path class="tw-fill-success-600 tw-stroke-secondary-600" d="M62.2727 59.2015C62.759 59.2015 63.1533 58.8073 63.1533 58.321C63.1533 57.8347 62.759 57.4404 62.2727 57.4404C61.7864 57.4404 61.3922 57.8347 61.3922 58.321C61.3922 58.8073 61.7864 59.2015 62.2727 59.2015Z"/>
|
||||
<path class="tw-fill-secondary-300 tw-stroke-primary-700" d="M96.0333 89.0621L95.4703 89.5329C94.2269 90.5728 92.3758 90.4078 91.3359 89.1644L78.2766 73.5488L74.79 69.3798C74.4843 69.0105 74.6096 68.4514 75.0271 68.2155C76.7198 67.2592 78.097 65.9974 78.8894 65.1364C79.1502 64.853 79.6089 64.8477 79.856 65.1431L83.3425 69.3121L96.4018 84.9277C97.4418 86.1712 97.2768 88.0222 96.0333 89.0621Z" stroke-width="1.76106" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
`;
|
@ -1 +1,2 @@
|
||||
export * from "./section.component";
|
||||
export * from "./section-header.component";
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div class="tw-flex tw-justify-between tw-items-end tw-gap-1">
|
||||
<div class="[&>*]:tw-mb-0 [&>*]:tw-text-headers tw-flex tw-items-center tw-gap-1">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="tw-text-muted has-[button]:-tw-mb-1">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
</div>
|
16
libs/components/src/section/section-header.component.ts
Normal file
16
libs/components/src/section/section-header.component.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "bit-section-header",
|
||||
templateUrl: "./section-header.component.html",
|
||||
imports: [TypographyModule],
|
||||
host: {
|
||||
class:
|
||||
// apply bottom and x padding when a `bit-card` or `bit-item` is the immediate sibling, or nested in the immediate sibling
|
||||
"tw-block has-[+_*_bit-card]:tw-pb-1 has-[+_bit-card]:tw-pb-1 has-[+_*_bit-item]:tw-pb-1 has-[+_bit-item]:tw-pb-1 has-[+_*_bit-card]:tw-px-1 has-[+_bit-card]:tw-px-1 has-[+_*_bit-item]:tw-px-1 has-[+_bit-item]:tw-px-1",
|
||||
},
|
||||
})
|
||||
export class SectionHeaderComponent {}
|
89
libs/components/src/section/section.mdx
Normal file
89
libs/components/src/section/section.mdx
Normal file
@ -0,0 +1,89 @@
|
||||
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./section.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
```ts
|
||||
import { SectionComponent, SectionHeaderComponent } from "@bitwarden/components";
|
||||
```
|
||||
|
||||
# Section
|
||||
|
||||
Sections are simple containers that apply a responsive bottom margin and utilize the semantic
|
||||
`section` HTML element.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.Default} />
|
||||
</Canvas>
|
||||
|
||||
## Section Header
|
||||
|
||||
Sections often contain a heading. Use `bit-section-header` inside of the `bit-section`.
|
||||
|
||||
```html
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h1 bitTypography="h1">I'm a section header</h2>
|
||||
</bit-section-header>
|
||||
<div>Section content here!</div>
|
||||
</bit-section>
|
||||
```
|
||||
|
||||
### Section Header Padding
|
||||
|
||||
When placed inside of a section with a `bit-card` or `bit-item` as the immediate next sibling (or
|
||||
nested in the immediate next sibling), the section header will automatically apply bottom and x-axis
|
||||
padding to align the header with the border radius of the card/item.
|
||||
|
||||
```html
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">I'm a section header</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<h3 bitTypography="h3">I'm card content</h3>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
```
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.HeaderWithPadding} />
|
||||
</Canvas>
|
||||
|
||||
If placed inside of a section without a `bit-card` or `bit-item`, or with a `bit-card`/`bit-item`
|
||||
that is not a descendant of the immediate next sibling, the padding is not applied.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.HeaderWithoutPadding} />
|
||||
</Canvas>
|
||||
|
||||
### Section Header Content Slots
|
||||
|
||||
`bit-section-header` contains the following slots to help position the content:
|
||||
|
||||
| Slot | Description |
|
||||
| ------------ | ------------------------------- |
|
||||
| default | title text of the header |
|
||||
| `slot="end"` | placed at the end of the header |
|
||||
|
||||
#### Default slot
|
||||
|
||||
Anything passed to the default slot will display as part of the title. The title should be a
|
||||
`bitTypography` element, usually an `h2` styled as an `h6`.
|
||||
|
||||
Title suffixes (typically an icon or icon button) can be added as well. A gap is automatically
|
||||
applied between the children of the default slot.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.HeaderVariants} />
|
||||
</Canvas>
|
||||
|
||||
#### End slot
|
||||
|
||||
The `end` slot will typically be used for text or an icon button.
|
||||
|
||||
<Canvas>
|
||||
<Story of={stories.HeaderEndSlotVariants} />
|
||||
</Canvas>
|
@ -1,15 +1,24 @@
|
||||
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { CardComponent } from "../card";
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { ItemModule } from "../item";
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
import { SectionComponent } from "./section.component";
|
||||
import { SectionComponent, SectionHeaderComponent } from "./";
|
||||
|
||||
export default {
|
||||
title: "Component Library/Section",
|
||||
component: SectionComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [TypographyModule],
|
||||
imports: [
|
||||
TypographyModule,
|
||||
SectionHeaderComponent,
|
||||
CardComponent,
|
||||
IconButtonModule,
|
||||
ItemModule,
|
||||
],
|
||||
}),
|
||||
componentWrapperDecorator((story) => `<div class="tw-text-main">${story}</div>`),
|
||||
],
|
||||
@ -17,19 +26,149 @@ export default {
|
||||
|
||||
type Story = StoryObj<SectionComponent>;
|
||||
|
||||
/** Sections are simple containers that apply a responsive bottom margin. They often contain a heading. */
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
render: () => ({
|
||||
template: /*html*/ `
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h2">Foo</h2>
|
||||
<p bitTypography="body1">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p>
|
||||
</bit-section-header>
|
||||
<p bitTypography="body1">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h2">Bar</h2>
|
||||
<p bitTypography="body1">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p>
|
||||
</bit-section-header>
|
||||
<p bitTypography="body1">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex. </p>
|
||||
</bit-section>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const HeaderVariants: Story = {
|
||||
render: () => ({
|
||||
template: /*html*/ `
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Title only
|
||||
</h2>
|
||||
</bit-section-header>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Title with icon button suffix
|
||||
</h2>
|
||||
<button bitIconButton="bwi-refresh" size="small"></button>
|
||||
</bit-section-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const HeaderEndSlotVariants: Story = {
|
||||
render: () => ({
|
||||
template: /*html*/ `
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Title with end slot text
|
||||
</h2>
|
||||
<span bitTypography="body2" slot="end">13</span>
|
||||
</bit-section-header>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Title with end slot icon button
|
||||
</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const HeaderWithPadding: Story = {
|
||||
render: () => ({
|
||||
template: /*html*/ `
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Card as immediate sibling
|
||||
</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
<bit-card>
|
||||
<h3 bitTypography="h3">bit-section-header has padding</h3>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Card nested in immediate sibling
|
||||
</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
<div>
|
||||
<bit-card>
|
||||
<h3 bitTypography="h3">bit-section-header has padding</h3>
|
||||
</bit-card>
|
||||
</div>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Item as immediate sibling
|
||||
</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
<bit-item>
|
||||
<bit-item-content bitTypography="body1">bit-section-header has padding</bit-item-content>
|
||||
</bit-item>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Item nested in immediate sibling
|
||||
</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
<bit-item-group>
|
||||
<bit-item>
|
||||
<bit-item-content bitTypography="body1">bit-section-header has padding</bit-item-content>
|
||||
</bit-item>
|
||||
</bit-item-group>
|
||||
</bit-section>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const HeaderWithoutPadding: Story = {
|
||||
render: () => ({
|
||||
template: /*html*/ `
|
||||
<div class="tw-bg-background-alt tw-p-2">
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
No card or item used
|
||||
</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
<div>
|
||||
<h3 bitTypography="h3">just a div, so bit-section-header has no padding</h3>
|
||||
</div>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">
|
||||
Card nested in non-immediate sibling
|
||||
</h2>
|
||||
<button bitIconButton="bwi-star" size="small" slot="end"></button>
|
||||
</bit-section-header>
|
||||
<div class="tw-text-main">
|
||||
a div here
|
||||
</div>
|
||||
<bit-card>
|
||||
<h3 bitTypography="h3">bit-section-header has no padding</h3>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user