Merge branch 'main' into autofill/pm-8027-inline-menu-appears-within-input-fields-that-do-not-relate-to-user-login

This commit is contained in:
Cesar Gonzalez 2024-06-05 13:24:33 -05:00 committed by GitHub
commit a743a969e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 677 additions and 622 deletions

View File

@ -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,

View File

@ -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";

View File

@ -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);
});
}
}

View File

@ -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";

View File

@ -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";

View File

@ -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>

View File

@ -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"], {});

View File

@ -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);

View File

@ -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);
}
/**

View File

@ -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",

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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",
},
});
}
}

View File

@ -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";

View File

@ -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>

View File

@ -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(["/"]);
};
}

View File

@ -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">&times;</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>

View File

@ -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);
};
}

View File

@ -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",
},
});
}
}

View File

@ -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">&times;</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>

View File

@ -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);
}
}

View File

@ -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: {

View File

@ -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">&times;</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>

View File

@ -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);
};
}

View File

@ -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,

View File

@ -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">

View File

@ -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() {

View File

@ -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";

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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 {

View File

@ -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({

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,2 @@
export { SyncService } from "./sync.service";
export { SyncResponse } from "./sync.response";

View File

@ -1 +1,2 @@
export { DefaultSyncService } from "./default-sync.service";
export { CoreSyncService } from "./core-sync.service";

View File

@ -1,4 +1,4 @@
import { SyncResponse } from "../models/response/sync.response";
import { SyncResponse } from "./sync.response";
type SyncStatus = "Started" | "Completed";

View File

@ -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;

View 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>;
}

View File

@ -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

View File

@ -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;

View File

@ -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.
*/

View File

@ -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;
}

View File

@ -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";

View File

@ -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);
}
}

View File

@ -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;
}
});
}

View File

@ -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>`;

View File

@ -2,3 +2,4 @@ export * from "./deactivated-org";
export * from "./search";
export * from "./no-access";
export * from "./vault";
export * from "./no-results";

View 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>
`;