mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[PM-24533] Initialize Archive Feature (#16226)
* [PM-19237] Add Archive Filter Type (#13852) * Browser can archive and unarchive items * Create Archive Cipher Service * Add flag and premium permissions to Archive --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: Shane <smelton@bitwarden.com> Co-authored-by: Patrick Pimentel <ppimentel@bitwarden.com>
This commit is contained in:
@@ -550,6 +550,33 @@
|
|||||||
"resetSearch": {
|
"resetSearch": {
|
||||||
"message": "Reset search"
|
"message": "Reset search"
|
||||||
},
|
},
|
||||||
|
"archive": {
|
||||||
|
"message": "Archive"
|
||||||
|
},
|
||||||
|
"unarchive": {
|
||||||
|
"message": "Unarchive"
|
||||||
|
},
|
||||||
|
"itemsInArchive": {
|
||||||
|
"message": "Items in archive"
|
||||||
|
},
|
||||||
|
"noItemsInArchive": {
|
||||||
|
"message": "No items in archive"
|
||||||
|
},
|
||||||
|
"noItemsInArchiveDesc": {
|
||||||
|
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||||
|
},
|
||||||
|
"itemSentToArchive": {
|
||||||
|
"message": "Item sent to archive"
|
||||||
|
},
|
||||||
|
"itemRemovedFromArchive": {
|
||||||
|
"message": "Item removed from archive"
|
||||||
|
},
|
||||||
|
"archiveItem": {
|
||||||
|
"message": "Archive item"
|
||||||
|
},
|
||||||
|
"archiveItemConfirmDesc": {
|
||||||
|
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
||||||
|
},
|
||||||
"edit": {
|
"edit": {
|
||||||
"message": "Edit"
|
"message": "Edit"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
LoginSecondaryContentComponent,
|
LoginSecondaryContentComponent,
|
||||||
LoginViaAuthRequestComponent,
|
LoginViaAuthRequestComponent,
|
||||||
|
NewDeviceVerificationComponent,
|
||||||
PasswordHintComponent,
|
PasswordHintComponent,
|
||||||
RegistrationFinishComponent,
|
RegistrationFinishComponent,
|
||||||
RegistrationStartComponent,
|
RegistrationStartComponent,
|
||||||
@@ -38,7 +39,6 @@ import {
|
|||||||
SsoComponent,
|
SsoComponent,
|
||||||
TwoFactorAuthComponent,
|
TwoFactorAuthComponent,
|
||||||
TwoFactorAuthGuard,
|
TwoFactorAuthGuard,
|
||||||
NewDeviceVerificationComponent,
|
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||||
@@ -80,6 +80,7 @@ import { canAccessAtRiskPasswords } from "../vault/popup/guards/at-risk-password
|
|||||||
import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard";
|
import { clearVaultStateGuard } from "../vault/popup/guards/clear-vault-state.guard";
|
||||||
import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard";
|
import { IntroCarouselGuard } from "../vault/popup/guards/intro-carousel.guard";
|
||||||
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
||||||
|
import { ArchiveComponent } from "../vault/popup/settings/archive.component";
|
||||||
import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component";
|
import { DownloadBitwardenComponent } from "../vault/popup/settings/download-bitwarden.component";
|
||||||
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
||||||
import { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component";
|
import { MoreFromBitwardenPageV2Component } from "../vault/popup/settings/more-from-bitwarden-page-v2.component";
|
||||||
@@ -675,6 +676,12 @@ const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "archive",
|
||||||
|
component: ArchiveComponent,
|
||||||
|
canActivate: [authGuard],
|
||||||
|
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "security",
|
path: "security",
|
||||||
component: AnonLayoutWrapperComponent,
|
component: AnonLayoutWrapperComponent,
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ import {
|
|||||||
DefaultSshImportPromptService,
|
DefaultSshImportPromptService,
|
||||||
PasswordRepromptService,
|
PasswordRepromptService,
|
||||||
SshImportPromptService,
|
SshImportPromptService,
|
||||||
|
CipherArchiveService,
|
||||||
|
DefaultCipherArchiveService,
|
||||||
} from "@bitwarden/vault";
|
} from "@bitwarden/vault";
|
||||||
|
|
||||||
import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service";
|
import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service";
|
||||||
@@ -703,6 +705,18 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: ExtensionDeviceManagementComponentService,
|
useClass: ExtensionDeviceManagementComponentService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CipherArchiveService,
|
||||||
|
useClass: DefaultCipherArchiveService,
|
||||||
|
deps: [
|
||||||
|
CipherService,
|
||||||
|
ApiService,
|
||||||
|
DialogService,
|
||||||
|
PasswordRepromptService,
|
||||||
|
BillingAccountProfileStateService,
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -38,5 +38,10 @@
|
|||||||
{{ "assignToCollections" | i18n }}
|
{{ "assignToCollections" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@if (canArchive$ | async) {
|
||||||
|
<button type="button" bitMenuItem (click)="archive()" *ngIf="canArchive$ | async">
|
||||||
|
{{ "archive" | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</bit-menu>
|
</bit-menu>
|
||||||
</bit-item-action>
|
</bit-item-action>
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
import { Router, RouterModule } from "@angular/router";
|
||||||
import { BehaviorSubject, combineLatest, filter, firstValueFrom, map, switchMap } from "rxjs";
|
import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs";
|
||||||
|
import { filter } from "rxjs/operators";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
@@ -11,6 +12,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
@@ -26,7 +28,7 @@ import {
|
|||||||
MenuModule,
|
MenuModule,
|
||||||
ToastService,
|
ToastService,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { CipherArchiveService, PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||||
@@ -103,6 +105,20 @@ export class ItemMoreOptionsComponent {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Observable Boolean checking if item can show Archive menu option */
|
||||||
|
protected canArchive$ = combineLatest([
|
||||||
|
this._cipher$,
|
||||||
|
this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||||
|
),
|
||||||
|
]).pipe(
|
||||||
|
filter(([cipher, userId]) => cipher != null && userId != null),
|
||||||
|
map(([cipher, canArchive]) => {
|
||||||
|
return canArchive && !CipherViewLikeUtils.isArchived(cipher) && cipher.organizationId == null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
@@ -116,6 +132,7 @@ export class ItemMoreOptionsComponent {
|
|||||||
private cipherAuthorizationService: CipherAuthorizationService,
|
private cipherAuthorizationService: CipherAuthorizationService,
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
|
private cipherArchiveService: CipherArchiveService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get canEdit() {
|
get canEdit() {
|
||||||
@@ -233,4 +250,23 @@ export class ItemMoreOptionsComponent {
|
|||||||
queryParams: { cipherId: this.cipher.id },
|
queryParams: { cipherId: this.cipher.id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async archive() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "archiveItem" },
|
||||||
|
content: { key: "archiveItemConfirmDesc" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
|
await this.cipherArchiveService.archiveWithServer(this.cipher.id as CipherId, activeUserId);
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
message: this.i18nService.t("itemSentToArchive"),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { WritableSignal, signal } from "@angular/core";
|
import { signal, WritableSignal } from "@angular/core";
|
||||||
import { TestBed } from "@angular/core/testing";
|
import { TestBed } from "@angular/core/testing";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, of, take, timeout } from "rxjs";
|
||||||
@@ -8,10 +8,11 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
|||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec";
|
import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec";
|
||||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
RestrictedItemTypesService,
|
RestrictedItemTypesService,
|
||||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||||
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
import { CipherViewLikeUtils } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
|
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
@@ -43,7 +45,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
|
|
||||||
let mockOrg: Organization;
|
let mockOrg: Organization;
|
||||||
let mockCollections: CollectionView[];
|
let mockCollections: CollectionView[];
|
||||||
let activeUserLastSync$: BehaviorSubject<Date>;
|
let activeUserLastSync$: BehaviorSubject<Date | null>;
|
||||||
let viewCacheService: {
|
let viewCacheService: {
|
||||||
signal: jest.Mock;
|
signal: jest.Mock;
|
||||||
mockSignal: WritableSignal<string | null>;
|
mockSignal: WritableSignal<string | null>;
|
||||||
@@ -64,6 +66,9 @@ describe("VaultPopupItemsService", () => {
|
|||||||
const inlineMenuFieldQualificationServiceMock = mock<InlineMenuFieldQualificationService>();
|
const inlineMenuFieldQualificationServiceMock = mock<InlineMenuFieldQualificationService>();
|
||||||
const userId = Utils.newGuid() as UserId;
|
const userId = Utils.newGuid() as UserId;
|
||||||
const accountServiceMock = mockAccountServiceWith(userId);
|
const accountServiceMock = mockAccountServiceWith(userId);
|
||||||
|
const configServiceMock = mock<ConfigService>();
|
||||||
|
const cipherArchiveServiceMock = mock<CipherArchiveService>();
|
||||||
|
cipherArchiveServiceMock.userCanArchive$.mockReturnValue(of(true));
|
||||||
|
|
||||||
const restrictedItemTypesService = {
|
const restrictedItemTypesService = {
|
||||||
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),
|
||||||
@@ -101,7 +106,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
failedToDecryptCiphersSubject.asObservable(),
|
failedToDecryptCiphersSubject.asObservable(),
|
||||||
);
|
);
|
||||||
|
|
||||||
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers);
|
searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers!);
|
||||||
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
|
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
|
||||||
ciphers.filter((c) => ["0", "1"].includes(uuidAsString(c.id))),
|
ciphers.filter((c) => ["0", "1"].includes(uuidAsString(c.id))),
|
||||||
);
|
);
|
||||||
@@ -142,8 +147,9 @@ describe("VaultPopupItemsService", () => {
|
|||||||
organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg]));
|
organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg]));
|
||||||
collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections));
|
collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections));
|
||||||
|
|
||||||
activeUserLastSync$ = new BehaviorSubject(new Date());
|
activeUserLastSync$ = new BehaviorSubject<Date | null>(new Date());
|
||||||
syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$);
|
syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$);
|
||||||
|
configServiceMock.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
|
||||||
const testSearchSignal = createMockSignal<string | null>("");
|
const testSearchSignal = createMockSignal<string | null>("");
|
||||||
viewCacheService = {
|
viewCacheService = {
|
||||||
@@ -168,10 +174,15 @@ describe("VaultPopupItemsService", () => {
|
|||||||
useValue: inlineMenuFieldQualificationServiceMock,
|
useValue: inlineMenuFieldQualificationServiceMock,
|
||||||
},
|
},
|
||||||
{ provide: PopupViewCacheService, useValue: viewCacheService },
|
{ provide: PopupViewCacheService, useValue: viewCacheService },
|
||||||
|
{ provide: ConfigService, useValue: configServiceMock },
|
||||||
{
|
{
|
||||||
provide: RestrictedItemTypesService,
|
provide: RestrictedItemTypesService,
|
||||||
useValue: restrictedItemTypesService,
|
useValue: restrictedItemTypesService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CipherArchiveService,
|
||||||
|
useValue: cipherArchiveServiceMock,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -297,7 +308,7 @@ describe("VaultPopupItemsService", () => {
|
|||||||
const searchText = "Login";
|
const searchText = "Login";
|
||||||
|
|
||||||
searchService.searchCiphers.mockImplementation(async (userId, q, _, ciphers) => {
|
searchService.searchCiphers.mockImplementation(async (userId, q, _, ciphers) => {
|
||||||
return ciphers.filter((cipher) => {
|
return ciphers!.filter((cipher) => {
|
||||||
return cipher.name.includes(searchText);
|
return cipher.name.includes(searchText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -390,12 +401,12 @@ describe("VaultPopupItemsService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true when all ciphers are deleted", (done) => {
|
it("should return true when all ciphers are deleted/archived", (done) => {
|
||||||
cipherServiceMock.cipherListViews$.mockReturnValue(
|
cipherServiceMock.cipherListViews$.mockReturnValue(
|
||||||
of([
|
of([
|
||||||
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
|
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
|
||||||
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
|
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
|
||||||
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true },
|
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: false, isArchived: true },
|
||||||
] as CipherView[]),
|
] as CipherView[]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { CollectionService } from "@bitwarden/admin-console/common";
|
|||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||||
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
CipherViewLike,
|
CipherViewLike,
|
||||||
CipherViewLikeUtils,
|
CipherViewLikeUtils,
|
||||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
|
import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator";
|
||||||
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
|
import { PopupViewCacheService } from "../../../platform/popup/view-cache/popup-view-cache.service";
|
||||||
@@ -133,14 +135,24 @@ export class VaultPopupItemsService {
|
|||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private userCanArchive$ = this.activeUserId$.pipe(
|
||||||
|
switchMap((userId) => {
|
||||||
|
return this.cipherArchiveService.userCanArchive$(userId);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
|
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
|
||||||
switchMap((ciphers) =>
|
switchMap((ciphers) =>
|
||||||
combineLatest([this.organizations$, this.decryptedCollections$]).pipe(
|
combineLatest([this.organizations$, this.decryptedCollections$, this.userCanArchive$]).pipe(
|
||||||
map(([organizations, collections]) => {
|
map(([organizations, collections, canArchive]) => {
|
||||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||||
return ciphers
|
return ciphers
|
||||||
.filter((c) => !CipherViewLikeUtils.isDeleted(c))
|
.filter(
|
||||||
|
(c) =>
|
||||||
|
!CipherViewLikeUtils.isDeleted(c) &&
|
||||||
|
(!canArchive || !CipherViewLikeUtils.isArchived(c)),
|
||||||
|
)
|
||||||
.map((cipher) => {
|
.map((cipher) => {
|
||||||
(cipher as PopupCipherViewLike).collections = cipher.collectionIds?.map(
|
(cipher as PopupCipherViewLike).collections = cipher.collectionIds?.map(
|
||||||
(colId) => collectionMap[colId as CollectionId],
|
(colId) => collectionMap[colId as CollectionId],
|
||||||
@@ -330,6 +342,8 @@ export class VaultPopupItemsService {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
private cipherArchiveService: CipherArchiveService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
applyFilter(newSearchText: string) {
|
applyFilter(newSearchText: string) {
|
||||||
|
|||||||
81
apps/browser/src/vault/popup/settings/archive.component.html
Normal file
81
apps/browser/src/vault/popup/settings/archive.component.html
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<popup-page [loading]="loading$ | async">
|
||||||
|
<popup-header slot="header" [pageTitle]="'archive' | i18n" showBackButton>
|
||||||
|
<ng-container slot="end">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</ng-container>
|
||||||
|
</popup-header>
|
||||||
|
|
||||||
|
@if (archivedCiphers$ | async; as archivedItems) {
|
||||||
|
@if (archivedItems.length) {
|
||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">
|
||||||
|
{{ "itemsInArchive" | i18n }}
|
||||||
|
</h2>
|
||||||
|
<span bitTypography="body1" slot="end">{{ archivedItems.length }}</span>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-item-group>
|
||||||
|
@for (cipher of archivedItems; track cipher.id) {
|
||||||
|
<bit-item>
|
||||||
|
<button
|
||||||
|
bit-item-content
|
||||||
|
type="button"
|
||||||
|
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||||
|
(click)="view(cipher)"
|
||||||
|
>
|
||||||
|
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
|
||||||
|
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||||
|
</div>
|
||||||
|
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||||
|
@if (cipher.hasAttachments) {
|
||||||
|
<i class="bwi bwi-paperclip bwi-sm" [appA11yTitle]="'attachments' | i18n"></i>
|
||||||
|
}
|
||||||
|
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||||
|
</button>
|
||||||
|
<bit-item-action slot="end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
label="{{ 'options' | i18n }}"
|
||||||
|
[appA11yTitle]="'moreOptionsTitle' | i18n: cipher.name"
|
||||||
|
[bitMenuTriggerFor]="moreOptions"
|
||||||
|
></button>
|
||||||
|
<bit-menu #moreOptions>
|
||||||
|
<button type="button" bitMenuItem (click)="edit(cipher)">
|
||||||
|
{{ "edit" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="clone(cipher)">
|
||||||
|
{{ "clone" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="unarchive(cipher)">
|
||||||
|
{{ "unarchive" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
*appCanDeleteCipher="cipher"
|
||||||
|
(click)="delete(cipher)"
|
||||||
|
>
|
||||||
|
<span class="tw-text-danger">
|
||||||
|
{{ "delete" | i18n }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</bit-item-action>
|
||||||
|
</bit-item>
|
||||||
|
}
|
||||||
|
</bit-item-group>
|
||||||
|
</bit-section>
|
||||||
|
} @else {
|
||||||
|
<bit-no-items class="tw-flex tw-h-full tw-items-center tw-justify-center">
|
||||||
|
<ng-container slot="title">
|
||||||
|
{{ "noItemsInArchive" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container slot="description">
|
||||||
|
{{ "noItemsInArchiveDesc" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
</bit-no-items>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</popup-page>
|
||||||
159
apps/browser/src/vault/popup/settings/archive.component.ts
Normal file
159
apps/browser/src/vault/popup/settings/archive.component.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Component, inject } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom, map, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import {
|
||||||
|
DialogService,
|
||||||
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
|
MenuModule,
|
||||||
|
NoItemsModule,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
ToastService,
|
||||||
|
TypographyModule,
|
||||||
|
} from "@bitwarden/components";
|
||||||
|
import { CanDeleteCipherDirective, CipherArchiveService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
|
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
templateUrl: "archive.component.html",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
PopupPageComponent,
|
||||||
|
PopupHeaderComponent,
|
||||||
|
PopOutComponent,
|
||||||
|
NoItemsModule,
|
||||||
|
ItemModule,
|
||||||
|
MenuModule,
|
||||||
|
IconButtonModule,
|
||||||
|
CanDeleteCipherDirective,
|
||||||
|
SectionComponent,
|
||||||
|
SectionHeaderComponent,
|
||||||
|
TypographyModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ArchiveComponent {
|
||||||
|
private dialogService = inject(DialogService);
|
||||||
|
private router = inject(Router);
|
||||||
|
private cipherService = inject(CipherService);
|
||||||
|
private accountService = inject(AccountService);
|
||||||
|
private logService = inject(LogService);
|
||||||
|
private toastService = inject(ToastService);
|
||||||
|
private i18nService = inject(I18nService);
|
||||||
|
private cipherArchiveService = inject(CipherArchiveService);
|
||||||
|
|
||||||
|
private userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
|
|
||||||
|
protected archivedCiphers$ = this.userId$.pipe(
|
||||||
|
switchMap((userId) => this.cipherArchiveService.archivedCiphers$(userId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
protected loading$ = this.archivedCiphers$.pipe(
|
||||||
|
map(() => false),
|
||||||
|
startWith(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
async view(cipher: CipherView) {
|
||||||
|
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate(["/view-cipher"], {
|
||||||
|
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async edit(cipher: CipherView) {
|
||||||
|
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate(["/edit-cipher"], {
|
||||||
|
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(cipher: CipherView) {
|
||||||
|
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "deleteItem" },
|
||||||
|
content: { key: "deleteItemConfirmation" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.cipherService.softDeleteWithServer(cipher.id, activeUserId);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
message: this.i18nService.t("deletedItem"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unarchive(cipher: CipherView) {
|
||||||
|
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeUserId = await firstValueFrom(this.userId$);
|
||||||
|
|
||||||
|
await this.cipherArchiveService.unarchiveWithServer(cipher.id as CipherId, activeUserId);
|
||||||
|
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "success",
|
||||||
|
message: this.i18nService.t("itemRemovedFromArchive"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clone(cipher: CipherView) {
|
||||||
|
if (!(await this.cipherArchiveService.canInteract(cipher))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipher.login?.hasFido2Credentials) {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "passkeyNotCopied" },
|
||||||
|
content: { key: "passkeyNotCopiedAlert" },
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.router.navigate(["/clone-cipher"], {
|
||||||
|
queryParams: {
|
||||||
|
clone: true.toString(),
|
||||||
|
cipherId: cipher.id,
|
||||||
|
type: cipher.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,14 @@
|
|||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
|
@if (userCanArchive() || showArchiveFilter()) {
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="/archive">
|
||||||
|
{{ "archive" | i18n }}
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
}
|
||||||
<bit-item>
|
<bit-item>
|
||||||
<a bit-item-content routerLink="/trash">
|
<a bit-item-content routerLink="/trash">
|
||||||
{{ "trash" | i18n }}
|
{{ "trash" | i18n }}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { toSignal } from "@angular/core/rxjs-interop";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
import { Router, RouterModule } from "@angular/router";
|
||||||
import { firstValueFrom, switchMap } from "rxjs";
|
import { firstValueFrom, switchMap } from "rxjs";
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils";
|
||||||
@@ -32,6 +34,17 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co
|
|||||||
})
|
})
|
||||||
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||||
lastSync = "--";
|
lastSync = "--";
|
||||||
|
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
|
|
||||||
|
// Check if user is premium user, they will be able to archive items
|
||||||
|
protected userCanArchive = toSignal(
|
||||||
|
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if user has archived items (does not check if user is premium)
|
||||||
|
protected showArchiveFilter = toSignal(
|
||||||
|
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))),
|
||||||
|
);
|
||||||
|
|
||||||
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
|
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
|
||||||
getUserId,
|
getUserId,
|
||||||
@@ -47,6 +60,7 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private nudgeService: NudgesService,
|
private nudgeService: NudgesService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private cipherArchiveService: CipherArchiveService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
|||||||
@@ -4107,5 +4107,32 @@
|
|||||||
},
|
},
|
||||||
"editShortcut": {
|
"editShortcut": {
|
||||||
"message": "Edit shortcut"
|
"message": "Edit shortcut"
|
||||||
|
},
|
||||||
|
"archive": {
|
||||||
|
"message": "Archive"
|
||||||
|
},
|
||||||
|
"unarchive": {
|
||||||
|
"message": "Unarchive"
|
||||||
|
},
|
||||||
|
"itemsInArchive": {
|
||||||
|
"message": "Items in archive"
|
||||||
|
},
|
||||||
|
"noItemsInArchive": {
|
||||||
|
"message": "No items in archive"
|
||||||
|
},
|
||||||
|
"noItemsInArchiveDesc": {
|
||||||
|
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||||
|
},
|
||||||
|
"itemSentToArchive": {
|
||||||
|
"message": "Item sent to archive"
|
||||||
|
},
|
||||||
|
"itemRemovedFromArchive": {
|
||||||
|
"message": "Item removed from archive"
|
||||||
|
},
|
||||||
|
"archiveItem": {
|
||||||
|
"message": "Archive item"
|
||||||
|
},
|
||||||
|
"archiveItemConfirmDesc": {
|
||||||
|
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
|||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/vault";
|
||||||
|
|
||||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||||
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||||
@@ -51,10 +51,10 @@ export class VaultFilterComponent
|
|||||||
protected toastService: ToastService,
|
protected toastService: ToastService,
|
||||||
protected billingApiService: BillingApiServiceAbstraction,
|
protected billingApiService: BillingApiServiceAbstraction,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected configService: ConfigService,
|
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
protected cipherService: CipherService,
|
protected cipherService: CipherService,
|
||||||
|
protected cipherArchiveService: CipherArchiveService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
vaultFilterService,
|
vaultFilterService,
|
||||||
@@ -64,10 +64,10 @@ export class VaultFilterComponent
|
|||||||
toastService,
|
toastService,
|
||||||
billingApiService,
|
billingApiService,
|
||||||
dialogService,
|
dialogService,
|
||||||
configService,
|
|
||||||
accountService,
|
accountService,
|
||||||
restrictedItemTypesService,
|
restrictedItemTypesService,
|
||||||
cipherService,
|
cipherService,
|
||||||
|
cipherArchiveService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@@ -25,6 +24,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
|||||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/vault";
|
||||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||||
|
|
||||||
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
|
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
|
||||||
@@ -112,6 +112,9 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
if (this.activeFilter.isDeleted) {
|
if (this.activeFilter.isDeleted) {
|
||||||
return "searchTrash";
|
return "searchTrash";
|
||||||
}
|
}
|
||||||
|
if (this.activeFilter.isArchived) {
|
||||||
|
return "searchArchive";
|
||||||
|
}
|
||||||
if (this.activeFilter.cipherType === CipherType.Login) {
|
if (this.activeFilter.cipherType === CipherType.Login) {
|
||||||
return "searchLogin";
|
return "searchLogin";
|
||||||
}
|
}
|
||||||
@@ -153,10 +156,10 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
protected toastService: ToastService,
|
protected toastService: ToastService,
|
||||||
protected billingApiService: BillingApiServiceAbstraction,
|
protected billingApiService: BillingApiServiceAbstraction,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected configService: ConfigService,
|
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
protected cipherService: CipherService,
|
protected cipherService: CipherService,
|
||||||
|
protected cipherArchiveService: CipherArchiveService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
@@ -248,11 +251,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async buildAllFilters(): Promise<VaultFilterList> {
|
async buildAllFilters(): Promise<VaultFilterList> {
|
||||||
|
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
const builderFilter = {} as VaultFilterList;
|
const builderFilter = {} as VaultFilterList;
|
||||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||||
builderFilter.typeFilter = await this.addTypeFilter();
|
builderFilter.typeFilter = await this.addTypeFilter();
|
||||||
builderFilter.folderFilter = await this.addFolderFilter();
|
builderFilter.folderFilter = await this.addFolderFilter();
|
||||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||||
|
if (
|
||||||
|
(await firstValueFrom(this.cipherArchiveService.userCanArchive$(userId))) ||
|
||||||
|
(await firstValueFrom(this.cipherArchiveService.showArchiveVault$(userId)))
|
||||||
|
) {
|
||||||
|
builderFilter.archiveFilter = await this.addArchiveFilter();
|
||||||
|
}
|
||||||
builderFilter.trashFilter = await this.addTrashFilter();
|
builderFilter.trashFilter = await this.addTrashFilter();
|
||||||
return builderFilter;
|
return builderFilter;
|
||||||
}
|
}
|
||||||
@@ -412,4 +422,31 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
return trashFilterSection;
|
return trashFilterSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async addArchiveFilter(): Promise<VaultFilterSection> {
|
||||||
|
const archiveFilterSection: VaultFilterSection = {
|
||||||
|
data$: this.vaultFilterService.buildTypeTree(
|
||||||
|
{
|
||||||
|
id: "headArchive",
|
||||||
|
name: "HeadArchive",
|
||||||
|
type: "archive",
|
||||||
|
icon: "bwi-archive",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "archive",
|
||||||
|
name: this.i18nService.t("archive"),
|
||||||
|
type: "archive",
|
||||||
|
icon: "bwi-archive",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
header: {
|
||||||
|
showHeader: false,
|
||||||
|
isSelectable: true,
|
||||||
|
},
|
||||||
|
action: this.applyTypeFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||||
|
};
|
||||||
|
return archiveFilterSection;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,11 @@ function createLegacyFilterForEndUser(
|
|||||||
{ id: "trash", name: "", type: "trash", icon: "" },
|
{ id: "trash", name: "", type: "trash", icon: "" },
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
} else if (filter.type !== undefined && filter.type === "archive") {
|
||||||
|
legacyFilter.selectedCipherTypeNode = new TreeNode<CipherTypeFilter>(
|
||||||
|
{ id: "archive", name: "", type: "archive", icon: "" },
|
||||||
|
null,
|
||||||
|
);
|
||||||
} else if (filter.type !== undefined && filter.type !== "trash") {
|
} else if (filter.type !== undefined && filter.type !== "trash") {
|
||||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||||
cipherTypeTree,
|
cipherTypeTree,
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
|||||||
|
|
||||||
export type FilterFunction = (cipher: CipherViewLike) => boolean;
|
export type FilterFunction = (cipher: CipherViewLike) => boolean;
|
||||||
|
|
||||||
export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction {
|
export function createFilterFunction(
|
||||||
|
filter: RoutedVaultFilterModel,
|
||||||
|
archiveEnabled?: boolean,
|
||||||
|
): FilterFunction {
|
||||||
return (cipher) => {
|
return (cipher) => {
|
||||||
const type = CipherViewLikeUtils.getType(cipher);
|
const type = CipherViewLikeUtils.getType(cipher);
|
||||||
const isDeleted = CipherViewLikeUtils.isDeleted(cipher);
|
const isDeleted = CipherViewLikeUtils.isDeleted(cipher);
|
||||||
@@ -39,6 +42,15 @@ export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunc
|
|||||||
if (filter.type !== "trash" && isDeleted) {
|
if (filter.type !== "trash" && isDeleted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Archive filter logic is only applied if the feature flag is enabled
|
||||||
|
if (archiveEnabled) {
|
||||||
|
if (filter.type === "archive" && !CipherViewLikeUtils.isArchived(cipher)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filter.type !== "archive" && CipherViewLikeUtils.isArchived(cipher)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
// No folder
|
// No folder
|
||||||
if (filter.folderId === Unassigned && cipher.folderId != null) {
|
if (filter.folderId === Unassigned && cipher.folderId != null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ export class RoutedVaultFilterBridge implements VaultFilter {
|
|||||||
get isDeleted(): boolean {
|
get isDeleted(): boolean {
|
||||||
return this.legacyFilter.isDeleted;
|
return this.legacyFilter.isDeleted;
|
||||||
}
|
}
|
||||||
|
get isArchived(): boolean {
|
||||||
|
return this.legacyFilter.isArchived;
|
||||||
|
}
|
||||||
get organizationId(): string {
|
get organizationId(): string {
|
||||||
return this.legacyFilter.organizationId;
|
return this.legacyFilter.organizationId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const itemTypes = [
|
|||||||
"identity",
|
"identity",
|
||||||
"note",
|
"note",
|
||||||
"sshKey",
|
"sshKey",
|
||||||
|
"archive",
|
||||||
"trash",
|
"trash",
|
||||||
All,
|
All,
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const VaultFilterLabel = {
|
|||||||
TypeFilter: "typeFilter",
|
TypeFilter: "typeFilter",
|
||||||
FolderFilter: "folderFilter",
|
FolderFilter: "folderFilter",
|
||||||
CollectionFilter: "collectionFilter",
|
CollectionFilter: "collectionFilter",
|
||||||
|
ArchiveFilter: "archiveFilter",
|
||||||
TrashFilter: "trashFilter",
|
TrashFilter: "trashFilter",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ export class VaultFilter {
|
|||||||
return this.selectedCipherTypeNode?.node.type === "trash" ? true : null;
|
return this.selectedCipherTypeNode?.node.type === "trash" ? true : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isArchived(): boolean {
|
||||||
|
return this.selectedCipherTypeNode?.node.type === "archive";
|
||||||
|
}
|
||||||
|
|
||||||
get organizationId(): string {
|
get organizationId(): string {
|
||||||
return this.selectedOrganizationNode?.node.id;
|
return this.selectedOrganizationNode?.node.id;
|
||||||
}
|
}
|
||||||
@@ -121,6 +125,9 @@ export class VaultFilter {
|
|||||||
if (this.isDeleted && cipherPassesFilter) {
|
if (this.isDeleted && cipherPassesFilter) {
|
||||||
cipherPassesFilter = cipher.isDeleted;
|
cipherPassesFilter = cipher.isDeleted;
|
||||||
}
|
}
|
||||||
|
if (this.isArchived && cipherPassesFilter) {
|
||||||
|
cipherPassesFilter = cipher.isArchived;
|
||||||
|
}
|
||||||
if (this.cipherType && cipherPassesFilter) {
|
if (this.cipherType && cipherPassesFilter) {
|
||||||
cipherPassesFilter = cipher.type === this.cipherType;
|
cipherPassesFilter = cipher.type === this.cipherType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
|||||||
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
export type CipherStatus = "all" | "favorites" | "trash" | CipherType;
|
export type CipherStatus = "all" | "favorites" | "archive" | "trash" | CipherType;
|
||||||
|
|
||||||
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
|
export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string };
|
||||||
export type CollectionFilter = CollectionAdminView & {
|
export type CollectionFilter = CollectionAdminView & {
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ export class VaultHeaderComponent {
|
|||||||
return this.i18nService.t("myVault");
|
return this.i18nService.t("myVault");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.filter.type === "archive") {
|
||||||
|
return this.i18nService.t("archive");
|
||||||
|
}
|
||||||
|
|
||||||
const activeOrganization = this.activeOrganization;
|
const activeOrganization = this.activeOrganization;
|
||||||
if (activeOrganization) {
|
if (activeOrganization) {
|
||||||
return `${activeOrganization.name} ${this.i18nService.t("vault").toLowerCase()}`;
|
return `${activeOrganization.name} ${this.i18nService.t("vault").toLowerCase()}`;
|
||||||
|
|||||||
@@ -69,14 +69,18 @@
|
|||||||
*ngIf="isEmpty && !performingInitialLoad"
|
*ngIf="isEmpty && !performingInitialLoad"
|
||||||
>
|
>
|
||||||
<bit-no-items [icon]="noItemIcon">
|
<bit-no-items [icon]="noItemIcon">
|
||||||
<div slot="title">{{ "noItemsInList" | i18n }}</div>
|
<div slot="title" *ngIf="filter.type === 'archive'">{{ "noItemsInArchive" | i18n }}</div>
|
||||||
|
<p slot="description" class="tw-text-center tw-max-w-md" *ngIf="filter.type === 'archive'">
|
||||||
|
{{ "archivedItemsDescription" | i18n }}
|
||||||
|
</p>
|
||||||
|
<div slot="title" *ngIf="filter.type !== 'archive'">{{ "noItemsInList" | i18n }}</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
bitButton
|
bitButton
|
||||||
(click)="addCipher()"
|
(click)="addCipher()"
|
||||||
*ngIf="filter.type !== 'trash'"
|
|
||||||
slot="button"
|
slot="button"
|
||||||
|
*ngIf="filter.type !== 'trash' && filter.type !== 'archive'"
|
||||||
>
|
>
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
{{ "newItem" | i18n }}
|
{{ "newItem" | i18n }}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import {
|
|||||||
AttachmentDialogCloseResult,
|
AttachmentDialogCloseResult,
|
||||||
AttachmentDialogResult,
|
AttachmentDialogResult,
|
||||||
AttachmentsV2Component,
|
AttachmentsV2Component,
|
||||||
|
CipherArchiveService,
|
||||||
CipherFormConfig,
|
CipherFormConfig,
|
||||||
CollectionAssignmentResult,
|
CollectionAssignmentResult,
|
||||||
DecryptionFailureDialogComponent,
|
DecryptionFailureDialogComponent,
|
||||||
@@ -183,6 +184,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
.pipe(map((a) => a?.id))
|
.pipe(map((a) => a?.id))
|
||||||
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
||||||
|
|
||||||
|
private userCanArchive$ = this.accountService.activeAccount$.pipe(
|
||||||
|
getUserId,
|
||||||
|
switchMap((userId) => {
|
||||||
|
return this.cipherArchiveService.userCanArchive$(userId);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -213,6 +221,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
private cipherFormConfigService: DefaultCipherFormConfigService,
|
private cipherFormConfigService: DefaultCipherFormConfigService,
|
||||||
protected billingApiService: BillingApiServiceAbstraction,
|
protected billingApiService: BillingApiServiceAbstraction,
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
|
private cipherArchiveService: CipherArchiveService,
|
||||||
private organizationWarningsService: OrganizationWarningsService,
|
private organizationWarningsService: OrganizationWarningsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -309,12 +318,17 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ciphers$ = combineLatest([allowedCiphers$, filter$, this.currentSearchText$]).pipe(
|
const ciphers$ = combineLatest([
|
||||||
|
allowedCiphers$,
|
||||||
|
filter$,
|
||||||
|
this.currentSearchText$,
|
||||||
|
this.userCanArchive$,
|
||||||
|
]).pipe(
|
||||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||||
concatMap(async ([ciphers, filter, searchText]) => {
|
concatMap(async ([ciphers, filter, searchText, archiveEnabled]) => {
|
||||||
const failedCiphers =
|
const failedCiphers =
|
||||||
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
||||||
const filterFunction = createFilterFunction(filter);
|
const filterFunction = createFilterFunction(filter, archiveEnabled);
|
||||||
// Append any failed to decrypt ciphers to the top of the cipher list
|
// Append any failed to decrypt ciphers to the top of the cipher list
|
||||||
const allCiphers = [...failedCiphers, ...ciphers];
|
const allCiphers = [...failedCiphers, ...ciphers];
|
||||||
|
|
||||||
|
|||||||
@@ -11009,6 +11009,18 @@
|
|||||||
"cannotCreateCollection": {
|
"cannotCreateCollection": {
|
||||||
"message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections."
|
"message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections."
|
||||||
},
|
},
|
||||||
|
"searchArchive": {
|
||||||
|
"message": "Search archive"
|
||||||
|
},
|
||||||
|
"archive": {
|
||||||
|
"message": "Archive"
|
||||||
|
},
|
||||||
|
"noItemsInArchive": {
|
||||||
|
"message": "No items in archive"
|
||||||
|
},
|
||||||
|
"archivedItemsDescription": {
|
||||||
|
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||||
|
},
|
||||||
"businessUnit": {
|
"businessUnit": {
|
||||||
"message": "Business Unit"
|
"message": "Business Unit"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
|
|||||||
import {
|
import {
|
||||||
AnonLayoutWrapperDataService,
|
AnonLayoutWrapperDataService,
|
||||||
DefaultAnonLayoutWrapperDataService,
|
DefaultAnonLayoutWrapperDataService,
|
||||||
|
DialogService,
|
||||||
ToastService,
|
ToastService,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import {
|
import {
|
||||||
@@ -340,7 +341,11 @@ import {
|
|||||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import {
|
||||||
|
CipherArchiveService,
|
||||||
|
DefaultCipherArchiveService,
|
||||||
|
PasswordRepromptService,
|
||||||
|
} from "@bitwarden/vault";
|
||||||
import {
|
import {
|
||||||
IndividualVaultExportService,
|
IndividualVaultExportService,
|
||||||
IndividualVaultExportServiceAbstraction,
|
IndividualVaultExportServiceAbstraction,
|
||||||
@@ -1620,6 +1625,18 @@ const safeProviders: SafeProvider[] = [
|
|||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: CipherArchiveService,
|
||||||
|
useClass: DefaultCipherArchiveService,
|
||||||
|
deps: [
|
||||||
|
CipherServiceAbstraction,
|
||||||
|
ApiServiceAbstraction,
|
||||||
|
DialogService,
|
||||||
|
PasswordRepromptService,
|
||||||
|
BillingAccountProfileStateService,
|
||||||
|
ConfigService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ export enum FeatureFlag {
|
|||||||
IpcChannelFramework = "ipc-channel-framework",
|
IpcChannelFramework = "ipc-channel-framework",
|
||||||
InactiveUserServerNotification = "pm-25130-receive-push-notifications-for-inactive-users",
|
InactiveUserServerNotification = "pm-25130-receive-push-notifications-for-inactive-users",
|
||||||
PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked",
|
PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked",
|
||||||
|
|
||||||
|
/* Innovation */
|
||||||
|
PM19148_InnovationArchive = "pm-19148-innovation-archive",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||||
@@ -112,6 +115,9 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.IpcChannelFramework]: FALSE,
|
[FeatureFlag.IpcChannelFramework]: FALSE,
|
||||||
[FeatureFlag.InactiveUserServerNotification]: FALSE,
|
[FeatureFlag.InactiveUserServerNotification]: FALSE,
|
||||||
[FeatureFlag.PushNotificationsWhenLocked]: FALSE,
|
[FeatureFlag.PushNotificationsWhenLocked]: FALSE,
|
||||||
|
|
||||||
|
/* Innovation */
|
||||||
|
[FeatureFlag.PM19148_InnovationArchive]: FALSE,
|
||||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||||
|
|
||||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export class CipherExport {
|
|||||||
req.creationDate = null;
|
req.creationDate = null;
|
||||||
req.revisionDate = null;
|
req.revisionDate = null;
|
||||||
req.deletedDate = null;
|
req.deletedDate = null;
|
||||||
|
req.archivedDate = null;
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ export class CipherExport {
|
|||||||
view.creationDate = req.creationDate ? new Date(req.creationDate) : view.creationDate;
|
view.creationDate = req.creationDate ? new Date(req.creationDate) : view.creationDate;
|
||||||
view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : view.revisionDate;
|
view.revisionDate = req.revisionDate ? new Date(req.revisionDate) : view.revisionDate;
|
||||||
view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : view.deletedDate;
|
view.deletedDate = req.deletedDate ? new Date(req.deletedDate) : view.deletedDate;
|
||||||
|
view.archivedDate = req.archivedDate ? new Date(req.archivedDate) : view.archivedDate;
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +130,7 @@ export class CipherExport {
|
|||||||
domain.creationDate = req.creationDate ? new Date(req.creationDate) : null;
|
domain.creationDate = req.creationDate ? new Date(req.creationDate) : null;
|
||||||
domain.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
|
domain.revisionDate = req.revisionDate ? new Date(req.revisionDate) : null;
|
||||||
domain.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
|
domain.deletedDate = req.deletedDate ? new Date(req.deletedDate) : null;
|
||||||
|
domain.archivedDate = req.archivedDate ? new Date(req.archivedDate) : null;
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +152,7 @@ export class CipherExport {
|
|||||||
revisionDate: Date = null;
|
revisionDate: Date = null;
|
||||||
creationDate: Date = null;
|
creationDate: Date = null;
|
||||||
deletedDate: Date = null;
|
deletedDate: Date = null;
|
||||||
|
archivedDate: Date = null;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
// Use build method instead of ctor so that we can control order of JSON stringify for pretty print
|
// Use build method instead of ctor so that we can control order of JSON stringify for pretty print
|
||||||
@@ -195,5 +199,6 @@ export class CipherExport {
|
|||||||
this.creationDate = o.creationDate;
|
this.creationDate = o.creationDate;
|
||||||
this.revisionDate = o.revisionDate;
|
this.revisionDate = o.revisionDate;
|
||||||
this.deletedDate = o.deletedDate;
|
this.deletedDate = o.deletedDate;
|
||||||
|
this.archivedDate = o.archivedDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ export class CipherData {
|
|||||||
passwordHistory?: PasswordHistoryData[];
|
passwordHistory?: PasswordHistoryData[];
|
||||||
collectionIds?: string[];
|
collectionIds?: string[];
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
deletedDate: string | null;
|
deletedDate: string | undefined;
|
||||||
|
archivedDate: string | undefined;
|
||||||
reprompt: CipherRepromptType;
|
reprompt: CipherRepromptType;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ export class CipherData {
|
|||||||
this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds;
|
this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds;
|
||||||
this.creationDate = response.creationDate;
|
this.creationDate = response.creationDate;
|
||||||
this.deletedDate = response.deletedDate;
|
this.deletedDate = response.deletedDate;
|
||||||
|
this.archivedDate = response.archivedDate;
|
||||||
this.reprompt = response.reprompt;
|
this.reprompt = response.reprompt;
|
||||||
this.key = response.key;
|
this.key = response.key;
|
||||||
|
|
||||||
|
|||||||
@@ -60,13 +60,14 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
localData: null,
|
localData: null,
|
||||||
creationDate: null,
|
creationDate: null,
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: undefined,
|
reprompt: undefined,
|
||||||
attachments: null,
|
attachments: null,
|
||||||
fields: null,
|
fields: null,
|
||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
key: null,
|
key: null,
|
||||||
permissions: undefined,
|
permissions: undefined,
|
||||||
|
archivedDate: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.name = mockEnc("EncryptedString");
|
cipher.name = mockEnc("EncryptedString");
|
||||||
cipher.notes = mockEnc("EncryptedString");
|
cipher.notes = mockEnc("EncryptedString");
|
||||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||||
cipher.deletedDate = null;
|
cipher.deletedDate = undefined;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
cipher.permissions = new CipherPermissionsApi();
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
@@ -123,7 +124,7 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
@@ -149,10 +150,11 @@ describe("Cipher DTO", () => {
|
|||||||
name: "EncryptedString",
|
name: "EncryptedString",
|
||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
key: "EncryptedString",
|
key: "EncryptedString",
|
||||||
|
archivedDate: undefined,
|
||||||
login: {
|
login: {
|
||||||
uris: [
|
uris: [
|
||||||
{
|
{
|
||||||
@@ -224,10 +226,11 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
localData: null,
|
localData: null,
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
key: { encryptedString: "EncryptedString", encryptionType: 0 },
|
key: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||||
|
archivedDate: undefined,
|
||||||
login: {
|
login: {
|
||||||
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
autofillOnPageLoad: false,
|
autofillOnPageLoad: false,
|
||||||
@@ -302,10 +305,11 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.name = mockEnc("EncryptedString");
|
cipher.name = mockEnc("EncryptedString");
|
||||||
cipher.notes = mockEnc("EncryptedString");
|
cipher.notes = mockEnc("EncryptedString");
|
||||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||||
cipher.deletedDate = null;
|
cipher.deletedDate = undefined;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
cipher.permissions = new CipherPermissionsApi();
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
cipher.archivedDate = undefined;
|
||||||
|
|
||||||
const loginView = new LoginView();
|
const loginView = new LoginView();
|
||||||
loginView.username = "username";
|
loginView.username = "username";
|
||||||
@@ -347,10 +351,11 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
|
archivedDate: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -372,13 +377,14 @@ describe("Cipher DTO", () => {
|
|||||||
name: "EncryptedString",
|
name: "EncryptedString",
|
||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
key: "EncKey",
|
key: "EncKey",
|
||||||
secureNote: {
|
secureNote: {
|
||||||
type: SecureNoteType.Generic,
|
type: SecureNoteType.Generic,
|
||||||
},
|
},
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
|
archivedDate: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -401,7 +407,7 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
localData: null,
|
localData: null,
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
secureNote: { type: SecureNoteType.Generic },
|
secureNote: { type: SecureNoteType.Generic },
|
||||||
attachments: null,
|
attachments: null,
|
||||||
@@ -409,6 +415,7 @@ describe("Cipher DTO", () => {
|
|||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
|
archivedDate: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -431,12 +438,13 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.name = mockEnc("EncryptedString");
|
cipher.name = mockEnc("EncryptedString");
|
||||||
cipher.notes = mockEnc("EncryptedString");
|
cipher.notes = mockEnc("EncryptedString");
|
||||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||||
cipher.deletedDate = null;
|
cipher.deletedDate = undefined;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.secureNote = new SecureNote();
|
cipher.secureNote = new SecureNote();
|
||||||
cipher.secureNote.type = SecureNoteType.Generic;
|
cipher.secureNote.type = SecureNoteType.Generic;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
cipher.permissions = new CipherPermissionsApi();
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
cipher.archivedDate = undefined;
|
||||||
|
|
||||||
const keyService = mock<KeyService>();
|
const keyService = mock<KeyService>();
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
@@ -470,10 +478,11 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
|
archivedDate: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -495,7 +504,7 @@ describe("Cipher DTO", () => {
|
|||||||
name: "EncryptedString",
|
name: "EncryptedString",
|
||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
card: {
|
card: {
|
||||||
@@ -507,6 +516,7 @@ describe("Cipher DTO", () => {
|
|||||||
code: "EncryptedString",
|
code: "EncryptedString",
|
||||||
},
|
},
|
||||||
key: "EncKey",
|
key: "EncKey",
|
||||||
|
archivedDate: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -529,7 +539,7 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
localData: null,
|
localData: null,
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
card: {
|
card: {
|
||||||
cardholderName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
cardholderName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||||
@@ -544,6 +554,7 @@ describe("Cipher DTO", () => {
|
|||||||
passwordHistory: null,
|
passwordHistory: null,
|
||||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
|
archivedDate: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -566,10 +577,11 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.name = mockEnc("EncryptedString");
|
cipher.name = mockEnc("EncryptedString");
|
||||||
cipher.notes = mockEnc("EncryptedString");
|
cipher.notes = mockEnc("EncryptedString");
|
||||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||||
cipher.deletedDate = null;
|
cipher.deletedDate = undefined;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
cipher.permissions = new CipherPermissionsApi();
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
cipher.archivedDate = undefined;
|
||||||
|
|
||||||
const cardView = new CardView();
|
const cardView = new CardView();
|
||||||
cardView.cardholderName = "cardholderName";
|
cardView.cardholderName = "cardholderName";
|
||||||
@@ -611,10 +623,11 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
|
archivedDate: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -636,10 +649,11 @@ describe("Cipher DTO", () => {
|
|||||||
name: "EncryptedString",
|
name: "EncryptedString",
|
||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
key: "EncKey",
|
key: "EncKey",
|
||||||
|
archivedDate: undefined,
|
||||||
identity: {
|
identity: {
|
||||||
title: "EncryptedString",
|
title: "EncryptedString",
|
||||||
firstName: "EncryptedString",
|
firstName: "EncryptedString",
|
||||||
@@ -682,8 +696,9 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
localData: null,
|
localData: null,
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
|
archivedDate: undefined,
|
||||||
identity: {
|
identity: {
|
||||||
title: { encryptedString: "EncryptedString", encryptionType: 0 },
|
title: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||||
firstName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
firstName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||||
@@ -731,10 +746,11 @@ describe("Cipher DTO", () => {
|
|||||||
cipher.name = mockEnc("EncryptedString");
|
cipher.name = mockEnc("EncryptedString");
|
||||||
cipher.notes = mockEnc("EncryptedString");
|
cipher.notes = mockEnc("EncryptedString");
|
||||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||||
cipher.deletedDate = null;
|
cipher.deletedDate = undefined;
|
||||||
cipher.reprompt = CipherRepromptType.None;
|
cipher.reprompt = CipherRepromptType.None;
|
||||||
cipher.key = mockEnc("EncKey");
|
cipher.key = mockEnc("EncKey");
|
||||||
cipher.permissions = new CipherPermissionsApi();
|
cipher.permissions = new CipherPermissionsApi();
|
||||||
|
cipher.archivedDate = undefined;
|
||||||
|
|
||||||
const identityView = new IdentityView();
|
const identityView = new IdentityView();
|
||||||
identityView.firstName = "firstName";
|
identityView.firstName = "firstName";
|
||||||
@@ -776,10 +792,11 @@ describe("Cipher DTO", () => {
|
|||||||
collectionIds: undefined,
|
collectionIds: undefined,
|
||||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
reprompt: 0,
|
reprompt: 0,
|
||||||
localData: undefined,
|
localData: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
|
archivedDate: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -793,6 +810,7 @@ describe("Cipher DTO", () => {
|
|||||||
|
|
||||||
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
|
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
|
||||||
const deletedDate = new Date("2022-09-04T01:06:40.441Z");
|
const deletedDate = new Date("2022-09-04T01:06:40.441Z");
|
||||||
|
const archivedDate = new Date("2022-10-04T01:06:40.441Z");
|
||||||
const actual = Cipher.fromJSON({
|
const actual = Cipher.fromJSON({
|
||||||
name: "myName",
|
name: "myName",
|
||||||
notes: "myNotes",
|
notes: "myNotes",
|
||||||
@@ -801,6 +819,7 @@ describe("Cipher DTO", () => {
|
|||||||
fields: ["field1", "field2"] as any,
|
fields: ["field1", "field2"] as any,
|
||||||
passwordHistory: ["ph1", "ph2"] as any,
|
passwordHistory: ["ph1", "ph2"] as any,
|
||||||
deletedDate: deletedDate.toISOString(),
|
deletedDate: deletedDate.toISOString(),
|
||||||
|
archivedDate: archivedDate.toISOString(),
|
||||||
} as Jsonify<Cipher>);
|
} as Jsonify<Cipher>);
|
||||||
|
|
||||||
expect(actual).toMatchObject({
|
expect(actual).toMatchObject({
|
||||||
@@ -811,6 +830,7 @@ describe("Cipher DTO", () => {
|
|||||||
fields: ["field1_fromJSON", "field2_fromJSON"],
|
fields: ["field1_fromJSON", "field2_fromJSON"],
|
||||||
passwordHistory: ["ph1_fromJSON", "ph2_fromJSON"],
|
passwordHistory: ["ph1_fromJSON", "ph2_fromJSON"],
|
||||||
deletedDate: deletedDate,
|
deletedDate: deletedDate,
|
||||||
|
archivedDate: archivedDate,
|
||||||
});
|
});
|
||||||
expect(actual).toBeInstanceOf(Cipher);
|
expect(actual).toBeInstanceOf(Cipher);
|
||||||
});
|
});
|
||||||
@@ -862,7 +882,8 @@ describe("Cipher DTO", () => {
|
|||||||
name: "EncryptedString",
|
name: "EncryptedString",
|
||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
|
archivedDate: undefined,
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
key: "EncryptedString",
|
key: "EncryptedString",
|
||||||
login: {
|
login: {
|
||||||
@@ -1084,6 +1105,7 @@ describe("Cipher DTO", () => {
|
|||||||
],
|
],
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: undefined,
|
deletedDate: undefined,
|
||||||
|
archivedDate: undefined,
|
||||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1105,7 +1127,8 @@ describe("Cipher DTO", () => {
|
|||||||
name: "EncryptedString",
|
name: "EncryptedString",
|
||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
|
archivedDate: undefined,
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
key: "EncryptedString",
|
key: "EncryptedString",
|
||||||
login: {
|
login: {
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
passwordHistory: Password[];
|
passwordHistory: Password[];
|
||||||
collectionIds: string[];
|
collectionIds: string[];
|
||||||
creationDate: Date;
|
creationDate: Date;
|
||||||
deletedDate: Date;
|
deletedDate: Date | undefined;
|
||||||
|
archivedDate: Date | undefined;
|
||||||
reprompt: CipherRepromptType;
|
reprompt: CipherRepromptType;
|
||||||
key: EncString;
|
key: EncString;
|
||||||
|
|
||||||
@@ -94,7 +95,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
this.collectionIds = obj.collectionIds;
|
this.collectionIds = obj.collectionIds;
|
||||||
this.localData = localData;
|
this.localData = localData;
|
||||||
this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null;
|
this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null;
|
||||||
this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : null;
|
this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : undefined;
|
||||||
|
this.archivedDate = obj.archivedDate != null ? new Date(obj.archivedDate) : undefined;
|
||||||
this.reprompt = obj.reprompt;
|
this.reprompt = obj.reprompt;
|
||||||
|
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
@@ -244,10 +246,11 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
c.type = this.type;
|
c.type = this.type;
|
||||||
c.collectionIds = this.collectionIds;
|
c.collectionIds = this.collectionIds;
|
||||||
c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null;
|
c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null;
|
||||||
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
|
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : undefined;
|
||||||
c.reprompt = this.reprompt;
|
c.reprompt = this.reprompt;
|
||||||
c.key = this.key?.encryptedString;
|
c.key = this.key?.encryptedString;
|
||||||
c.permissions = this.permissions;
|
c.permissions = this.permissions;
|
||||||
|
c.archivedDate = this.archivedDate != null ? this.archivedDate.toISOString() : undefined;
|
||||||
|
|
||||||
this.buildDataModel(this, c, {
|
this.buildDataModel(this, c, {
|
||||||
name: null,
|
name: null,
|
||||||
@@ -296,11 +299,12 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
const notes = EncString.fromJSON(obj.notes);
|
const notes = EncString.fromJSON(obj.notes);
|
||||||
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||||
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
const deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate);
|
||||||
const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a));
|
const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a));
|
||||||
const fields = obj.fields?.map((f: any) => Field.fromJSON(f));
|
const fields = obj.fields?.map((f: any) => Field.fromJSON(f));
|
||||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph));
|
const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph));
|
||||||
const key = EncString.fromJSON(obj.key);
|
const key = EncString.fromJSON(obj.key);
|
||||||
|
const archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate);
|
||||||
|
|
||||||
Object.assign(domain, obj, {
|
Object.assign(domain, obj, {
|
||||||
name,
|
name,
|
||||||
@@ -312,6 +316,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
fields,
|
fields,
|
||||||
passwordHistory,
|
passwordHistory,
|
||||||
key,
|
key,
|
||||||
|
archivedDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
switch (obj.type) {
|
switch (obj.type) {
|
||||||
@@ -369,6 +374,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
revisionDate: this.revisionDate?.toISOString(),
|
revisionDate: this.revisionDate?.toISOString(),
|
||||||
creationDate: this.creationDate?.toISOString(),
|
creationDate: this.creationDate?.toISOString(),
|
||||||
deletedDate: this.deletedDate?.toISOString(),
|
deletedDate: this.deletedDate?.toISOString(),
|
||||||
|
archivedDate: this.archivedDate?.toISOString(),
|
||||||
reprompt: this.reprompt,
|
reprompt: this.reprompt,
|
||||||
// Initialize all cipher-type-specific properties as undefined
|
// Initialize all cipher-type-specific properties as undefined
|
||||||
login: undefined,
|
login: undefined,
|
||||||
@@ -434,7 +440,8 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
|||||||
sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? [];
|
sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? [];
|
||||||
cipher.creationDate = new Date(sdkCipher.creationDate);
|
cipher.creationDate = new Date(sdkCipher.creationDate);
|
||||||
cipher.revisionDate = new Date(sdkCipher.revisionDate);
|
cipher.revisionDate = new Date(sdkCipher.revisionDate);
|
||||||
cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : null;
|
cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : undefined;
|
||||||
|
cipher.archivedDate = sdkCipher.archivedDate ? new Date(sdkCipher.archivedDate) : undefined;
|
||||||
cipher.reprompt = sdkCipher.reprompt;
|
cipher.reprompt = sdkCipher.reprompt;
|
||||||
|
|
||||||
// Cipher type specific properties
|
// Cipher type specific properties
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { CipherId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
|
export class CipherBulkArchiveRequest {
|
||||||
|
ids: CipherId[];
|
||||||
|
|
||||||
|
constructor(ids: CipherId[]) {
|
||||||
|
this.ids = ids == null ? [] : ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CipherBulkUnarchiveRequest {
|
||||||
|
ids: CipherId[];
|
||||||
|
|
||||||
|
constructor(ids: CipherId[]) {
|
||||||
|
this.ids = ids == null ? [] : ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ export class CipherRequest {
|
|||||||
attachments: { [id: string]: string };
|
attachments: { [id: string]: string };
|
||||||
attachments2: { [id: string]: AttachmentRequest };
|
attachments2: { [id: string]: AttachmentRequest };
|
||||||
lastKnownRevisionDate: Date;
|
lastKnownRevisionDate: Date;
|
||||||
|
archivedDate: Date | null;
|
||||||
reprompt: CipherRepromptType;
|
reprompt: CipherRepromptType;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ export class CipherRequest {
|
|||||||
this.notes = cipher.notes ? cipher.notes.encryptedString : null;
|
this.notes = cipher.notes ? cipher.notes.encryptedString : null;
|
||||||
this.favorite = cipher.favorite;
|
this.favorite = cipher.favorite;
|
||||||
this.lastKnownRevisionDate = cipher.revisionDate;
|
this.lastKnownRevisionDate = cipher.revisionDate;
|
||||||
|
this.archivedDate = cipher.archivedDate;
|
||||||
this.reprompt = cipher.reprompt;
|
this.reprompt = cipher.reprompt;
|
||||||
this.key = cipher.key?.encryptedString;
|
this.key = cipher.key?.encryptedString;
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class CipherResponse extends BaseResponse {
|
|||||||
collectionIds: string[];
|
collectionIds: string[];
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
deletedDate: string;
|
deletedDate: string;
|
||||||
|
archivedDate: string;
|
||||||
reprompt: CipherRepromptType;
|
reprompt: CipherRepromptType;
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ export class CipherResponse extends BaseResponse {
|
|||||||
this.collectionIds = this.getResponseProperty("CollectionIds");
|
this.collectionIds = this.getResponseProperty("CollectionIds");
|
||||||
this.creationDate = this.getResponseProperty("CreationDate");
|
this.creationDate = this.getResponseProperty("CreationDate");
|
||||||
this.deletedDate = this.getResponseProperty("DeletedDate");
|
this.deletedDate = this.getResponseProperty("DeletedDate");
|
||||||
|
this.archivedDate = this.getResponseProperty("ArchivedDate");
|
||||||
|
|
||||||
const login = this.getResponseProperty("Login");
|
const login = this.getResponseProperty("Login");
|
||||||
if (login != null) {
|
if (login != null) {
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ describe("CipherView", () => {
|
|||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
revisionDate: "2022-01-02T12:00:00.000Z",
|
revisionDate: "2022-01-02T12:00:00.000Z",
|
||||||
deletedDate: undefined,
|
deletedDate: undefined,
|
||||||
|
archivedDate: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
collectionIds: string[] = null;
|
collectionIds: string[] = null;
|
||||||
revisionDate: Date = null;
|
revisionDate: Date = null;
|
||||||
creationDate: Date = null;
|
creationDate: Date = null;
|
||||||
deletedDate: Date = null;
|
deletedDate: Date | null = null;
|
||||||
|
archivedDate: Date | null = null;
|
||||||
reprompt: CipherRepromptType = CipherRepromptType.None;
|
reprompt: CipherRepromptType = CipherRepromptType.None;
|
||||||
// We need a copy of the encrypted key so we can pass it to
|
// We need a copy of the encrypted key so we can pass it to
|
||||||
// the SdkCipherView during encryption
|
// the SdkCipherView during encryption
|
||||||
@@ -79,6 +80,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
this.revisionDate = c.revisionDate;
|
this.revisionDate = c.revisionDate;
|
||||||
this.creationDate = c.creationDate;
|
this.creationDate = c.creationDate;
|
||||||
this.deletedDate = c.deletedDate;
|
this.deletedDate = c.deletedDate;
|
||||||
|
this.archivedDate = c.archivedDate;
|
||||||
// Old locally stored ciphers might have reprompt == null. If so set it to None.
|
// Old locally stored ciphers might have reprompt == null. If so set it to None.
|
||||||
this.reprompt = c.reprompt ?? CipherRepromptType.None;
|
this.reprompt = c.reprompt ?? CipherRepromptType.None;
|
||||||
this.key = c.key;
|
this.key = c.key;
|
||||||
@@ -143,6 +145,10 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
return this.deletedDate != null;
|
return this.deletedDate != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isArchived(): boolean {
|
||||||
|
return this.archivedDate != null;
|
||||||
|
}
|
||||||
|
|
||||||
get linkedFieldOptions() {
|
get linkedFieldOptions() {
|
||||||
return this.item?.linkedFieldOptions;
|
return this.item?.linkedFieldOptions;
|
||||||
}
|
}
|
||||||
@@ -197,6 +203,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||||
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||||
|
const archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
|
||||||
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
||||||
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
||||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
||||||
@@ -217,6 +224,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
creationDate: creationDate,
|
creationDate: creationDate,
|
||||||
revisionDate: revisionDate,
|
revisionDate: revisionDate,
|
||||||
deletedDate: deletedDate,
|
deletedDate: deletedDate,
|
||||||
|
archivedDate: archivedDate,
|
||||||
attachments: attachments,
|
attachments: attachments,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
passwordHistory: passwordHistory,
|
passwordHistory: passwordHistory,
|
||||||
@@ -277,6 +285,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||||
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate);
|
||||||
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||||
|
cipherView.archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate);
|
||||||
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
cipherView.reprompt = obj.reprompt ?? CipherRepromptType.None;
|
||||||
cipherView.key = EncString.fromJSON(obj.key);
|
cipherView.key = EncString.fromJSON(obj.key);
|
||||||
|
|
||||||
@@ -330,6 +339,7 @@ export class CipherView implements View, InitializerMetadata {
|
|||||||
revisionDate: (this.revisionDate ?? new Date()).toISOString(),
|
revisionDate: (this.revisionDate ?? new Date()).toISOString(),
|
||||||
creationDate: (this.creationDate ?? new Date()).toISOString(),
|
creationDate: (this.creationDate ?? new Date()).toISOString(),
|
||||||
deletedDate: this.deletedDate?.toISOString(),
|
deletedDate: this.deletedDate?.toISOString(),
|
||||||
|
archivedDate: this.archivedDate?.toISOString(),
|
||||||
reprompt: this.reprompt ?? CipherRepromptType.None,
|
reprompt: this.reprompt ?? CipherRepromptType.None,
|
||||||
key: this.key?.toSdk(),
|
key: this.key?.toSdk(),
|
||||||
// Cipher type specific properties are set in the switch statement below
|
// Cipher type specific properties are set in the switch statement below
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const cipherData: CipherData = {
|
|||||||
deletedDate: null,
|
deletedDate: null,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
key: "EncKey",
|
key: "EncKey",
|
||||||
|
archivedDate: null,
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
login: {
|
login: {
|
||||||
uris: [
|
uris: [
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
cipher.collectionIds = model.collectionIds;
|
cipher.collectionIds = model.collectionIds;
|
||||||
cipher.creationDate = model.creationDate;
|
cipher.creationDate = model.creationDate;
|
||||||
cipher.revisionDate = model.revisionDate;
|
cipher.revisionDate = model.revisionDate;
|
||||||
|
cipher.archivedDate = model.archivedDate;
|
||||||
cipher.reprompt = model.reprompt;
|
cipher.reprompt = model.reprompt;
|
||||||
cipher.edit = model.edit;
|
cipher.edit = model.edit;
|
||||||
|
|
||||||
@@ -634,6 +635,10 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
);
|
);
|
||||||
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
|
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
|
||||||
|
|
||||||
|
const archiveFeatureEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PM19148_InnovationArchive,
|
||||||
|
);
|
||||||
|
|
||||||
return ciphers.filter((cipher) => {
|
return ciphers.filter((cipher) => {
|
||||||
const type = CipherViewLikeUtils.getType(cipher);
|
const type = CipherViewLikeUtils.getType(cipher);
|
||||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||||
@@ -643,6 +648,10 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (archiveFeatureEnabled && CipherViewLikeUtils.isArchived(cipher)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(includeOtherTypes) && includeOtherTypes.includes(type) && !cipherIsLogin) {
|
if (Array.isArray(includeOtherTypes) && includeOtherTypes.includes(type) && !cipherIsLogin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -666,8 +675,16 @@ export class CipherService implements CipherServiceAbstraction {
|
|||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Promise<CipherView[]> {
|
): Promise<CipherView[]> {
|
||||||
const ciphers = await this.getAllDecrypted(userId);
|
const ciphers = await this.getAllDecrypted(userId);
|
||||||
|
const archiveFeatureEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PM19148_InnovationArchive,
|
||||||
|
);
|
||||||
return ciphers
|
return ciphers
|
||||||
.filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type))
|
.filter(
|
||||||
|
(cipher) =>
|
||||||
|
cipher.deletedDate == null &&
|
||||||
|
(!archiveFeatureEnabled || !cipher.isArchived) &&
|
||||||
|
type.includes(cipher.type),
|
||||||
|
)
|
||||||
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
|
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ const cipherData: CipherData = {
|
|||||||
name: "EncryptedString",
|
name: "EncryptedString",
|
||||||
notes: "EncryptedString",
|
notes: "EncryptedString",
|
||||||
creationDate: "2022-01-01T12:00:00.000Z",
|
creationDate: "2022-01-01T12:00:00.000Z",
|
||||||
deletedDate: null,
|
deletedDate: undefined,
|
||||||
|
archivedDate: undefined,
|
||||||
permissions: new CipherPermissionsApi(),
|
permissions: new CipherPermissionsApi(),
|
||||||
key: "EncKey",
|
key: "EncKey",
|
||||||
reprompt: CipherRepromptType.None,
|
reprompt: CipherRepromptType.None,
|
||||||
|
|||||||
@@ -110,6 +110,32 @@ describe("CipherViewLikeUtils", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isArchived", () => {
|
||||||
|
it("returns true when the cipher is archived", () => {
|
||||||
|
const cipherListView = {
|
||||||
|
id: "1",
|
||||||
|
archivedDate: "2024-02-02",
|
||||||
|
type: "identity",
|
||||||
|
} as unknown as CipherListView;
|
||||||
|
const cipherView = createCipherView();
|
||||||
|
cipherView.archivedDate = new Date();
|
||||||
|
|
||||||
|
expect(CipherViewLikeUtils.isArchived(cipherListView)).toBe(true);
|
||||||
|
expect(CipherViewLikeUtils.isArchived(cipherView)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the cipher is not archived", () => {
|
||||||
|
const cipherListView = {
|
||||||
|
id: "2",
|
||||||
|
type: "identity",
|
||||||
|
} as unknown as CipherListView;
|
||||||
|
const cipherView = createCipherView();
|
||||||
|
|
||||||
|
expect(CipherViewLikeUtils.isArchived(cipherListView)).toBe(false);
|
||||||
|
expect(CipherViewLikeUtils.isArchived(cipherView)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("isDeleted", () => {
|
describe("isDeleted", () => {
|
||||||
it("returns true when the cipher is deleted", () => {
|
it("returns true when the cipher is deleted", () => {
|
||||||
const cipherListView = { deletedDate: "2024-02-02", type: "identity" } as CipherListView;
|
const cipherListView = { deletedDate: "2024-02-02", type: "identity" } as CipherListView;
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ export class CipherViewLikeUtils {
|
|||||||
return cipher.type === CipherType.Card ? cipher.card : null;
|
return cipher.type === CipherType.Card ? cipher.card : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @returns `true` when the cipher has been archived, `false` otherwise. */
|
||||||
|
static isArchived = (cipher: CipherViewLike): boolean => {
|
||||||
|
if (this.isCipherListView(cipher)) {
|
||||||
|
return !!cipher.archivedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipher.isArchived;
|
||||||
|
};
|
||||||
|
|
||||||
/** @returns `true` when the cipher has been deleted, `false` otherwise. */
|
/** @returns `true` when the cipher has been deleted, `false` otherwise. */
|
||||||
static isDeleted = (cipher: CipherViewLike): boolean => {
|
static isDeleted = (cipher: CipherViewLike): boolean => {
|
||||||
if (this.isCipherListView(cipher)) {
|
if (this.isCipherListView(cipher)) {
|
||||||
|
|||||||
14
libs/vault/src/abstractions/cipher-archive.service.ts
Normal file
14
libs/vault/src/abstractions/cipher-archive.service.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
|
|
||||||
|
export abstract class CipherArchiveService {
|
||||||
|
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
||||||
|
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
||||||
|
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
||||||
|
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||||
|
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||||
|
abstract canInteract(cipher: CipherView): Promise<boolean>;
|
||||||
|
}
|
||||||
@@ -186,6 +186,11 @@ export class ItemDetailsSectionComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get showOwnership() {
|
get showOwnership() {
|
||||||
|
// Don't show ownership field for archived ciphers
|
||||||
|
if (this.originalCipherView?.isArchived) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Show ownership field when editing with available orgs
|
// Show ownership field when editing with available orgs
|
||||||
const isEditingWithOrgs = this.organizations.length > 0 && this.config.mode === "edit";
|
const isEditingWithOrgs = this.organizations.length > 0 && this.config.mode === "edit";
|
||||||
|
|
||||||
|
|||||||
@@ -27,3 +27,5 @@ export { SshImportPromptService } from "./services/ssh-import-prompt.service";
|
|||||||
|
|
||||||
export * from "./abstractions/change-login-password.service";
|
export * from "./abstractions/change-login-password.service";
|
||||||
export * from "./services/default-change-login-password.service";
|
export * from "./services/default-change-login-password.service";
|
||||||
|
export * from "./abstractions/cipher-archive.service";
|
||||||
|
export * from "./services/default-cipher-archive.service";
|
||||||
|
|||||||
289
libs/vault/src/services/default-cipher-archive.service.spec.ts
Normal file
289
libs/vault/src/services/default-cipher-archive.service.spec.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import {
|
||||||
|
CipherBulkArchiveRequest,
|
||||||
|
CipherBulkUnarchiveRequest,
|
||||||
|
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||||
|
|
||||||
|
import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component";
|
||||||
|
|
||||||
|
import { DefaultCipherArchiveService } from "./default-cipher-archive.service";
|
||||||
|
import { PasswordRepromptService } from "./password-reprompt.service";
|
||||||
|
|
||||||
|
describe("DefaultCipherArchiveService", () => {
|
||||||
|
let service: DefaultCipherArchiveService;
|
||||||
|
let mockCipherService: jest.Mocked<CipherService>;
|
||||||
|
let mockApiService: jest.Mocked<ApiService>;
|
||||||
|
let mockDialogService: jest.Mocked<DialogService>;
|
||||||
|
let mockPasswordRepromptService: jest.Mocked<PasswordRepromptService>;
|
||||||
|
let mockBillingAccountProfileStateService: jest.Mocked<BillingAccountProfileStateService>;
|
||||||
|
let mockConfigService: jest.Mocked<ConfigService>;
|
||||||
|
|
||||||
|
const userId = "user-id" as UserId;
|
||||||
|
const cipherId = "123" as CipherId;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockCipherService = mock<CipherService>();
|
||||||
|
mockApiService = mock<ApiService>();
|
||||||
|
mockDialogService = mock<DialogService>();
|
||||||
|
mockPasswordRepromptService = mock<PasswordRepromptService>();
|
||||||
|
mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||||
|
mockConfigService = mock<ConfigService>();
|
||||||
|
|
||||||
|
service = new DefaultCipherArchiveService(
|
||||||
|
mockCipherService,
|
||||||
|
mockApiService,
|
||||||
|
mockDialogService,
|
||||||
|
mockPasswordRepromptService,
|
||||||
|
mockBillingAccountProfileStateService,
|
||||||
|
mockConfigService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("archivedCiphers$", () => {
|
||||||
|
it("should return only archived ciphers", async () => {
|
||||||
|
const mockCiphers: CipherListView[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||||
|
type: "identity",
|
||||||
|
} as unknown as CipherListView,
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
type: "secureNote",
|
||||||
|
} as unknown as CipherListView,
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||||
|
deletedDate: "2024-01-16T10:30:00.000Z",
|
||||||
|
type: "sshKey",
|
||||||
|
} as unknown as CipherListView,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.archivedCiphers$(userId));
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].id).toEqual("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when no archived ciphers exist", async () => {
|
||||||
|
const mockCiphers: CipherListView[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
type: "identity",
|
||||||
|
} as unknown as CipherListView,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.archivedCiphers$(userId));
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("userCanArchive$", () => {
|
||||||
|
it("should return true when user has premium and feature flag is enabled", async () => {
|
||||||
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||||
|
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.userCanArchive$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockBillingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
expect(mockConfigService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||||
|
FeatureFlag.PM19148_InnovationArchive,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when feature flag is disabled", async () => {
|
||||||
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||||
|
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.userCanArchive$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("archiveWithServer", () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: cipherId,
|
||||||
|
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||||
|
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockApiService.send.mockResolvedValue(mockResponse);
|
||||||
|
mockCipherService.ciphers$.mockReturnValue(
|
||||||
|
of({
|
||||||
|
[cipherId]: {
|
||||||
|
id: cipherId,
|
||||||
|
revisionDate: "2024-01-15T10:00:00.000Z",
|
||||||
|
} as any,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
mockCipherService.replace.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should archive single cipher", async () => {
|
||||||
|
await service.archiveWithServer(cipherId, userId);
|
||||||
|
|
||||||
|
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/ciphers/archive",
|
||||||
|
expect.any(CipherBulkArchiveRequest),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
|
||||||
|
expect(mockCipherService.replace).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
[cipherId]: expect.objectContaining({
|
||||||
|
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||||
|
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should archive multiple ciphers", async () => {
|
||||||
|
const cipherIds = [cipherId, "cipher-id-2" as CipherId];
|
||||||
|
|
||||||
|
await service.archiveWithServer(cipherIds, userId);
|
||||||
|
|
||||||
|
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/ciphers/archive",
|
||||||
|
expect.objectContaining({
|
||||||
|
ids: cipherIds,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("unarchiveWithServer", () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: cipherId,
|
||||||
|
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockApiService.send.mockResolvedValue(mockResponse);
|
||||||
|
mockCipherService.ciphers$.mockReturnValue(
|
||||||
|
of({
|
||||||
|
[cipherId]: {
|
||||||
|
id: cipherId,
|
||||||
|
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||||
|
revisionDate: "2024-01-15T10:00:00.000Z",
|
||||||
|
} as any,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
mockCipherService.replace.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should unarchive single cipher", async () => {
|
||||||
|
await service.unarchiveWithServer(cipherId, userId);
|
||||||
|
|
||||||
|
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/ciphers/unarchive",
|
||||||
|
expect.any(CipherBulkUnarchiveRequest),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
|
||||||
|
expect(mockCipherService.replace).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
[cipherId]: expect.objectContaining({
|
||||||
|
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should unarchive multiple ciphers", async () => {
|
||||||
|
const cipherIds = [cipherId, "cipher-id-2" as CipherId];
|
||||||
|
|
||||||
|
await service.unarchiveWithServer(cipherIds, userId);
|
||||||
|
|
||||||
|
expect(mockApiService.send).toHaveBeenCalledWith(
|
||||||
|
"PUT",
|
||||||
|
"/ciphers/unarchive",
|
||||||
|
expect.objectContaining({
|
||||||
|
ids: cipherIds,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("canInteract", () => {
|
||||||
|
let mockCipherView: CipherView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockCipherView = {
|
||||||
|
id: cipherId,
|
||||||
|
decryptionFailure: false,
|
||||||
|
} as unknown as CipherView;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false and open dialog when cipher has decryption failure", async () => {
|
||||||
|
mockCipherView.decryptionFailure = true;
|
||||||
|
const openSpy = jest.spyOn(DecryptionFailureDialogComponent, "open").mockImplementation();
|
||||||
|
|
||||||
|
const result = await service.canInteract(mockCipherView);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(openSpy).toHaveBeenCalledWith(mockDialogService, {
|
||||||
|
cipherIds: [cipherId],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return password reprompt result when no decryption failure", async () => {
|
||||||
|
mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await service.canInteract(mockCipherView);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockPasswordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(
|
||||||
|
mockCipherView,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when password reprompt fails", async () => {
|
||||||
|
mockPasswordRepromptService.passwordRepromptCheck.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await service.canInteract(mockCipherView);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
145
libs/vault/src/services/default-cipher-archive.service.ts
Normal file
145
libs/vault/src/services/default-cipher-archive.service.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { filter, map, Observable, shareReplay, combineLatest, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
|
import {
|
||||||
|
CipherBulkArchiveRequest,
|
||||||
|
CipherBulkUnarchiveRequest,
|
||||||
|
} from "@bitwarden/common/vault/models/request/cipher-bulk-archive.request";
|
||||||
|
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import {
|
||||||
|
CipherViewLike,
|
||||||
|
CipherViewLikeUtils,
|
||||||
|
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { CipherArchiveService } from "../abstractions/cipher-archive.service";
|
||||||
|
import { DecryptionFailureDialogComponent } from "../components/decryption-failure-dialog/decryption-failure-dialog.component";
|
||||||
|
|
||||||
|
import { PasswordRepromptService } from "./password-reprompt.service";
|
||||||
|
|
||||||
|
export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||||
|
constructor(
|
||||||
|
private cipherService: CipherService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
/**
|
||||||
|
* Observable that contains the list of ciphers that have been archived.
|
||||||
|
*/
|
||||||
|
archivedCiphers$(userId: UserId): Observable<CipherViewLike[]> {
|
||||||
|
return this.cipherService.cipherListViews$(userId).pipe(
|
||||||
|
filter((cipher) => cipher != null),
|
||||||
|
map((ciphers) =>
|
||||||
|
ciphers.filter(
|
||||||
|
(cipher) =>
|
||||||
|
CipherViewLikeUtils.isArchived(cipher) && !CipherViewLikeUtils.isDeleted(cipher),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User can archive items if:
|
||||||
|
* Feature Flag is enabled
|
||||||
|
* User has premium from any source (personal or organization)
|
||||||
|
*/
|
||||||
|
userCanArchive$(userId: UserId): Observable<boolean> {
|
||||||
|
return combineLatest([
|
||||||
|
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive),
|
||||||
|
]).pipe(
|
||||||
|
map(([hasPremium, archiveFlagEnabled]) => hasPremium && archiveFlagEnabled),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User can access the archive vault if:
|
||||||
|
* Feature Flag is enabled
|
||||||
|
* There is at least one archived item
|
||||||
|
* ///////////// NOTE /////////////
|
||||||
|
* This is separated from userCanArchive because a user that loses premium status, but has archived items,
|
||||||
|
* should still be able to access their archive vault. The items will be read-only, and can be restored.
|
||||||
|
*/
|
||||||
|
showArchiveVault$(userId: UserId): Observable<boolean> {
|
||||||
|
return combineLatest([
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive),
|
||||||
|
this.archivedCiphers$(userId),
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([archiveFlagEnabled, hasArchivedItems]) =>
|
||||||
|
archiveFlagEnabled && hasArchivedItems.length > 0,
|
||||||
|
),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
|
||||||
|
const request = new CipherBulkArchiveRequest(Array.isArray(ids) ? ids : [ids]);
|
||||||
|
const r = await this.apiService.send("PUT", "/ciphers/archive", request, true, true);
|
||||||
|
const response = new ListResponse(r, CipherResponse);
|
||||||
|
|
||||||
|
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||||
|
|
||||||
|
for (const cipher of response.data) {
|
||||||
|
const localCipher = currentCiphers[cipher.id as CipherId];
|
||||||
|
|
||||||
|
if (localCipher == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
localCipher.archivedDate = cipher.archivedDate;
|
||||||
|
localCipher.revisionDate = cipher.revisionDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.cipherService.replace(currentCiphers, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
|
||||||
|
const request = new CipherBulkUnarchiveRequest(Array.isArray(ids) ? ids : [ids]);
|
||||||
|
const r = await this.apiService.send("PUT", "/ciphers/unarchive", request, true, true);
|
||||||
|
const response = new ListResponse(r, CipherResponse);
|
||||||
|
|
||||||
|
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
|
||||||
|
|
||||||
|
for (const cipher of response.data) {
|
||||||
|
const localCipher = currentCiphers[cipher.id as CipherId];
|
||||||
|
|
||||||
|
if (localCipher == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
localCipher.archivedDate = cipher.archivedDate;
|
||||||
|
localCipher.revisionDate = cipher.revisionDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.cipherService.replace(currentCiphers, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user is able to interact with the cipher
|
||||||
|
* (password re-prompt / decryption failure checks).
|
||||||
|
* @param cipher
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async canInteract(cipher: CipherView) {
|
||||||
|
if (cipher.decryptionFailure) {
|
||||||
|
DecryptionFailureDialogComponent.open(this.dialogService, {
|
||||||
|
cipherIds: [cipher.id as CipherId],
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -23,7 +23,7 @@
|
|||||||
"@angular/platform-browser": "19.2.14",
|
"@angular/platform-browser": "19.2.14",
|
||||||
"@angular/platform-browser-dynamic": "19.2.14",
|
"@angular/platform-browser-dynamic": "19.2.14",
|
||||||
"@angular/router": "19.2.14",
|
"@angular/router": "19.2.14",
|
||||||
"@bitwarden/sdk-internal": "0.2.0-main.266",
|
"@bitwarden/sdk-internal": "0.2.0-main.296",
|
||||||
"@electron/fuses": "1.8.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@emotion/css": "11.13.5",
|
"@emotion/css": "11.13.5",
|
||||||
"@koa/multer": "4.0.0",
|
"@koa/multer": "4.0.0",
|
||||||
@@ -4688,9 +4688,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@bitwarden/sdk-internal": {
|
"node_modules/@bitwarden/sdk-internal": {
|
||||||
"version": "0.2.0-main.266",
|
"version": "0.2.0-main.296",
|
||||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.266.tgz",
|
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.296.tgz",
|
||||||
"integrity": "sha512-2Axa1D9AEkax2ssqahZYHVkk2RdguzLV2bJ6j99AZhh4qjGIYtDvmc5gDh7zhuw7Ig7H3mNpKwCZ/eJgadyH6g==",
|
"integrity": "sha512-SDTWRwnR+KritfgJVBgWKd27TJxl4IlUdTldVJ/tA0qM5OqGWrY6s4ubtl5eaGIl2X4WYRAvpe+VR93FLakk6A==",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^4.41.0"
|
"type-fest": "^4.41.0"
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
"@angular/platform-browser": "19.2.14",
|
"@angular/platform-browser": "19.2.14",
|
||||||
"@angular/platform-browser-dynamic": "19.2.14",
|
"@angular/platform-browser-dynamic": "19.2.14",
|
||||||
"@angular/router": "19.2.14",
|
"@angular/router": "19.2.14",
|
||||||
"@bitwarden/sdk-internal": "0.2.0-main.266",
|
"@bitwarden/sdk-internal": "0.2.0-main.296",
|
||||||
"@electron/fuses": "1.8.0",
|
"@electron/fuses": "1.8.0",
|
||||||
"@emotion/css": "11.13.5",
|
"@emotion/css": "11.13.5",
|
||||||
"@koa/multer": "4.0.0",
|
"@koa/multer": "4.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user