1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-23 16:13:21 +00:00

[PM-24534] Archive via CLI (#16502)

* refactor `canInteract` into a component level usage.

- The default service is going to be used in the CLI which won't make use of the UI-related aspects

* all nested entities to be imported from the vault

* initial add of archive command to the cli

* add archive to oss serve

* check for deleted cipher when attempting to archive

* add searchability/list functionality for archived ciphers

* restore an archived cipher

* unarchive a cipher when a user is editing it and has lost their premium status

* add missing feature flags

* re-export only needed services from the vault

* add needed await

* add prompt when applicable for editing an archived cipher

* move cipher archive service into `common/vault`

* fix testing code
This commit is contained in:
Nick Krantz
2025-09-30 09:45:04 -05:00
committed by GitHub
parent 7848b7d480
commit 727689d827
27 changed files with 401 additions and 131 deletions

View File

@@ -1,5 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as inquirer from "inquirer";
import { firstValueFrom, map, switchMap } from "rxjs";
import { UpdateCollectionRequest } from "@bitwarden/admin-console/common";
@@ -9,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
@@ -40,6 +42,7 @@ export class EditCommand {
private accountService: AccountService,
private cliRestrictedItemTypesService: CliRestrictedItemTypesService,
private policyService: PolicyService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
async run(
@@ -92,6 +95,10 @@ export class EditCommand {
private async editCipher(id: string, req: CipherExport) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.get(id, activeUserId);
const hasPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
);
if (cipher == null) {
return Response.notFound();
}
@@ -102,6 +109,17 @@ export class EditCommand {
}
cipherView = CipherExport.toView(req, cipherView);
// When a user is editing an archived cipher and does not have premium, automatically unarchive it
if (cipherView.isArchived && !hasPremium) {
const acceptedPrompt = await this.promptForArchiveEdit();
if (!acceptedPrompt) {
return Response.error("Edit cancelled.");
}
cipherView.archivedDate = null;
}
const isCipherRestricted =
await this.cliRestrictedItemTypesService.isCipherRestricted(cipherView);
if (isCipherRestricted) {
@@ -240,6 +258,38 @@ export class EditCommand {
return Response.error(e);
}
}
/** Prompt the user to accept movement of their cipher back to the their vault. */
private async promptForArchiveEdit(): Promise<boolean> {
// When running in serve or no interaction mode, automatically accept the prompt
if (process.env.BW_SERVE === "true" || process.env.BW_NOINTERACTION === "true") {
CliUtils.writeLn(
"Archive is only available with a Premium subscription, which has ended. Your edit was saved and the item was moved back to your vault.",
);
return true;
}
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "list",
name: "confirm",
message:
"When you edit and save details for an archived item without a Premium subscription, it'll be moved from your archive back to your vault.",
choices: [
{
name: "Move now",
value: "confirmed",
},
{
name: "Cancel",
value: "cancel",
},
],
});
return answer.confirm === "confirmed";
}
}
class Options {