diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 64dd8d236e..97841f2d45 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 2cbbca79c0..777e0ab60d 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 cc7758d968..9883a5cfb6 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 ff8dc7eeb6..8645cb797b 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 957747180e..fbe1d60b44 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 0000000000..c2516fe05f --- /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 0000000000..4fe88da555 --- /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 abb810c04d..df78806edf 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 5e449572bf..cee2bdde4e 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 1d88e2d96d..c22eb1febb 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 b3cfeb3c16..17d9f39f8e 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 0000000000..931c357870 --- /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 0000000000..a9660f3fc2 --- /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 0000000000..acce6f2622 --- /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 0000000000..c274fa4e9a --- /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 0000000000..575d80257e --- /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 0000000000..4764b57147 --- /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 0000000000..91a380f53d --- /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 0000000000..a40bca2d26 --- /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 0000000000..8231f5c161 --- /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 0000000000..0ade00679a --- /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 0000000000..b0d158c140 --- /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 0000000000..c1e11b9e58 --- /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 0000000000..51badfdbc8 --- /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 82b20bbe53..e4e17e7aa5 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 6aba602c16..8621c436ba 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;