mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-18 20:41:31 +01:00
Merge branch 'main' into ps/PM-7846-rust-ipc
This commit is contained in:
commit
af89116b22
@ -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"
|
||||
},
|
||||
|
@ -1,9 +1,12 @@
|
||||
<footer
|
||||
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">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-h-9">
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -266,6 +266,7 @@ class MockSettingsPageComponent {}
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton buttonType="primary">Save</button>
|
||||
<button bitButton buttonType="secondary">Cancel</button>
|
||||
<button slot="end" type="button" buttonType="danger" bitIconButton="bwi-trash"></button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
`,
|
||||
@ -279,6 +280,7 @@ class MockSettingsPageComponent {}
|
||||
MockPopoutButtonComponent,
|
||||
MockCurrentAccountComponent,
|
||||
VaultComponent,
|
||||
IconButtonModule,
|
||||
],
|
||||
})
|
||||
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 { 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,
|
||||
|
@ -21,7 +21,7 @@
|
||||
<a
|
||||
bit-item-content
|
||||
[routerLink]="['/view-cipher']"
|
||||
[queryParams]="{ cipherId: cipher.id }"
|
||||
[queryParams]="{ cipherId: cipher.id, type: cipher.type }"
|
||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||
>
|
||||
<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);
|
||||
// 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);
|
||||
|
@ -1,66 +1,27 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="text-center mb-4">
|
||||
<i class="bwi bwi-lock bwi-4x text-muted" aria-hidden="true"></i>
|
||||
</p>
|
||||
<p class="lead text-center mx-4 mb-4">{{ "yourVaultIsLocked" | i18n }}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<div class="d-flex">
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="MasterPassword"
|
||||
class="text-monospace form-control"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 btn btn-link"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted form-text">
|
||||
{{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }}
|
||||
</small>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-block btn-submit"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<span> <i class="bwi bwi-unlock" aria-hidden="true"></i> {{ "unlock" | i18n }} </span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-block ml-2 mt-0"
|
||||
(click)="logOut()"
|
||||
>
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form [bitSubmit]="submit" [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
type="password"
|
||||
formControlName="masterPassword"
|
||||
bitInput
|
||||
appAutofocus
|
||||
name="masterPassword"
|
||||
required
|
||||
/>
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
<bit-hint>{{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" block>
|
||||
<i class="bwi bwi-unlock" aria-hidden="true"></i>
|
||||
{{ "unlock" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton block (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
@ -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()],
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid
|
||||
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
|
||||
* e.g. <form [appApiAction]="this.formPromise">
|
||||
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
|
||||
*
|
||||
* @deprecated Use the CL's {@link BitSubmitDirective} instead
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[appApiAction]",
|
||||
|
@ -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"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ export abstract class FolderService implements UserKeyRotationDataProvider<Folde
|
||||
clearCache: () => Promise<void>;
|
||||
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
|
||||
get: (id: string) => Promise<Folder>;
|
||||
getDecrypted$: (id: string) => Observable<FolderView>;
|
||||
getAllFromState: () => Promise<Folder[]>;
|
||||
/**
|
||||
* @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 { 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<FolderView | undefined> {
|
||||
return this.folderViews$.pipe(
|
||||
map((folders) => folders.find((folder) => folder.id === id)),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
}
|
||||
|
||||
async getAllFromState(): Promise<Folder[]> {
|
||||
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 { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive";
|
||||
|
||||
export * from "./cipher-view";
|
||||
export * from "./cipher-form";
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user