mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-23 11:56:00 +01:00
[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
This commit is contained in:
parent
7dfef8991c
commit
6d6785297b
@ -1483,6 +1483,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"viewItemHeader": {
|
||||||
|
"message": "View $TYPE$",
|
||||||
|
"placeholders": {
|
||||||
|
"type": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"passwordHistory": {
|
"passwordHistory": {
|
||||||
"message": "Password history"
|
"message": "Password history"
|
||||||
},
|
},
|
||||||
@ -3535,6 +3544,30 @@
|
|||||||
"contactYourOrgAdmin": {
|
"contactYourOrgAdmin": {
|
||||||
"message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance."
|
"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": {
|
"upload": {
|
||||||
"message": "Upload"
|
"message": "Upload"
|
||||||
},
|
},
|
||||||
@ -3574,6 +3607,15 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"message": "Filters"
|
"message": "Filters"
|
||||||
},
|
},
|
||||||
|
"downloadAttachment": {
|
||||||
|
"message": "Download - $ITEMNAME$",
|
||||||
|
"placeholders": {
|
||||||
|
"itemname": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Your File"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"cardDetails": {
|
"cardDetails": {
|
||||||
"message": "Card details"
|
"message": "Card details"
|
||||||
},
|
},
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
<footer
|
<footer
|
||||||
class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
|
class="tw-p-3 tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-bg-background"
|
||||||
>
|
>
|
||||||
<div class="tw-max-w-screen-sm tw-mx-auto">
|
<div class="tw-max-w-screen-sm tw-mx-auto tw-flex tw-justify-between tw-w-full">
|
||||||
<div class="tw-flex tw-justify-start tw-gap-2">
|
<div class="tw-flex tw-justify-start tw-gap-2">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||||
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -266,6 +266,7 @@ class MockSettingsPageComponent {}
|
|||||||
<popup-footer slot="footer">
|
<popup-footer slot="footer">
|
||||||
<button bitButton buttonType="primary">Save</button>
|
<button bitButton buttonType="primary">Save</button>
|
||||||
<button bitButton buttonType="secondary">Cancel</button>
|
<button bitButton buttonType="secondary">Cancel</button>
|
||||||
|
<button slot="end" type="button" buttonType="danger" bitIconButton="bwi-trash"></button>
|
||||||
</popup-footer>
|
</popup-footer>
|
||||||
</popup-page>
|
</popup-page>
|
||||||
`,
|
`,
|
||||||
@ -279,6 +280,7 @@ class MockSettingsPageComponent {}
|
|||||||
MockPopoutButtonComponent,
|
MockPopoutButtonComponent,
|
||||||
MockCurrentAccountComponent,
|
MockCurrentAccountComponent,
|
||||||
VaultComponent,
|
VaultComponent,
|
||||||
|
IconButtonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
class MockVaultSubpageComponent {}
|
class MockVaultSubpageComponent {}
|
||||||
|
@ -71,6 +71,7 @@ import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.compo
|
|||||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||||
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.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 { 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 { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||||
@ -211,12 +212,11 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "ciphers" },
|
data: { state: "ciphers" },
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(ViewComponent, ViewV2Component, {
|
||||||
path: "view-cipher",
|
path: "view-cipher",
|
||||||
component: ViewComponent,
|
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "view-cipher" },
|
data: { state: "view-cipher" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "cipher-password-history",
|
path: "cipher-password-history",
|
||||||
component: PasswordHistoryComponent,
|
component: PasswordHistoryComponent,
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<a
|
<a
|
||||||
bit-item-content
|
bit-item-content
|
||||||
[routerLink]="['/view-cipher']"
|
[routerLink]="['/view-cipher']"
|
||||||
[queryParams]="{ cipherId: cipher.id }"
|
[queryParams]="{ cipherId: cipher.id, type: cipher.type }"
|
||||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||||
>
|
>
|
||||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
<popup-page>
|
||||||
|
<popup-header slot="header" [pageTitle]="headerText" showBackButton> </popup-header>
|
||||||
|
|
||||||
|
<app-cipher-view *ngIf="cipher" [cipher]="cipher"></app-cipher-view>
|
||||||
|
|
||||||
|
<popup-footer slot="footer">
|
||||||
|
<a bitButton type="button" buttonType="primary" (click)="editCipher()">
|
||||||
|
{{ "edit" | i18n }}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
slot="end"
|
||||||
|
*ngIf="cipher && cipher.edit"
|
||||||
|
[bitAction]="delete"
|
||||||
|
type="button"
|
||||||
|
buttonType="danger"
|
||||||
|
bitIconButton="bwi-trash"
|
||||||
|
[appA11yTitle]="'delete' | i18n"
|
||||||
|
></button>
|
||||||
|
</popup-footer>
|
||||||
|
</popup-page>
|
@ -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<Organization>;
|
||||||
|
folder$: Observable<FolderView>;
|
||||||
|
collections$: Observable<CollectionView[]>;
|
||||||
|
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<boolean> => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -198,7 +198,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn
|
|||||||
super.selectCipher(cipher);
|
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.
|
// 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
|
// 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;
|
this.preventSelected = false;
|
||||||
}, 200);
|
}, 200);
|
||||||
|
@ -1,16 +1,32 @@
|
|||||||
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { Directive, HostListener, Input } from "@angular/core";
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "[appCopyClick]",
|
selector: "[appCopyClick]",
|
||||||
})
|
})
|
||||||
export class CopyClickDirective {
|
export class CopyClickDirective {
|
||||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
constructor(
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Input("appCopyClick") valueToCopy = "";
|
@Input("appCopyClick") valueToCopy = "";
|
||||||
|
@Input({ transform: coerceBooleanProperty }) showToast?: boolean;
|
||||||
|
|
||||||
@HostListener("click") onClick() {
|
@HostListener("click") onClick() {
|
||||||
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
this.platformUtilsService.copyToClipboard(this.valueToCopy);
|
||||||
|
|
||||||
|
if (this.showToast) {
|
||||||
|
this.toastService.showToast({
|
||||||
|
variant: "info",
|
||||||
|
title: null,
|
||||||
|
message: this.i18nService.t("copySuccessful"),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ export abstract class FolderService implements UserKeyRotationDataProvider<Folde
|
|||||||
clearCache: () => Promise<void>;
|
clearCache: () => Promise<void>;
|
||||||
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
|
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
|
||||||
get: (id: string) => Promise<Folder>;
|
get: (id: string) => Promise<Folder>;
|
||||||
|
getDecrypted$: (id: string) => Observable<FolderView>;
|
||||||
getAllFromState: () => Promise<Folder[]>;
|
getAllFromState: () => Promise<Folder[]>;
|
||||||
/**
|
/**
|
||||||
* @deprecated Only use in CLI!
|
* @deprecated Only use in CLI!
|
||||||
|
@ -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 { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||||
@ -61,6 +61,13 @@ export class FolderService implements InternalFolderServiceAbstraction {
|
|||||||
return folders.find((folder) => folder.id === id);
|
return folders.find((folder) => folder.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDecrypted$(id: string): Observable<FolderView | undefined> {
|
||||||
|
return this.folderViews$.pipe(
|
||||||
|
map((folders) => folders.find((folder) => folder.id === id)),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getAllFromState(): Promise<Folder[]> {
|
async getAllFromState(): Promise<Folder[]> {
|
||||||
return await firstValueFrom(this.folders$);
|
return await firstValueFrom(this.folders$);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "additionalInformation" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-card>
|
||||||
|
<label class="tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
{{ "note" | i18n }}
|
||||||
|
</label>
|
||||||
|
<div class="tw-flex tw-justify-between">
|
||||||
|
<textarea readonly bitInput aria-readonly="true">{{ notes }}</textarea>
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
[appCopyClick]="notes"
|
||||||
|
showToast
|
||||||
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</bit-card>
|
||||||
|
</bit-section>
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "attachments" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-item-group>
|
||||||
|
<bit-item *ngFor="let attachment of cipher.attachments">
|
||||||
|
<div slot="start" class="tw-py-4 tw-px-3">
|
||||||
|
<h3>
|
||||||
|
{{ attachment.fileName }}
|
||||||
|
</h3>
|
||||||
|
<div class="tw-mb-1 tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
{{ attachment.sizeName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex tw-items-center" (click)="downloadAttachment(attachment)" slot="end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-download"
|
||||||
|
size="small"
|
||||||
|
[appA11yTitle]="'downloadAttachment' | i18n: attachment.fileName"
|
||||||
|
*ngIf="!$any(attachment).downloading"
|
||||||
|
></button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-spinner bwi-spin"
|
||||||
|
size="small"
|
||||||
|
*ngIf="$any(attachment).downloading"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
</bit-section>
|
@ -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<OrganizationId, OrgKey> | 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;
|
||||||
|
}
|
||||||
|
}
|
28
libs/vault/src/cipher-view/cipher-view.component.html
Normal file
28
libs/vault/src/cipher-view/cipher-view.component.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<ng-container *ngIf="!!cipher">
|
||||||
|
<!-- ITEM DETAILS -->
|
||||||
|
<app-item-details-v2
|
||||||
|
[cipher]="cipher"
|
||||||
|
[organization]="organization$ | async"
|
||||||
|
[collections]="collections$ | async"
|
||||||
|
[folder]="folder$ | async"
|
||||||
|
>
|
||||||
|
</app-item-details-v2>
|
||||||
|
|
||||||
|
<!-- ADDITIONAL INFORMATION -->
|
||||||
|
<ng-container *ngIf="cipher.notes">
|
||||||
|
<app-additional-information [notes]="cipher.notes"> </app-additional-information>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- CUSTOM FIELDS -->
|
||||||
|
<ng-container *ngIf="cipher.fields">
|
||||||
|
<app-custom-fields-v2 [fields]="cipher.fields"> </app-custom-fields-v2>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- ATTACHMENTS SECTION -->
|
||||||
|
<ng-container *ngIf="cipher.attachments">
|
||||||
|
<app-attachments-v2 [cipher]="cipher"> </app-attachments-v2>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- ITEM HISTORY SECTION -->
|
||||||
|
<app-item-history-v2 [cipher]="cipher"> </app-item-history-v2>
|
||||||
|
</ng-container>
|
84
libs/vault/src/cipher-view/cipher-view.component.ts
Normal file
84
libs/vault/src/cipher-view/cipher-view.component.ts
Normal file
@ -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<Organization>;
|
||||||
|
folder$: Observable<FolderView>;
|
||||||
|
collections$: Observable<CollectionView[]>;
|
||||||
|
private destroyed$: Subject<void> = 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$));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "customFields" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-card>
|
||||||
|
<div
|
||||||
|
class="tw-mb-4 tw-border-secondary-300 tw-bg-background"
|
||||||
|
*ngFor="let field of fields; let last = last"
|
||||||
|
[ngClass]="{ 'tw-border-0 tw-border-b tw-border-solid tw-pb-2 tw-mb-4': !last }"
|
||||||
|
>
|
||||||
|
<ng-container *ngIf="field.type === fieldType.Text">
|
||||||
|
<label class="tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
{{ field.name }}
|
||||||
|
</label>
|
||||||
|
<div class="tw-flex tw-justify-between">
|
||||||
|
<input readonly bitInput type="text" [value]="field.value" aria-readonly="true" />
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
[appCopyClick]="field.value"
|
||||||
|
showToast
|
||||||
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="field.type === fieldType.Hidden">
|
||||||
|
<label class="tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
{{ field.name }}
|
||||||
|
</label>
|
||||||
|
<bit-form-field>
|
||||||
|
<input readonly bitInput type="password" [value]="field.value" aria-readonly="true" />
|
||||||
|
<button type="button" bitIconButton bitPasswordInputToggle></button>
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
[appCopyClick]="field.value"
|
||||||
|
showToast
|
||||||
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
|
></button>
|
||||||
|
</bit-form-field>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="field.type === fieldType.Boolean">
|
||||||
|
<div class="tw-flex tw-my-2">
|
||||||
|
<input type="checkbox" [value]="field.value" readonly aria-readonly="true" />
|
||||||
|
<h5 class="tw-ml-3">
|
||||||
|
{{ field.name }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="field.type === fieldType.Linked">
|
||||||
|
<label class="tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
{{ "linked" | i18n }}: {{ field.name }}
|
||||||
|
</label>
|
||||||
|
<div class="tw-flex tw-justify-between">
|
||||||
|
<input
|
||||||
|
readonly
|
||||||
|
bitInput
|
||||||
|
type="text"
|
||||||
|
[value]="getLinkedType(field.linkedId)"
|
||||||
|
aria-readonly="true"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
bitIconButton="bwi-clone"
|
||||||
|
size="small"
|
||||||
|
type="button"
|
||||||
|
[appCopyClick]="field.name"
|
||||||
|
showToast
|
||||||
|
[appA11yTitle]="'copyValue' | i18n"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</bit-card>
|
||||||
|
</bit-section>
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
libs/vault/src/cipher-view/index.ts
Normal file
1
libs/vault/src/cipher-view/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./cipher-view.component";
|
@ -0,0 +1,33 @@
|
|||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "itemDetails" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-card>
|
||||||
|
<div class="tw-pb-2 tw-mb-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300">
|
||||||
|
<label class="tw-block tw-w-full tw-mb-1 tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
{{ "itemName" | i18n }}
|
||||||
|
</label>
|
||||||
|
<input readonly bitInput type="text" [value]="cipher.login.username" aria-readonly="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="cipher.collectionIds || cipher.organizationId || cipher.folderId">
|
||||||
|
<div *ngIf="!cipher.organizationId" [ngClass]="{ 'tw-mb-3': cipher.collectionIds }">
|
||||||
|
<i class="bwi bwi-user bwi-lg bwi-fw"></i> {{ "ownerYou" | i18n }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
*ngIf="cipher.organizationId && organization"
|
||||||
|
[ngClass]="{ 'tw-mb-3': cipher.collectionIds }"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-business bwi-lg bwi-fw"></i> {{ organization.name }}
|
||||||
|
</div>
|
||||||
|
<div *ngIf="cipher.collectionIds && collections" [ngClass]="{ 'tw-mb-3': cipher.folderId }">
|
||||||
|
<h3 *ngFor="let collection of collections">
|
||||||
|
<i class="bwi bwi-collection bwi-lg bwi-fw"></i> {{ collection.name }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="cipher.folderId && folder">
|
||||||
|
<i class="bwi bwi-folder bwi-lg bwi-fw"></i> {{ folder.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</bit-card>
|
||||||
|
</bit-section>
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
<bit-section>
|
||||||
|
<bit-section-header>
|
||||||
|
<h2 bitTypography="h6">{{ "itemHistory" | i18n }}</h2>
|
||||||
|
</bit-section-header>
|
||||||
|
<bit-card>
|
||||||
|
<p class="tw-mb-1 tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
<span class="tw-font-bold">{{ "lastEdited" | i18n }}:</span>
|
||||||
|
{{ cipher.revisionDate | date: "medium" }}
|
||||||
|
</p>
|
||||||
|
<p class="tw-mb-1 tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
<span class="tw-font-bold">{{ "dateCreated" | i18n }}:</span>
|
||||||
|
{{ cipher.creationDate | date: "medium" }}
|
||||||
|
</p>
|
||||||
|
<p class="tw-mb-1 tw-text-xs tw-text-muted tw-select-none">
|
||||||
|
<span class="tw-font-bold">{{ "datePasswordUpdated" | i18n }}:</span>
|
||||||
|
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
*ngIf="cipher.hasPasswordHistory"
|
||||||
|
class="tw-font-bold tw-no-underline"
|
||||||
|
routerLink="/cipher-password-history"
|
||||||
|
[queryParams]="{ cipherId: cipher.id }"
|
||||||
|
>
|
||||||
|
{{ "passwordHistory" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-card>
|
||||||
|
</bit-section>
|
@ -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;
|
||||||
|
}
|
@ -2,4 +2,5 @@ export { PasswordRepromptService } from "./services/password-reprompt.service";
|
|||||||
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
|
export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service";
|
||||||
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
|
export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
|
||||||
|
|
||||||
|
export * from "./cipher-view";
|
||||||
export * from "./cipher-form";
|
export * from "./cipher-form";
|
||||||
|
@ -2,6 +2,8 @@ import { Injectable } from "@angular/core";
|
|||||||
import { lastValueFrom } from "rxjs";
|
import { lastValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
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 { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
||||||
@ -21,6 +23,14 @@ export class PasswordRepromptService {
|
|||||||
return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"];
|
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() {
|
async showPasswordPrompt() {
|
||||||
if (!(await this.enabled())) {
|
if (!(await this.enabled())) {
|
||||||
return true;
|
return true;
|
||||||
|
Loading…
Reference in New Issue
Block a user