From 64ad336d28823bb4516a721afc89d10d9c7fb4e9 Mon Sep 17 00:00:00 2001 From: Merissa Weinstein Date: Tue, 9 Jul 2024 15:06:36 -0500 Subject: [PATCH 1/5] remove erorr console log and add error toast (#10018) --- .../src/admin-console/components/collections.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 445727ac617..f490fa51b6f 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -100,7 +100,7 @@ export class CollectionsComponent implements OnInit { this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem")); return true; } catch (e) { - this.logService.error(e); + this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); return false; } } From 964438ff1bc4fb963f5b9f0bbe1f605de2e8ef87 Mon Sep 17 00:00:00 2001 From: Will Martin Date: Tue, 9 Jul 2024 16:31:03 -0400 Subject: [PATCH 2/5] [DS] add deprecation comment to `ApiActionDirective` (#10041) --- libs/angular/src/directives/api-action.directive.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/angular/src/directives/api-action.directive.ts b/libs/angular/src/directives/api-action.directive.ts index d07cb67b0fa..d7814b75427 100644 --- a/libs/angular/src/directives/api-action.directive.ts +++ b/libs/angular/src/directives/api-action.directive.ts @@ -9,6 +9,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid * Attach it to a
element and provide the name of the class property that will hold the api call promise. * e.g. * Any errors/rejections that occur will be intercepted and displayed as error toasts. + * + * @deprecated Use the CL's {@link BitSubmitDirective} instead */ @Directive({ selector: "[appApiAction]", From a87b5adb80435f81cbdb3258a17a821ae3289905 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:38:25 -0400 Subject: [PATCH 3/5] Added missing i18n key for "sponsored" (#10039) --- apps/web/src/locales/en/messages.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 66a768a0fa3..9f0dbf4e058 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8541,5 +8541,8 @@ "contactBitwardenSupport": { "message": "contact Bitwarden support.", "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'Notice: Later this month, client vault privacy will be improved and provider members will no longer have direct access to client vault items. For questions, please contact Bitwarden support'. 'Bitwarden' should not be translated" + }, + "sponsored": { + "message": "Sponsored" } } From 7dfef8991cf8ee7f1fc1bcfb7d6667307ed52412 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Wed, 10 Jul 2024 02:19:12 +0200 Subject: [PATCH 4/5] [PM-1935] Migrate vault lock screen to component library (#6237) * Migrate vault lock screen to component library * Remove unnecessary divs * Remove card from trial * Migrate to standalone component * refactor to use AnonLayout * revert login component * migrate web component to bitSubmit * remove class * use inject * update web pageTitle translation * validate on submit, not on blur --------- Co-authored-by: rr-bw <102181210+rr-bw@users.noreply.github.com> --- apps/web/src/app/auth/lock.component.html | 89 ++++++------------- apps/web/src/app/auth/lock.component.ts | 39 +++++++- apps/web/src/app/oss-routing.module.ts | 21 +++-- .../src/app/shared/loose-components.module.ts | 3 - apps/web/src/locales/en/messages.json | 4 +- 5 files changed, 78 insertions(+), 78 deletions(-) diff --git a/apps/web/src/app/auth/lock.component.html b/apps/web/src/app/auth/lock.component.html index 2cdc4d2a651..f630906223b 100644 --- a/apps/web/src/app/auth/lock.component.html +++ b/apps/web/src/app/auth/lock.component.html @@ -1,66 +1,27 @@ - -
-
-

- -

-

{{ "yourVaultIsLocked" | i18n }}

-
-
-
- -
- - -
- - {{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }} - -
-
-
- - -
-
-
-
+ + + {{ "masterPass" | i18n }} + + + {{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }} + + +
+ +
+ +
diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index 021bf0f9df4..6b52d0160b8 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,18 +1,49 @@ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; +import { SharedModule } from "../shared"; + @Component({ selector: "app-lock", templateUrl: "lock.component.html", + standalone: true, + imports: [SharedModule], }) export class LockComponent extends BaseLockComponent { + formBuilder = inject(FormBuilder); + + formGroup = this.formBuilder.group({ + masterPassword: ["", { validators: Validators.required, updateOn: "submit" }], + }); + + get masterPasswordFormControl() { + return this.formGroup.controls.masterPassword; + } + async ngOnInit() { await super.ngOnInit(); + + this.masterPasswordFormControl.setValue(this.masterPassword); + this.onSuccessfulSubmit = async () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigateByUrl(this.successRoute); + await this.router.navigateByUrl(this.successRoute); }; } + + async superSubmit() { + await super.submit(); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + this.masterPassword = this.masterPasswordFormControl.value; + await this.superSubmit(); + }; } diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 246f4bdc403..8f3638ac2e9 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -17,6 +17,7 @@ import { RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, + LockIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -113,11 +114,6 @@ const routes: Routes = [ component: SetPasswordComponent, data: { titleId: "setMasterPassword" } satisfies DataProperties, }, - { - path: "lock", - component: LockComponent, - canActivate: [deepLinkGuard(), lockGuard()], - }, { path: "verify-email", component: VerifyEmailTokenComponent }, { path: "accept-organization", @@ -246,6 +242,21 @@ const routes: Routes = [ pageTitle: "logIn", }, }, + { + path: "lock", + canActivate: [deepLinkGuard(), lockGuard()], + children: [ + { + path: "", + component: LockComponent, + }, + ], + data: { + pageTitle: "yourVaultIsLockedV2", + pageIcon: LockIcon, + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + }, { path: "2fa", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 107cdd040e4..9f67c859f2c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -22,7 +22,6 @@ import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component"; import { HintComponent } from "../auth/hint.component"; -import { LockComponent } from "../auth/lock.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; import { RegisterFormModule } from "../auth/register-form/register-form.module"; @@ -141,7 +140,6 @@ import { SharedModule } from "./shared.module"; FolderAddEditComponent, FrontendLayoutComponent, HintComponent, - LockComponent, OrgAddEditComponent, OrgAttachmentsComponent, OrgCollectionsComponent, @@ -213,7 +211,6 @@ import { SharedModule } from "./shared.module"; FolderAddEditComponent, FrontendLayoutComponent, HintComponent, - LockComponent, OrgAddEditComponent, OrganizationLayoutComponent, OrgAttachmentsComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 9f0dbf4e058..16df4420246 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -854,8 +854,8 @@ "emailAddress": { "message": "Email address" }, - "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your master password to continue." + "yourVaultIsLockedV2": { + "message": "Your vault is locked." }, "uuid":{ "message" : "UUID" From 6d6785297b43e33ecf158c55b7d0b16ea9c589c7 Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Wed, 10 Jul 2024 00:11:51 -0400 Subject: [PATCH 5/5] [PM-7161] browser v2 view container (#9723) * Build new view-v2 component and reusable view sections. Custom Fields, Item Details, Attachments, Additional Info, Item History --- apps/browser/src/_locales/en/messages.json | 42 +++++ .../popup/layout/popup-footer.component.html | 5 +- .../popup/layout/popup-layout.stories.ts | 2 + apps/browser/src/popup/app-routing.module.ts | 6 +- .../vault-list-items-container.component.html | 2 +- .../vault-v2/view-v2/view-v2.component.html | 20 +++ .../vault-v2/view-v2/view-v2.component.ts | 157 ++++++++++++++++++ .../components/vault/vault-items.component.ts | 4 +- .../src/directives/copy-click.directive.ts | 18 +- .../folder/folder.service.abstraction.ts | 1 + .../vault/services/folder/folder.service.ts | 9 +- .../additional-information.component.html | 21 +++ .../additional-information.component.ts | 29 ++++ .../attachments/attachments-v2.component.html | 32 ++++ .../attachments/attachments-v2.component.ts | 153 +++++++++++++++++ .../cipher-view/cipher-view.component.html | 28 ++++ .../src/cipher-view/cipher-view.component.ts | 84 ++++++++++ .../custom-fields-v2.component.html | 76 +++++++++ .../custom-fields-v2.component.ts | 47 ++++++ libs/vault/src/cipher-view/index.ts | 1 + .../item-details-v2.component.html | 33 ++++ .../item-details/item-details-v2.component.ts | 22 +++ .../item-history-v2.component.html | 27 +++ .../item-history/item-history-v2.component.ts | 24 +++ libs/vault/src/index.ts | 1 + .../src/services/password-reprompt.service.ts | 10 ++ 26 files changed, 846 insertions(+), 8 deletions(-) create mode 100644 apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html create mode 100644 apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts create mode 100644 libs/vault/src/cipher-view/additional-information/additional-information.component.html create mode 100644 libs/vault/src/cipher-view/additional-information/additional-information.component.ts create mode 100644 libs/vault/src/cipher-view/attachments/attachments-v2.component.html create mode 100644 libs/vault/src/cipher-view/attachments/attachments-v2.component.ts create mode 100644 libs/vault/src/cipher-view/cipher-view.component.html create mode 100644 libs/vault/src/cipher-view/cipher-view.component.ts create mode 100644 libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html create mode 100644 libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts create mode 100644 libs/vault/src/cipher-view/index.ts create mode 100644 libs/vault/src/cipher-view/item-details/item-details-v2.component.html create mode 100644 libs/vault/src/cipher-view/item-details/item-details-v2.component.ts create mode 100644 libs/vault/src/cipher-view/item-history/item-history-v2.component.html create mode 100644 libs/vault/src/cipher-view/item-history/item-history-v2.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 64dd8d236ed..97841f2d451 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1483,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -3535,6 +3544,30 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou":{ + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3574,6 +3607,15 @@ "filters": { "message": "Filters" }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html index 2cbbca79c0b..777e0ab60da 100644 --- a/apps/browser/src/platform/popup/layout/popup-footer.component.html +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html @@ -1,9 +1,12 @@
-
+
+
+ +
diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index cc7758d9680..9883a5cfb6f 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -266,6 +266,7 @@ class MockSettingsPageComponent {} + `, @@ -279,6 +280,7 @@ class MockSettingsPageComponent {} MockPopoutButtonComponent, MockCurrentAccountComponent, VaultComponent, + IconButtonModule, ], }) class MockVaultSubpageComponent {} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index ff8dc7eeb6d..8645cb797bd 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -71,6 +71,7 @@ import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.compo import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; +import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; @@ -211,12 +212,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "ciphers" }, }, - { + ...extensionRefreshSwap(ViewComponent, ViewV2Component, { path: "view-cipher", - component: ViewComponent, canActivate: [AuthGuard], data: { state: "view-cipher" }, - }, + }), { path: "cipher-password-history", component: PasswordHistoryComponent, diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index 957747180eb..fbe1d60b448 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -21,7 +21,7 @@ diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html new file mode 100644 index 00000000000..c2516fe05f7 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html @@ -0,0 +1,20 @@ + + + + + + + + {{ "edit" | i18n }} + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts new file mode 100644 index 00000000000..4fe88da5550 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -0,0 +1,157 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { Observable, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + AsyncActionsModule, + SearchModule, + ButtonModule, + IconButtonModule, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; + +import { PopupFooterComponent } from "./../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "./../../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component"; + +@Component({ + selector: "app-view-v2", + templateUrl: "view-v2.component.html", + standalone: true, + imports: [ + CommonModule, + SearchModule, + JslibModule, + FormsModule, + ButtonModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + IconButtonModule, + CipherViewComponent, + AsyncActionsModule, + ], +}) +export class ViewV2Component { + headerText: string; + cipherId: string; + cipher: CipherView; + organization$: Observable; + folder$: Observable; + collections$: Observable; + private passwordReprompted = false; + + constructor( + private route: ActivatedRoute, + private router: Router, + private i18nService: I18nService, + private cipherService: CipherService, + private passwordRepromptService: PasswordRepromptService, + private dialogService: DialogService, + private logService: LogService, + private toastService: ToastService, + ) { + this.subscribeToParams(); + } + + subscribeToParams(): void { + this.route.queryParams + .pipe( + switchMap((param) => { + return this.getCipherData(param.cipherId); + }), + takeUntilDestroyed(), + ) + .subscribe((data) => { + this.cipher = data; + this.headerText = this.setHeader(data.type); + }); + } + + setHeader(type: CipherType) { + switch (type) { + case CipherType.Login: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin")); + case CipherType.Card: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard")); + case CipherType.Identity: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeIdentity")); + case CipherType.SecureNote: + return this.i18nService.t("viewItemHeader", this.i18nService.t("note")); + } + } + + async getCipherData(id: string) { + const cipher = await this.cipherService.get(id); + return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); + } + + editCipher() { + if (this.cipher.isDeleted) { + return false; + } + void this.router.navigate(["/edit-cipher"], { + queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false }, + }); + return true; + } + + delete = async (): Promise => { + this.passwordReprompted = + this.passwordReprompted || + (await this.passwordRepromptService.passwordRepromptCheck(this.cipher)); + if (!this.passwordReprompted) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/vault"]); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), + }); + + return true; + }; + + protected deleteCipher() { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } +} diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index abb810c04d5..df78806edf2 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -198,7 +198,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn super.selectCipher(cipher); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); + this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: cipher.id }, + }); } this.preventSelected = false; }, 200); diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/angular/src/directives/copy-click.directive.ts index 5e449572bf8..cee2bdde4e8 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/angular/src/directives/copy-click.directive.ts @@ -1,16 +1,32 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Directive, HostListener, Input } from "@angular/core"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Directive({ selector: "[appCopyClick]", }) export class CopyClickDirective { - constructor(private platformUtilsService: PlatformUtilsService) {} + constructor( + private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, + private i18nService: I18nService, + ) {} @Input("appCopyClick") valueToCopy = ""; + @Input({ transform: coerceBooleanProperty }) showToast?: boolean; @HostListener("click") onClick() { this.platformUtilsService.copyToClipboard(this.valueToCopy); + + if (this.showToast) { + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("copySuccessful"), + }); + } } } diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index 1d88e2d96d7..c22eb1febb9 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -17,6 +17,7 @@ export abstract class FolderService implements UserKeyRotationDataProvider Promise; encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; + getDecrypted$: (id: string) => Observable; getAllFromState: () => Promise; /** * @deprecated Only use in CLI! diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index b3cfeb3c166..17d9f39f8ec 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -1,4 +1,4 @@ -import { Observable, firstValueFrom, map } from "rxjs"; +import { Observable, firstValueFrom, map, shareReplay } from "rxjs"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; @@ -61,6 +61,13 @@ export class FolderService implements InternalFolderServiceAbstraction { return folders.find((folder) => folder.id === id); } + getDecrypted$(id: string): Observable { + return this.folderViews$.pipe( + map((folders) => folders.find((folder) => folder.id === id)), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + } + async getAllFromState(): Promise { return await firstValueFrom(this.folders$); } diff --git a/libs/vault/src/cipher-view/additional-information/additional-information.component.html b/libs/vault/src/cipher-view/additional-information/additional-information.component.html new file mode 100644 index 00000000000..931c3578706 --- /dev/null +++ b/libs/vault/src/cipher-view/additional-information/additional-information.component.html @@ -0,0 +1,21 @@ + + +

{{ "additionalInformation" | i18n }}

+
+ + +
+ + +
+
+
diff --git a/libs/vault/src/cipher-view/additional-information/additional-information.component.ts b/libs/vault/src/cipher-view/additional-information/additional-information.component.ts new file mode 100644 index 00000000000..a9660f3fc27 --- /dev/null +++ b/libs/vault/src/cipher-view/additional-information/additional-information.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + IconButtonModule, + CardComponent, + InputModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; + +@Component({ + selector: "app-additional-information", + templateUrl: "additional-information.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + CardComponent, + IconButtonModule, + InputModule, + SectionComponent, + SectionHeaderComponent, + ], +}) +export class AdditionalInformationComponent { + @Input() notes: string; +} diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html new file mode 100644 index 00000000000..acce6f2622f --- /dev/null +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html @@ -0,0 +1,32 @@ + + +

{{ "attachments" | i18n }}

+
+ + +
+

+ {{ attachment.fileName }} +

+
+ {{ attachment.sizeName }} +
+
+
+ + +
+
+
+
diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts new file mode 100644 index 00000000000..c274fa4e9ac --- /dev/null +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -0,0 +1,153 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { NEVER, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + ToastService, + ItemModule, + IconButtonModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +@Component({ + selector: "app-attachments-v2", + templateUrl: "attachments-v2.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + ItemModule, + IconButtonModule, + SectionComponent, + SectionHeaderComponent, + ], +}) +export class AttachmentsV2Component { + @Input() cipher: CipherView; + + canAccessPremium: boolean; + orgKey: OrgKey; + private passwordReprompted = false; + + constructor( + private passwordRepromptService: PasswordRepromptService, + private i18nService: I18nService, + private apiService: ApiService, + private fileDownloadService: FileDownloadService, + private cryptoService: CryptoService, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, + private stateProvider: StateProvider, + private encryptService: EncryptService, + ) { + this.subscribeToHasPremiumCheck(); + this.subscribeToOrgKey(); + } + + subscribeToHasPremiumCheck() { + this.billingAccountProfileStateService.hasPremiumFromAnySource$ + .pipe(takeUntilDestroyed()) + .subscribe((data) => { + this.canAccessPremium = data; + }); + } + + subscribeToOrgKey() { + this.stateProvider.activeUserId$ + .pipe( + switchMap((userId) => (userId != null ? this.cryptoService.orgKeys$(userId) : NEVER)), + takeUntilDestroyed(), + ) + .subscribe((data: Record | null) => { + if (data) { + this.orgKey = data[this.cipher.organizationId as OrganizationId]; + } + }); + } + + async downloadAttachment(attachment: any) { + this.passwordReprompted = + this.passwordReprompted || + (await this.passwordRepromptService.passwordRepromptCheck(this.cipher)); + if (!this.passwordReprompted) { + return; + } + const file = attachment as any; + + if (file.downloading) { + return; + } + + if (this.cipher.organizationId == null && !this.canAccessPremium) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("premiumRequired"), + message: this.i18nService.t("premiumRequiredDesc"), + }); + return; + } + + let url: string; + try { + const attachmentDownloadResponse = await this.apiService.getAttachmentData( + this.cipher.id, + attachment.id, + ); + url = attachmentDownloadResponse.url; + } catch (e) { + if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { + url = attachment.url; + } else if (e instanceof ErrorResponse) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } else { + throw e; + } + } + + file.downloading = true; + const response = await fetch(new Request(url, { cache: "no-store" })); + if (response.status !== 200) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); + file.downloading = false; + return; + } + + try { + const encBuf = await EncArrayBuffer.fromResponse(response); + const key = attachment.key != null ? attachment.key : this.orgKey; + const decBuf = await this.encryptService.decryptToBytes(encBuf, key); + this.fileDownloadService.download({ + fileName: attachment.fileName, + blobData: decBuf, + }); + } catch (e) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); + } + + file.downloading = false; + } +} diff --git a/libs/vault/src/cipher-view/cipher-view.component.html b/libs/vault/src/cipher-view/cipher-view.component.html new file mode 100644 index 00000000000..575d80257ea --- /dev/null +++ b/libs/vault/src/cipher-view/cipher-view.component.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts new file mode 100644 index 00000000000..4764b571473 --- /dev/null +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -0,0 +1,84 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input, OnInit } from "@angular/core"; +import { Observable, Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionId } from "@bitwarden/common/types/guid"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { SearchModule } from "@bitwarden/components"; + +import { PopupFooterComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../apps/browser/src/platform/popup/layout/popup-page.component"; + +import { AdditionalInformationComponent } from "./additional-information/additional-information.component"; +import { AttachmentsV2Component } from "./attachments/attachments-v2.component"; +import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.component"; +import { ItemDetailsV2Component } from "./item-details/item-details-v2.component"; +import { ItemHistoryV2Component } from "./item-history/item-history-v2.component"; + +@Component({ + selector: "app-cipher-view", + templateUrl: "cipher-view.component.html", + standalone: true, + imports: [ + CommonModule, + SearchModule, + JslibModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + ItemDetailsV2Component, + AdditionalInformationComponent, + AttachmentsV2Component, + ItemHistoryV2Component, + CustomFieldV2Component, + ], +}) +export class CipherViewComponent implements OnInit { + @Input() cipher: CipherView; + organization$: Observable; + folder$: Observable; + collections$: Observable; + private destroyed$: Subject = new Subject(); + + constructor( + private organizationService: OrganizationService, + private collectionService: CollectionService, + private folderService: FolderService, + ) {} + + async ngOnInit() { + await this.loadCipherData(); + } + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + async loadCipherData() { + if (this.cipher.collectionIds.length > 0) { + this.collections$ = this.collectionService + .decryptedCollectionViews$(this.cipher.collectionIds as CollectionId[]) + .pipe(takeUntil(this.destroyed$)); + } + + if (this.cipher.organizationId) { + this.organization$ = this.organizationService + .get$(this.cipher.organizationId) + .pipe(takeUntil(this.destroyed$)); + } + + if (this.cipher.folderId) { + this.folder$ = this.folderService + .getDecrypted$(this.cipher.folderId) + .pipe(takeUntil(this.destroyed$)); + } + } +} diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html new file mode 100644 index 00000000000..91a380f53d5 --- /dev/null +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -0,0 +1,76 @@ + + +

{{ "customFields" | i18n }}

+
+ +
+ + +
+ + +
+
+ + + + + + + + + +
+ +
+ {{ field.name }} +
+
+
+ + +
+ + +
+
+
+
+
diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts new file mode 100644 index 00000000000..a40bca2d261 --- /dev/null +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -0,0 +1,47 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { FieldType, LinkedIdType, LoginLinkedId } from "@bitwarden/common/vault/enums"; +import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { + CardComponent, + IconButtonModule, + FormFieldModule, + InputModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; + +@Component({ + selector: "app-custom-fields-v2", + templateUrl: "custom-fields-v2.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + CardComponent, + IconButtonModule, + FormFieldModule, + InputModule, + SectionComponent, + SectionHeaderComponent, + ], +}) +export class CustomFieldV2Component { + @Input() fields: FieldView[]; + fieldType = FieldType; + + constructor(private i18nService: I18nService) {} + + getLinkedType(linkedId: LinkedIdType) { + if (linkedId === LoginLinkedId.Username) { + return this.i18nService.t("username"); + } + + if (linkedId === LoginLinkedId.Password) { + return this.i18nService.t("password"); + } + } +} diff --git a/libs/vault/src/cipher-view/index.ts b/libs/vault/src/cipher-view/index.ts new file mode 100644 index 00000000000..8231f5c1611 --- /dev/null +++ b/libs/vault/src/cipher-view/index.ts @@ -0,0 +1 @@ +export * from "./cipher-view.component"; diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.html b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html new file mode 100644 index 00000000000..0ade00679af --- /dev/null +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.html @@ -0,0 +1,33 @@ + + +

{{ "itemDetails" | i18n }}

+
+ +
+ + +
+ +
+
+ {{ "ownerYou" | i18n }} +
+
+ {{ organization.name }} +
+
+

+ {{ collection.name }} +

+
+
+ {{ folder.name }} +
+
+
+
diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts new file mode 100644 index 00000000000..b0d158c1409 --- /dev/null +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -0,0 +1,22 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwarden/components"; + +@Component({ + selector: "app-item-details-v2", + templateUrl: "item-details-v2.component.html", + standalone: true, + imports: [CommonModule, JslibModule, CardComponent, SectionComponent, SectionHeaderComponent], +}) +export class ItemDetailsV2Component { + @Input() cipher: CipherView; + @Input() organization?: Organization; + @Input() collections?: CollectionView[]; + @Input() folder?: FolderView; +} diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.html b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html new file mode 100644 index 00000000000..c1e11b9e58b --- /dev/null +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.html @@ -0,0 +1,27 @@ + + +

{{ "itemHistory" | i18n }}

+
+ +

+ {{ "lastEdited" | i18n }}: + {{ cipher.revisionDate | date: "medium" }} +

+

+ {{ "dateCreated" | i18n }}: + {{ cipher.creationDate | date: "medium" }} +

+

+ {{ "datePasswordUpdated" | i18n }}: + {{ cipher.passwordRevisionDisplayDate | date: "medium" }} +

+ + {{ "passwordHistory" | i18n }} + +
+
diff --git a/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts new file mode 100644 index 00000000000..51badfdbc89 --- /dev/null +++ b/libs/vault/src/cipher-view/item-history/item-history-v2.component.ts @@ -0,0 +1,24 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CardComponent, SectionComponent, SectionHeaderComponent } from "@bitwarden/components"; + +@Component({ + selector: "app-item-history-v2", + templateUrl: "item-history-v2.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + RouterModule, + CardComponent, + SectionComponent, + SectionHeaderComponent, + ], +}) +export class ItemHistoryV2Component { + @Input() cipher: CipherView; +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 82b20bbe538..e4e17e7aa5a 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -2,4 +2,5 @@ export { PasswordRepromptService } from "./services/password-reprompt.service"; export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service"; export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive"; +export * from "./cipher-view"; export * from "./cipher-form"; diff --git a/libs/vault/src/services/password-reprompt.service.ts b/libs/vault/src/services/password-reprompt.service.ts index 6aba602c16f..8621c436bae 100644 --- a/libs/vault/src/services/password-reprompt.service.ts +++ b/libs/vault/src/services/password-reprompt.service.ts @@ -2,6 +2,8 @@ import { Injectable } from "@angular/core"; import { lastValueFrom } from "rxjs"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptComponent } from "../components/password-reprompt.component"; @@ -21,6 +23,14 @@ export class PasswordRepromptService { return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"]; } + async passwordRepromptCheck(cipher: CipherView) { + if (cipher.reprompt === CipherRepromptType.None) { + return true; + } + + return await this.showPasswordPrompt(); + } + async showPasswordPrompt() { if (!(await this.enabled())) { return true;