1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-17 20:31:50 +01:00

Merge branch 'main' into ps/PM-7846-rust-ipc

This commit is contained in:
Daniel García 2024-07-10 11:28:58 +02:00
commit af89116b22
No known key found for this signature in database
GPG Key ID: 2FA30DC7130BE717
33 changed files with 930 additions and 87 deletions

View File

@ -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"
},

View File

@ -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>

View File

@ -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 {}

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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>

View File

@ -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();
};
}

View File

@ -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()],

View File

@ -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,

View File

@ -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"
}
}

View File

@ -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;
}
}

View File

@ -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]",

View File

@ -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"),
});
}
}
}

View File

@ -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!

View File

@ -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$);
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}
}

View 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>

View 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$));
}
}
}

View File

@ -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>

View File

@ -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");
}
}
}

View File

@ -0,0 +1 @@
export * from "./cipher-view.component";

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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";

View File

@ -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;