mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[PM-7105][PM-7242][PM-16256] Remove v1 code for Tab/Vault Part 2 (#12516)
* Remove v1 code for Tab/Vault Part 2 * Removal conditional for assign-collections --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
26f086368b
commit
6aa5b1b953
@@ -93,28 +93,16 @@ import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-
|
||||
import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component";
|
||||
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
|
||||
import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard";
|
||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
||||
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
|
||||
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
|
||||
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.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 { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
|
||||
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
|
||||
import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component";
|
||||
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
|
||||
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
|
||||
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
|
||||
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
|
||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||
import { TrashComponent } from "../vault/popup/settings/trash.component";
|
||||
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
||||
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||
|
||||
import { RouteElevation } from "./app-routing.animations";
|
||||
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
||||
@@ -271,56 +259,43 @@ const routes: Routes = [
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "ciphers",
|
||||
component: VaultItemsComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(ViewComponent, ViewV2Component, {
|
||||
path: "view-cipher",
|
||||
component: ViewV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
// Above "trash"
|
||||
elevation: 3,
|
||||
} satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(PasswordHistoryComponent, PasswordHistoryV2Component, {
|
||||
},
|
||||
{
|
||||
path: "cipher-password-history",
|
||||
component: PasswordHistoryV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
},
|
||||
{
|
||||
path: "add-cipher",
|
||||
component: AddEditV2Component,
|
||||
canActivate: [authGuard, debounceNavigationGuard()],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
runGuardsAndResolvers: "always",
|
||||
}),
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
},
|
||||
{
|
||||
path: "edit-cipher",
|
||||
component: AddEditV2Component,
|
||||
canActivate: [authGuard, debounceNavigationGuard()],
|
||||
data: {
|
||||
// Above "trash"
|
||||
elevation: 3,
|
||||
} satisfies RouteDataProperties,
|
||||
runGuardsAndResolvers: "always",
|
||||
}),
|
||||
{
|
||||
path: "share-cipher",
|
||||
component: ShareComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "collections",
|
||||
component: CollectionsComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, {
|
||||
path: "attachments",
|
||||
component: AttachmentsV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "generator",
|
||||
component: CredentialGeneratorComponent,
|
||||
@@ -361,33 +336,17 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, {
|
||||
{
|
||||
path: "vault-settings",
|
||||
component: VaultSettingsV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(FoldersComponent, FoldersV2Component, {
|
||||
},
|
||||
{
|
||||
path: "folders",
|
||||
component: FoldersV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 2 } satisfies RouteDataProperties,
|
||||
}),
|
||||
{
|
||||
path: "add-folder",
|
||||
component: FolderAddEditComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "edit-folder",
|
||||
component: FolderAddEditComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
path: "sync",
|
||||
component: SyncComponent,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
|
||||
path: "excluded-domains",
|
||||
@@ -400,16 +359,18 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, {
|
||||
{
|
||||
path: "appearance",
|
||||
component: AppearanceV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
},
|
||||
{
|
||||
path: "clone-cipher",
|
||||
component: AddEditV2Component,
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "add-send",
|
||||
component: SendAddEditV2Component,
|
||||
@@ -685,7 +646,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: "assign-collections",
|
||||
component: AssignCollections,
|
||||
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")],
|
||||
canActivate: [authGuard],
|
||||
data: { elevation: 1 } satisfies RouteDataProperties,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -54,25 +54,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp
|
||||
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
|
||||
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
|
||||
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
|
||||
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
|
||||
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
|
||||
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
|
||||
import { CurrentTabComponent } from "../vault/popup/components/vault/current-tab.component";
|
||||
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
|
||||
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
||||
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
||||
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
||||
import { ViewComponent } from "../vault/popup/components/vault/view.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";
|
||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
|
||||
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
@@ -127,48 +108,29 @@ import "../platform/popup/locales";
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
],
|
||||
declarations: [
|
||||
ActionButtonsComponent,
|
||||
AddEditComponent,
|
||||
AddEditCustomFieldsComponent,
|
||||
AppComponent,
|
||||
AttachmentsComponent,
|
||||
CipherRowComponent,
|
||||
VaultItemsComponent,
|
||||
CollectionsComponent,
|
||||
ColorPasswordPipe,
|
||||
ColorPasswordCountPipe,
|
||||
CurrentTabComponent,
|
||||
EnvironmentComponent,
|
||||
ExcludedDomainsV1Component,
|
||||
Fido2CipherRowV1Component,
|
||||
Fido2UseBrowserLinkV1Component,
|
||||
FolderAddEditComponent,
|
||||
FoldersComponent,
|
||||
VaultFilterComponent,
|
||||
HintComponent,
|
||||
HomeComponent,
|
||||
LoginViaAuthRequestComponentV1,
|
||||
LoginComponentV1,
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
NotificationsSettingsV1Component,
|
||||
AppearanceComponent,
|
||||
PasswordHistoryComponent,
|
||||
RegisterComponent,
|
||||
SetPasswordComponent,
|
||||
VaultSettingsComponent,
|
||||
ShareComponent,
|
||||
SsoComponentV1,
|
||||
SyncComponent,
|
||||
TabsV2Component,
|
||||
TwoFactorComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
UpdateTempPasswordComponent,
|
||||
UserVerificationComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
ViewComponent,
|
||||
ViewCustomFieldsComponent,
|
||||
RemovePasswordComponent,
|
||||
VaultSelectComponent,
|
||||
Fido2V1Component,
|
||||
AutofillV1Component,
|
||||
EnvironmentSelectorComponent,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanDeactivateFn } from "@angular/router";
|
||||
|
||||
import { VaultV2Component } from "../popup/components/vault/vault-v2.component";
|
||||
import { VaultV2Component } from "../popup/components/vault-v2/vault-v2.component";
|
||||
import { VaultPopupItemsService } from "../popup/services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../popup/services/vault-popup-list-filters.service";
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
(click)="view()"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'view' | i18n }}"
|
||||
*ngIf="showView"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-list-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ng-container *ngIf="cipher.type === cipherType.Login">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
(click)="launchCipher()"
|
||||
*ngIf="!showView"
|
||||
[ngClass]="{ disabled: !cipher.login.canLaunch }"
|
||||
[attr.disabled]="!cipher.login.canLaunch ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyUsername' | i18n }}"
|
||||
(click)="copy(cipher, cipher.login.username, 'username', 'Username')"
|
||||
[ngClass]="{ disabled: !cipher.login.username }"
|
||||
[attr.disabled]="!cipher.login.username ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-user" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(cipher, cipher.login.password, 'password', 'Password')"
|
||||
[ngClass]="{ disabled: !cipher.login.password || !cipher.viewPassword }"
|
||||
[attr.disabled]="!cipher.login.password ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyVerificationCode' | i18n }}"
|
||||
(click)="copy(cipher, cipher.login.totp, 'verificationCodeTotp', 'TOTP')"
|
||||
[ngClass]="{ disabled: !displayTotpCopyButton(cipher) }"
|
||||
[attr.disabled]="!displayTotpCopyButton(cipher) ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clock" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="cipher.type === cipherType.Card">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyNumber' | i18n }}"
|
||||
(click)="copy(cipher, cipher.card.number, 'number', 'Card Number')"
|
||||
[ngClass]="{ disabled: !cipher.card.number }"
|
||||
[attr.disabled]="!cipher.card.number ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-hashtag" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
|
||||
(click)="copy(cipher, cipher.card.code, 'securityCode', 'Security Code')"
|
||||
[ngClass]="{ disabled: !cipher.card.code }"
|
||||
[attr.disabled]="!cipher.card.code ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="cipher.type === cipherType.SecureNote">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appStopProp
|
||||
appA11yTitle="{{ 'copyNote' | i18n }}"
|
||||
(click)="copy(cipher, cipher.notes, 'note', 'Note')"
|
||||
[ngClass]="{ disabled: !cipher.notes }"
|
||||
[attr.disabled]="!cipher.notes ? '' : null"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
@@ -1,108 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
selector: "app-action-buttons",
|
||||
templateUrl: "action-buttons.component.html",
|
||||
})
|
||||
export class ActionButtonsComponent implements OnInit, OnDestroy {
|
||||
@Output() onView = new EventEmitter<CipherView>();
|
||||
@Output() launchEvent = new EventEmitter<CipherView>();
|
||||
@Input() cipher: CipherView;
|
||||
@Input() showView = false;
|
||||
|
||||
cipherType = CipherType;
|
||||
userHasPremiumAccess = false;
|
||||
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private eventCollectionService: EventCollectionService,
|
||||
private totpService: TotpServiceAbstraction,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((canAccessPremium: boolean) => {
|
||||
this.userHasPremiumAccess = canAccessPremium;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.componentIsDestroyed$.next(true);
|
||||
this.componentIsDestroyed$.complete();
|
||||
}
|
||||
|
||||
launchCipher() {
|
||||
this.launchEvent.emit(this.cipher);
|
||||
}
|
||||
|
||||
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
|
||||
if (
|
||||
this.cipher.reprompt !== CipherRepromptType.None &&
|
||||
this.passwordRepromptService.protectedFields().includes(aType) &&
|
||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
|
||||
return;
|
||||
} else if (aType === "TOTP") {
|
||||
value = await this.totpService.getCode(value);
|
||||
}
|
||||
|
||||
if (!cipher.viewPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.copyToClipboard(value, { window: window });
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
|
||||
);
|
||||
|
||||
if (typeI18nKey === "password") {
|
||||
// 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.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
|
||||
} else if (typeI18nKey === "verificationCodeTotp") {
|
||||
// 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.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, cipher.id);
|
||||
} else if (typeI18nKey === "securityCode") {
|
||||
// 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.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
|
||||
}
|
||||
}
|
||||
|
||||
displayTotpCopyButton(cipher: CipherView) {
|
||||
return (
|
||||
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
|
||||
);
|
||||
}
|
||||
|
||||
view() {
|
||||
this.onView.emit(this.cipher);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<div
|
||||
role="group"
|
||||
appA11yTitle="{{ cipher.name }}"
|
||||
class="virtual-scroll-item"
|
||||
[ngClass]="{ 'override-last': !last }"
|
||||
>
|
||||
<div class="box-content-row box-content-row-flex">
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectCipher(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
appStopClick
|
||||
title="{{ title }} - {{ cipher.name }}"
|
||||
class="row-main"
|
||||
>
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
<div class="row-main-content">
|
||||
<span class="text">
|
||||
<span class="truncate-box">
|
||||
<span class="truncate">{{ cipher.name }}</span>
|
||||
<ng-container *ngIf="cipher.organizationId">
|
||||
<i
|
||||
class="bwi bwi-collection text-muted"
|
||||
title="{{ 'shared' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "shared" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="cipher.hasAttachments">
|
||||
<i
|
||||
class="bwi bwi-paperclip text-muted"
|
||||
title="{{ 'attachments' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "attachments" | i18n }}</span>
|
||||
</ng-container>
|
||||
</span>
|
||||
</span>
|
||||
<span class="detail">{{ cipher.subTitle }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<app-action-buttons
|
||||
[cipher]="cipher"
|
||||
[showView]="showView"
|
||||
(onView)="viewCipher(cipher)"
|
||||
(launchEvent)="launchCipher(cipher)"
|
||||
class="action-buttons"
|
||||
>
|
||||
</app-action-buttons>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,31 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-cipher-row",
|
||||
templateUrl: "cipher-row.component.html",
|
||||
})
|
||||
export class CipherRowComponent {
|
||||
@Output() onSelected = new EventEmitter<CipherView>();
|
||||
@Output() launchEvent = new EventEmitter<CipherView>();
|
||||
@Output() onView = new EventEmitter<CipherView>();
|
||||
@Input() cipher: CipherView;
|
||||
@Input() last: boolean;
|
||||
@Input() showView = false;
|
||||
@Input() title: string;
|
||||
|
||||
selectCipher(c: CipherView) {
|
||||
this.onSelected.emit(c);
|
||||
}
|
||||
|
||||
launchCipher(c: CipherView) {
|
||||
this.launchEvent.emit(c);
|
||||
}
|
||||
|
||||
viewCipher(c: CipherView) {
|
||||
this.onView.emit(c);
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,14 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
|
||||
import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
|
||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
||||
|
||||
import {
|
||||
NewItemDropdownV2Component,
|
||||
NewItemInitialValues,
|
||||
} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import { VaultHeaderV2Component } from "../vault-v2/vault-header/vault-header-v2.component";
|
||||
} from "./new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
|
||||
|
||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
|
||||
|
||||
enum VaultState {
|
||||
Empty,
|
||||
@@ -1,140 +0,0 @@
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<!-- Current custom fields -->
|
||||
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
|
||||
<div
|
||||
role="group"
|
||||
class="box-content-row box-content-row-multi box-draggable-row"
|
||||
appBoxRow
|
||||
cdkDrag
|
||||
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
|
||||
[ngClass]="{ 'box-content-row-checkbox': f.type === fieldType.Boolean }"
|
||||
attr.aria-label="{{ f.name }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="removeField(f)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<label for="fieldName{{ i }}" class="sr-only">{{ "name" | i18n }}</label>
|
||||
<label for="fieldValue{{ i }}" class="sr-only">{{ "value" | i18n }}</label>
|
||||
<div class="row-main">
|
||||
<input
|
||||
id="fieldName{{ i }}"
|
||||
type="text"
|
||||
name="Field.Name{{ i }}"
|
||||
[(ngModel)]="f.name"
|
||||
class="row-label"
|
||||
placeholder="{{ 'name' | i18n }}"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Text -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
type="text"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.value"
|
||||
*ngIf="f.type === fieldType.Text"
|
||||
placeholder="{{ 'value' | i18n }}"
|
||||
appInputVerbatim
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Hidden -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
type="{{ f.showValue ? 'text' : 'password' }}"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.value"
|
||||
class="monospaced"
|
||||
appInputVerbatim
|
||||
*ngIf="f.type === fieldType.Hidden"
|
||||
placeholder="{{ 'value' | i18n }}"
|
||||
[disabled]="!cipher.viewPassword && !f.newField"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<!-- Linked -->
|
||||
<select
|
||||
id="fieldValue{{ i }}"
|
||||
name="Field.Value{{ i }}"
|
||||
[(ngModel)]="f.linkedId"
|
||||
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
>
|
||||
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Boolean -->
|
||||
<input
|
||||
id="fieldValue{{ i }}"
|
||||
name="Field.Value{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="f.value"
|
||||
*ngIf="f.type === fieldType.Boolean"
|
||||
appTrueFalseValue
|
||||
trueValue="true"
|
||||
falseValue="false"
|
||||
attr.aria-describedby="fieldName{{ i }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
<div
|
||||
class="action-buttons"
|
||||
*ngIf="f.type === fieldType.Hidden && (cipher.viewPassword || f.newField)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleFieldValue(f)"
|
||||
[attr.aria-pressed]="f.showValue"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !f.showValue, 'bwi-eye-slash': f.showValue }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="drag-handle"
|
||||
appA11yTitle="{{ 'dragToSort' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
cdkDragHandle
|
||||
>
|
||||
<i class="bwi bwi-hamburger" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add new custom field -->
|
||||
<div
|
||||
class="box-content-row box-content-row-newmulti"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
appBoxRow
|
||||
>
|
||||
<button type="button" appStopClick (click)="addField()">
|
||||
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
{{ "newCustomField" | i18n }}
|
||||
</button>
|
||||
<label for="addFieldType" class="sr-only">{{ "type" | i18n }}</label>
|
||||
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
|
||||
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
<option
|
||||
*ngIf="cipher.linkedFieldOptions != null"
|
||||
[ngValue]="addFieldLinkedTypeOption.value"
|
||||
>
|
||||
{{ addFieldLinkedTypeOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/vault/components/add-edit-custom-fields.component";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit-custom-fields",
|
||||
templateUrl: "add-edit-custom-fields.component.html",
|
||||
})
|
||||
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
|
||||
constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) {
|
||||
super(i18nService, eventCollectionService);
|
||||
}
|
||||
}
|
||||
@@ -1,826 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ title }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" *ngIf="cipher">
|
||||
<app-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
|
||||
{{ "personalOwnershipPolicyInEffect" | i18n }}
|
||||
</app-callout>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "itemInformation" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
|
||||
<label for="type">{{ "type" | i18n }}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type">
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
name="Name"
|
||||
[(ngModel)]="cipher.name"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginUsername">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="loginUsername"
|
||||
type="text"
|
||||
name="Login.Username"
|
||||
[(ngModel)]="cipher.login.username"
|
||||
inputmode="email"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generateUsername' | i18n }}"
|
||||
(click)="generateUsername()"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginPassword">{{ "password" | i18n }}</label>
|
||||
<input
|
||||
id="loginPassword"
|
||||
class="monospaced"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="Login.Password"
|
||||
[(ngModel)]="cipher.login.password"
|
||||
appInputVerbatim
|
||||
[disabled]="!cipher.viewPassword"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
#checkPasswordBtn
|
||||
class="row-btn btn"
|
||||
appA11yTitle="{{ 'checkPassword' | i18n }}"
|
||||
(click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise"
|
||||
[disabled]="$any(checkPasswordBtn).loading"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg bwi-check-circle"
|
||||
[hidden]="$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg bwi-spinner bwi-spin"
|
||||
[hidden]="!$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
*ngIf="cipher.viewPassword && cipher.login.password"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'generatePassword' | i18n }}"
|
||||
(click)="generatePassword()"
|
||||
*ngIf="cipher.viewPassword && !(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Passkey-->
|
||||
<div
|
||||
class="box"
|
||||
*ngIf="cipher.login.hasFido2Credentials && !cloneMode"
|
||||
tabindex="0"
|
||||
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-multi text-muted" appBoxRow>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="removePasskey()"
|
||||
appA11yTitle="{{ 'removePasskey' | i18n }}"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{ "typePasskey" | i18n }}</span>
|
||||
{{ "dateCreated" | i18n }}
|
||||
{{ cipher.login.fido2Credentials[0].creationDate | date: "short" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
|
||||
<input
|
||||
id="loginTotp"
|
||||
type="{{ showTotpSeed ? 'text' : 'password' }}"
|
||||
name="Login.Totp"
|
||||
class="monospaced"
|
||||
[(ngModel)]="cipher.login.totp"
|
||||
appInputVerbatim
|
||||
[disabled]="!cipher.viewPassword"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleTotpSeed()"
|
||||
*ngIf="cipher.viewPassword && cipher.login.totp"
|
||||
[attr.aria-pressed]="showTotpSeed"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showTotpSeed, 'bwi-eye-slash': showTotpSeed }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyTOTP' | i18n }}"
|
||||
(click)="copy(cipher.login.totp, 'totp', 'TOTP')"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'totpCapture' | i18n }}"
|
||||
(click)="captureTOTPFromTab()"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-camera" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.type === cipherType.Card">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
|
||||
<input
|
||||
id="cardCardholderName"
|
||||
type="text"
|
||||
name="Card.CardCardholderName"
|
||||
[(ngModel)]="cipher.card.cardholderName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="cardNumber">{{ "number" | i18n }}</label>
|
||||
<input
|
||||
id="cardNumber"
|
||||
class="monospaced"
|
||||
type="{{ showCardNumber ? 'text' : 'password' }}"
|
||||
name="Card.Number"
|
||||
(input)="onCardNumberChange()"
|
||||
[(ngModel)]="cipher.card.number"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardNumber()"
|
||||
[attr.aria-pressed]="showCardNumber"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardBrand">{{ "brand" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardBrand">
|
||||
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
|
||||
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardBrand>
|
||||
<input
|
||||
id="cardBrand"
|
||||
name="Card.Brand"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.card.brand"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardExpMonth">
|
||||
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyCardExpMonth>
|
||||
<input
|
||||
id="cardExpMonth"
|
||||
name="Card.ExpMonth"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="getCardExpMonthDisplay()"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
|
||||
<input
|
||||
id="cardExpYear"
|
||||
type="text"
|
||||
name="Card.ExpYear"
|
||||
[(ngModel)]="cipher.card.expYear"
|
||||
placeholder="{{ 'ex' | i18n }} {{ currentDate | date: 'yyyy' }}"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="cardCode">{{ "securityCode" | i18n }}</label>
|
||||
<input
|
||||
id="cardCode"
|
||||
class="monospaced"
|
||||
type="{{ showCardCode ? 'text' : 'password' }}"
|
||||
name="Card.Code"
|
||||
[(ngModel)]="cipher.card.code"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardCode()"
|
||||
[attr.aria-pressed]="showCardCode"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-fw bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.type === cipherType.Identity">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idTitle">{{ "title" | i18n }}</label>
|
||||
<span *ngIf="!(!cipher.edit && editMode); else readonlyIdTitle">
|
||||
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
|
||||
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<ng-template #readonlyIdTitle>
|
||||
<input
|
||||
id="idTitle"
|
||||
name="Identity.Title"
|
||||
type="text"
|
||||
[readonly]="true"
|
||||
[value]="cipher.identity.title"
|
||||
/>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idFirstName">{{ "firstName" | i18n }}</label>
|
||||
<input
|
||||
id="idFirstName"
|
||||
type="text"
|
||||
name="Identity.FirstName"
|
||||
[(ngModel)]="cipher.identity.firstName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
|
||||
<input
|
||||
id="idMiddleName"
|
||||
type="text"
|
||||
name="Identity.MiddleName"
|
||||
[(ngModel)]="cipher.identity.middleName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLastName">{{ "lastName" | i18n }}</label>
|
||||
<input
|
||||
id="idLastName"
|
||||
type="text"
|
||||
name="Identity.LastName"
|
||||
[(ngModel)]="cipher.identity.lastName"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idUsername">{{ "username" | i18n }}</label>
|
||||
<input
|
||||
id="idUsername"
|
||||
type="text"
|
||||
name="Identity.Username"
|
||||
[(ngModel)]="cipher.identity.username"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCompany">{{ "company" | i18n }}</label>
|
||||
<input
|
||||
id="idCompany"
|
||||
type="text"
|
||||
name="Identity.Company"
|
||||
[(ngModel)]="cipher.identity.company"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idSsn">{{ "ssn" | i18n }}</label>
|
||||
<input
|
||||
id="idSsn"
|
||||
type="text"
|
||||
name="Identity.SSN"
|
||||
[(ngModel)]="cipher.identity.ssn"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idPassportNumber"
|
||||
type="text"
|
||||
name="Identity.PassportNumber"
|
||||
[(ngModel)]="cipher.identity.passportNumber"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
|
||||
<input
|
||||
id="idLicenseNumber"
|
||||
type="text"
|
||||
name="Identity.LicenseNumber"
|
||||
[(ngModel)]="cipher.identity.licenseNumber"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idEmail">{{ "email" | i18n }}</label>
|
||||
<input
|
||||
id="idEmail"
|
||||
type="text"
|
||||
name="Identity.Email"
|
||||
[(ngModel)]="cipher.identity.email"
|
||||
appInputVerbatim
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPhone">{{ "phone" | i18n }}</label>
|
||||
<input
|
||||
id="idPhone"
|
||||
type="text"
|
||||
name="Identity.Phone"
|
||||
[(ngModel)]="cipher.identity.phone"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress1">{{ "address1" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress1"
|
||||
type="text"
|
||||
name="Identity.Address1"
|
||||
[(ngModel)]="cipher.identity.address1"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress2">{{ "address2" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress2"
|
||||
type="text"
|
||||
name="Identity.Address2"
|
||||
[(ngModel)]="cipher.identity.address2"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idAddress3">{{ "address3" | i18n }}</label>
|
||||
<input
|
||||
id="idAddress3"
|
||||
type="text"
|
||||
name="Identity.Address3"
|
||||
[(ngModel)]="cipher.identity.address3"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCity">{{ "cityTown" | i18n }}</label>
|
||||
<input
|
||||
id="idCity"
|
||||
type="text"
|
||||
name="Identity.City"
|
||||
[(ngModel)]="cipher.identity.city"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idState">{{ "stateProvince" | i18n }}</label>
|
||||
<input
|
||||
id="idState"
|
||||
type="text"
|
||||
name="Identity.State"
|
||||
[(ngModel)]="cipher.identity.state"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
|
||||
<input
|
||||
id="idPostalCode"
|
||||
type="text"
|
||||
name="Identity.PostalCode"
|
||||
[(ngModel)]="cipher.identity.postalCode"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="idCountry">{{ "country" | i18n }}</label>
|
||||
<input
|
||||
id="idCountry"
|
||||
type="text"
|
||||
name="Identity.Country"
|
||||
[(ngModel)]="cipher.identity.country"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SshKey -->
|
||||
<div *ngIf="cipher.sshKey">
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
|
||||
<span class="row-label"> {{ "sshPrivateKey" | i18n }}</span>
|
||||
{{ cipher.sshKey.privateKey }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
|
||||
<span class="row-label"> {{ "sshPublicKey" | i18n }}</span>
|
||||
{{ cipher.sshKey.publicKey }}
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row"
|
||||
*ngIf="cipher.sshKey.keyFingerprint"
|
||||
style="overflow: hidden"
|
||||
>
|
||||
<span class="row-label"> {{ "sshKeyFingerprint" | i18n }}</span>
|
||||
{{ cipher.sshKey.keyFingerprint }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.type === cipherType.Login">
|
||||
<div class="box-content">
|
||||
<ng-container *ngIf="cipher.login.hasUris">
|
||||
<div
|
||||
role="group"
|
||||
class="box-content-row box-content-row-multi"
|
||||
appBoxRow
|
||||
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
|
||||
attr.aria-label="{{ 'uriPosition' | i18n: i + 1 }}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
appStopClick
|
||||
(click)="removeUri(u)"
|
||||
appA11yTitle="{{ 'remove' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="row-main">
|
||||
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
|
||||
<input
|
||||
id="loginUri{{ i }}"
|
||||
type="text"
|
||||
name="Login.Uris[{{ i }}].Uri"
|
||||
[(ngModel)]="u.uri"
|
||||
[hidden]="$any(u).showUriOptionsInput === true"
|
||||
placeholder="{{ 'ex' | i18n }} https://google.com"
|
||||
inputmode="url"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
appInputVerbatim
|
||||
/>
|
||||
<label for="loginUriMatch{{ i }}" class="sr-only">
|
||||
{{ "currentUri" | i18n }} {{ i + 1 }}
|
||||
</label>
|
||||
<select
|
||||
*ngIf="currentUris && currentUris.length"
|
||||
id="currentUris{{ i }}"
|
||||
name="Login.Uris[{{ i }}].CurrentUris"
|
||||
[(ngModel)]="u.uri"
|
||||
[hidden]="!$any(u).showCurrentUris"
|
||||
>
|
||||
<option [ngValue]="null">-- {{ "select" | i18n }} --</option>
|
||||
<option *ngFor="let u of currentUris" [ngValue]="u">{{ u }}</option>
|
||||
</select>
|
||||
<label for="loginUriMatch{{ i }}" class="sr-only">
|
||||
{{ "matchDetection" | i18n }} {{ i + 1 }}
|
||||
</label>
|
||||
<select
|
||||
id="loginUriMatch{{ i }}"
|
||||
name="Login.Uris[{{ i }}].Match"
|
||||
[(ngModel)]="u.match"
|
||||
[hidden]="
|
||||
$any(u).showOptions === false || ($any(u).showOptions == null && u.match == null)
|
||||
"
|
||||
(change)="loginUriMatchChanged(u)"
|
||||
>
|
||||
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="currentUris && currentUris.length"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleCurrentUris' | i18n }}"
|
||||
(click)="toggleUriInput(u)"
|
||||
[attr.aria-pressed]="$any(u).showCurrentUris === true"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-fw bwi-lg bwi-list"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleOptions' | i18n }}"
|
||||
(click)="toggleUriOptions(u)"
|
||||
[attr.aria-pressed]="$any(u).showOptions === true"
|
||||
[disabled]="!cipher.edit && editMode"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-lg bwi-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="addUri()"
|
||||
class="box-content-row box-content-row-newmulti single-line"
|
||||
*ngIf="!(!cipher.edit && editMode)"
|
||||
>
|
||||
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i> {{ "newUri" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="showAutoFillOnPageLoadOptions">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="autofillOnPageLoad">{{ "itemAutoFillOnPageLoad" | i18n }} </label>
|
||||
<select
|
||||
id="autofillOnPageLoad"
|
||||
name="AutofillOnPageLoad"
|
||||
[disabled]="reprompt"
|
||||
[(ngModel)]="cipher.login.autofillOnPageLoad"
|
||||
>
|
||||
<option *ngFor="let o of autofillOnPageLoadOptions" [ngValue]="o.value">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer !tw-mb-0 !tw-pb-0" *ngIf="reprompt">
|
||||
{{ "turnOffMasterPasswordPromptToEditField" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="folder">{{ "folder" | i18n }}</label>
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
|
||||
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="favorite">{{ "favorite" | i18n }}</label>
|
||||
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite" />
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="canUseReprompt">
|
||||
<label for="passwordPrompt">
|
||||
{{ "passwordPrompt" | i18n }}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
href="https://bitwarden.com/help/managing-items/#protect-individual-items"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</label>
|
||||
<input
|
||||
id="passwordPrompt"
|
||||
type="checkbox"
|
||||
name="PasswordPrompt"
|
||||
[ngModel]="reprompt"
|
||||
(change)="repromptChanged()"
|
||||
[disabled]="!cipher.edit && editMode"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default single-line"
|
||||
appStopClick
|
||||
(click)="attachments()"
|
||||
*ngIf="editMode && showAttachments && !cloneMode"
|
||||
>
|
||||
<div class="row-main">{{ "attachments" | i18n }}</div>
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg bwi-fw"
|
||||
aria-hidden="true"
|
||||
*ngIf="openAttachmentsInPopup"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-angle-right row-sub-icon"
|
||||
aria-hidden="true"
|
||||
*ngIf="!openAttachmentsInPopup"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="editCollections()"
|
||||
*ngIf="editMode && cipher.organizationId && !cloneMode"
|
||||
>
|
||||
<div class="row-main">{{ "collections" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
<label for="notes">{{ "notes" | i18n }}</label>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<textarea
|
||||
id="notes"
|
||||
name="Notes"
|
||||
rows="6"
|
||||
[(ngModel)]="cipher.notes"
|
||||
[readonly]="!cipher.edit && editMode"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-vault-add-edit-custom-fields
|
||||
*ngIf="!(!cipher.hasFields && !cipher.edit && editMode)"
|
||||
[cipher]="cipher"
|
||||
[thisCipherType]="cipher.type"
|
||||
[editMode]="editMode"
|
||||
>
|
||||
</app-vault-add-edit-custom-fields>
|
||||
<div class="box" *ngIf="allowOwnershipOptions()">
|
||||
<h2 class="box-header">
|
||||
{{ "ownership" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
|
||||
<select
|
||||
id="organizationId"
|
||||
class="form-control"
|
||||
name="OrganizationId"
|
||||
[(ngModel)]="cipher.organizationId"
|
||||
(change)="organizationChanged()"
|
||||
>
|
||||
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="(!editMode || cloneMode) && cipher.organizationId">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="delete()"
|
||||
[appApiAction]="deletePromise"
|
||||
#deleteBtn
|
||||
>
|
||||
<div class="row-main text-danger">
|
||||
<div class="icon text-danger" aria-hidden="true">
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
></i>
|
||||
</div>
|
||||
<span>{{ "deleteItem" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
||||
@@ -1,417 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe, Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import qrcodeParser from "qrcode-parser";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
|
||||
import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service";
|
||||
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
||||
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit",
|
||||
templateUrl: "add-edit.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
|
||||
currentUris: string[];
|
||||
showAttachments = true;
|
||||
openAttachmentsInPopup: boolean;
|
||||
showAutoFillOnPageLoadOptions: boolean;
|
||||
|
||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
accountService: AccountService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
collectionService: CollectionService,
|
||||
messagingService: MessagingService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
eventCollectionService: EventCollectionService,
|
||||
policyService: PolicyService,
|
||||
private popupCloseWarningService: PopupCloseWarningService,
|
||||
organizationService: OrganizationService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
logService: LogService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigService,
|
||||
private fido2UserVerificationService: Fido2UserVerificationService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
accountService,
|
||||
collectionService,
|
||||
messagingService,
|
||||
eventCollectionService,
|
||||
policyService,
|
||||
logService,
|
||||
passwordRepromptService,
|
||||
organizationService,
|
||||
dialogService,
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
if (params.folderId) {
|
||||
this.folderId = params.folderId;
|
||||
}
|
||||
if (params.collectionId) {
|
||||
this.collectionId = params.collectionId;
|
||||
const collection = this.writeableCollections.find((c) => c.id === params.collectionId);
|
||||
if (collection != null) {
|
||||
this.collectionIds = [collection.id];
|
||||
this.organizationId = collection.organizationId;
|
||||
}
|
||||
}
|
||||
if (params.type) {
|
||||
const type = parseInt(params.type, null);
|
||||
this.type = type;
|
||||
}
|
||||
this.editMode = !params.cipherId;
|
||||
|
||||
if (params.cloneMode != null) {
|
||||
this.cloneMode = params.cloneMode === "true";
|
||||
}
|
||||
if (params.selectedVault) {
|
||||
this.organizationId = params.selectedVault;
|
||||
}
|
||||
|
||||
await this.load();
|
||||
|
||||
if (!this.editMode || this.cloneMode) {
|
||||
// Only allow setting username if there's no existing value
|
||||
if (
|
||||
params.username &&
|
||||
(this.cipher.login.username == null || this.cipher.login.username === "")
|
||||
) {
|
||||
this.cipher.login.username = params.username;
|
||||
}
|
||||
|
||||
if (params.name && (this.cipher.name == null || this.cipher.name === "")) {
|
||||
this.cipher.name = params.name;
|
||||
}
|
||||
if (
|
||||
params.uri &&
|
||||
this.cipher.login.uris[0] &&
|
||||
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
||||
) {
|
||||
this.cipher.login.uris[0].uri = params.uri;
|
||||
}
|
||||
}
|
||||
|
||||
this.openAttachmentsInPopup = BrowserPopupUtils.inPopup(window);
|
||||
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
BrowserApi.messageListener("add-edit-popout", this.handleExtensionMessage.bind(this));
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.editMode) {
|
||||
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
|
||||
this.currentUris =
|
||||
tabs == null
|
||||
? null
|
||||
: tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url);
|
||||
}
|
||||
|
||||
this.setFocus();
|
||||
|
||||
if (BrowserPopupUtils.inPopout(window)) {
|
||||
this.popupCloseWarningService.enable();
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
await super.load();
|
||||
this.showAutoFillOnPageLoadOptions =
|
||||
this.cipher.type === CipherType.Login &&
|
||||
(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$));
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
|
||||
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
|
||||
|
||||
// normalize card expiry year on save
|
||||
if (this.cipher.type === this.cipherType.Card) {
|
||||
this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
|
||||
}
|
||||
|
||||
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
|
||||
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
|
||||
if (
|
||||
inFido2PopoutWindow &&
|
||||
!(await this.handleFido2UserVerification(sessionId, userVerification))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = await super.submit();
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BrowserPopupUtils.inPopout(window)) {
|
||||
this.popupCloseWarningService.disable();
|
||||
}
|
||||
|
||||
if (inFido2PopoutWindow) {
|
||||
BrowserFido2UserInterfaceSession.confirmNewCredentialResponse(
|
||||
sessionId,
|
||||
this.cipher.id,
|
||||
userVerification,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
this.messagingService.send("addEditCipherSubmitted");
|
||||
await closeAddEditVaultItemPopout(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.cloneMode) {
|
||||
// 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(["/tabs/vault"]);
|
||||
} else {
|
||||
this.location.back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
attachments() {
|
||||
super.attachments();
|
||||
|
||||
if (this.openAttachmentsInPopup) {
|
||||
const destinationUrl = this.router
|
||||
.createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } })
|
||||
.toString();
|
||||
const currentBaseUrl = window.location.href.replace(this.router.url, "");
|
||||
// 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
|
||||
BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl);
|
||||
} else {
|
||||
// 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(["/attachments"], { queryParams: { cipherId: this.cipher.id } });
|
||||
}
|
||||
}
|
||||
|
||||
editCollections() {
|
||||
super.editCollections();
|
||||
if (this.cipher.organizationId != null) {
|
||||
// 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(["/collections"], { queryParams: { cipherId: this.cipher.id } });
|
||||
}
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
super.cancel();
|
||||
|
||||
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
if (BrowserPopupUtils.inPopout(window) && sessionData.isFido2Session) {
|
||||
this.popupCloseWarningService.disable();
|
||||
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.inAddEditPopoutWindow()) {
|
||||
// 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
|
||||
closeAddEditVaultItemPopout();
|
||||
return;
|
||||
}
|
||||
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
async generateUsername(): Promise<boolean> {
|
||||
const confirmed = await super.generateUsername();
|
||||
if (confirmed) {
|
||||
await this.saveCipherState();
|
||||
// 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(["generator"], { queryParams: { type: "username" } });
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
async generatePassword(): Promise<boolean> {
|
||||
const confirmed = await super.generatePassword();
|
||||
if (confirmed) {
|
||||
await this.saveCipherState();
|
||||
// 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(["generator"], { queryParams: { type: "password" } });
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
async delete(): Promise<boolean> {
|
||||
const confirmed = await super.delete();
|
||||
if (confirmed) {
|
||||
// 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(["/tabs/vault"]);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
toggleUriInput(uri: LoginUriView) {
|
||||
const u = uri as any;
|
||||
u.showCurrentUris = !u.showCurrentUris;
|
||||
}
|
||||
|
||||
allowOwnershipOptions(): boolean {
|
||||
return (
|
||||
(!this.editMode || this.cloneMode) &&
|
||||
this.ownershipOptions &&
|
||||
(this.ownershipOptions.length > 1 || !this.allowPersonal)
|
||||
);
|
||||
}
|
||||
|
||||
private saveCipherState() {
|
||||
return this.cipherService.setAddEditCipherInfo({
|
||||
cipher: this.cipher,
|
||||
collectionIds:
|
||||
this.collections == null
|
||||
? []
|
||||
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
|
||||
});
|
||||
}
|
||||
|
||||
private setFocus() {
|
||||
window.setTimeout(() => {
|
||||
if (this.editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.cipher.name != null && this.cipher.name !== "") {
|
||||
document.getElementById("loginUsername").focus();
|
||||
} else {
|
||||
document.getElementById("name").focus();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
repromptChanged() {
|
||||
super.repromptChanged();
|
||||
|
||||
if (!this.showAutoFillOnPageLoadOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.reprompt) {
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("passwordRepromptDisabledAutofillOnPageLoad"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("autofillOnPageLoadSetToDefault"),
|
||||
);
|
||||
}
|
||||
|
||||
private inAddEditPopoutWindow() {
|
||||
return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem);
|
||||
}
|
||||
|
||||
async captureTOTPFromTab() {
|
||||
try {
|
||||
const screenshot = await BrowserApi.captureVisibleTab();
|
||||
const data = await qrcodeParser(screenshot);
|
||||
const url = new URL(data.toString());
|
||||
if (url.protocol == "otpauth:" && url.searchParams.has("secret")) {
|
||||
this.cipher.login.totp = data.toString();
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("totpCaptureSuccess"),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("totpCaptureError"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleExtensionMessage(message: { [key: string]: any; command: string }) {
|
||||
if (message.command === "inlineAutofillMenuRefreshAddEditCipher") {
|
||||
this.load().catch((error) => this.logService.error(error));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
|
||||
// Be sure to make the same changes to add-edit-v2.component.ts if applicable
|
||||
private async handleFido2UserVerification(
|
||||
sessionId: string,
|
||||
userVerification: boolean,
|
||||
): Promise<boolean> {
|
||||
// We are bypassing user verification pending approval for production.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="close()" *ngIf="openedAttachmentsInPopup">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
<button type="button" (click)="back()" *ngIf="!openedAttachmentsInPopup">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "attachments" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box" *ngIf="cipher && cipher.hasAttachments">
|
||||
<div class="box-content no-hover single-line">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let a of cipher.attachments">
|
||||
<div class="row-main">
|
||||
{{ a.fileName }}
|
||||
</div>
|
||||
<small class="row-sub-label">{{ a.sizeName }}</small>
|
||||
<div class="action-buttons no-pad">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn btn"
|
||||
type="button"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'deleteAttachment' | i18n }}"
|
||||
(click)="delete(a)"
|
||||
#deleteBtn
|
||||
[appApiAction]="deletePromises[a.id]"
|
||||
[disabled]="$any(deleteBtn).loading"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
||||
[hidden]="$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "newAttachment" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content no-hover">
|
||||
<div class="box-content-row">
|
||||
<label for="file">{{ "file" | i18n }}</label>
|
||||
<input type="file" id="file" name="file" aria-describedby="fileHelp" required />
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileHelp" class="box-footer">
|
||||
{{ "maxFileSize" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
||||
@@ -1,82 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-attachments",
|
||||
templateUrl: "attachments.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit {
|
||||
openedAttachmentsInPopup: boolean;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
private location: Location,
|
||||
private route: ActivatedRoute,
|
||||
stateService: StateService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.cipherId = params.cipherId;
|
||||
await this.init();
|
||||
});
|
||||
|
||||
this.openedAttachmentsInPopup = history.length === 1;
|
||||
}
|
||||
|
||||
back() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
close() {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="back()">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "collections" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box">
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="$any(c).checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-collections",
|
||||
templateUrl: "collections.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class CollectionsComponent extends BaseCollectionsComponent implements OnInit {
|
||||
constructor(
|
||||
collectionService: CollectionService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
cipherService: CipherService,
|
||||
organizationService: OrganizationService,
|
||||
private route: ActivatedRoute,
|
||||
private location: Location,
|
||||
logService: LogService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
collectionService,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
organizationService,
|
||||
logService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.onSavedCollections.subscribe(() => {
|
||||
this.back();
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.cipherId = params.cipherId;
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
back() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
<app-header>
|
||||
<h1 class="sr-only">{{ "currentTab" | i18n }}</h1>
|
||||
<div class="left">
|
||||
<app-pop-out *ngIf="!inSidebar"></app-pop-out>
|
||||
<button
|
||||
type="button"
|
||||
(click)="refresh()"
|
||||
appA11yTitle="{{ 'refresh' | i18n }}"
|
||||
*ngIf="inSidebar"
|
||||
>
|
||||
<i class="bwi bwi-refresh-tab bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="search center">
|
||||
<input
|
||||
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||
placeholder="{{ 'searchVault' | i18n }}"
|
||||
id="search"
|
||||
[(ngModel)]="searchText"
|
||||
(input)="search$.next()"
|
||||
autocomplete="off"
|
||||
(keydown)="closeOnEsc($event)"
|
||||
appAutofocus
|
||||
/>
|
||||
<i class="bwi bwi-search" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1">
|
||||
<div class="no-items" *ngIf="!loaded">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||
</div>
|
||||
<ng-container *ngIf="loaded">
|
||||
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
|
||||
<div class="box list" *ngIf="loginCiphers">
|
||||
<h2 class="box-header">
|
||||
{{ "typeLogins" | i18n }}
|
||||
<span class="flex-right">{{ loginCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let loginCipher of loginCiphers"
|
||||
[cipher]="loginCipher"
|
||||
title="{{ 'autoFill' | i18n }}"
|
||||
[showView]="true"
|
||||
(onSelected)="fillCipher($event)"
|
||||
(onView)="viewCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
<div class="box-content-row padded no-hover" *ngIf="!loginCiphers.length">
|
||||
<p class="text-center">{{ "autoFillInfo" | i18n }}</p>
|
||||
<button type="button" class="btn primary link block" (click)="addCipher()">
|
||||
{{ "addLogin" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="cardCiphers && cardCiphers.length">
|
||||
<h2 class="box-header">
|
||||
{{ "cards" | i18n }}
|
||||
<span class="flex-right">{{ cardCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let cardCipher of cardCiphers"
|
||||
[cipher]="cardCipher"
|
||||
title="{{ 'autoFill' | i18n }}"
|
||||
[showView]="true"
|
||||
(onSelected)="fillCipher($event)"
|
||||
(onView)="viewCipher($event)"
|
||||
></app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="identityCiphers && identityCiphers.length">
|
||||
<h2 class="box-header">
|
||||
{{ "identities" | i18n }}
|
||||
<span class="flex-right">{{ identityCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let identityCipher of identityCiphers"
|
||||
[cipher]="identityCipher"
|
||||
title="{{ 'autoFill' | i18n }}"
|
||||
[showView]="true"
|
||||
(onSelected)="fillCipher($event)"
|
||||
(onView)="viewCipher($event)"
|
||||
></app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</main>
|
||||
@@ -1,354 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, from, Subscription } from "rxjs";
|
||||
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "CurrentTabComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-current-tab",
|
||||
templateUrl: "current-tab.component.html",
|
||||
})
|
||||
export class CurrentTabComponent implements OnInit, OnDestroy {
|
||||
pageDetails: any[] = [];
|
||||
tab: chrome.tabs.Tab;
|
||||
cardCiphers: CipherView[];
|
||||
identityCiphers: CipherView[];
|
||||
loginCiphers: CipherView[];
|
||||
url: string;
|
||||
hostname: string;
|
||||
searchText: string;
|
||||
inSidebar = false;
|
||||
searchTypeSearch = false;
|
||||
loaded = false;
|
||||
isLoading = false;
|
||||
showOrganizations = false;
|
||||
showHowToAutofill = false;
|
||||
autofillCalloutText: string;
|
||||
protected search$ = new Subject<void>();
|
||||
private destroy$ = new Subject<void>();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private totpCode: string;
|
||||
private totpTimeout: number;
|
||||
private loadedTimeout: number;
|
||||
private searchTimeout: number;
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private cipherService: CipherService,
|
||||
private autofillService: AutofillService,
|
||||
private i18nService: I18nService,
|
||||
private router: Router,
|
||||
private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private syncService: SyncService,
|
||||
private searchService: SearchService,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private passwordRepromptService: PasswordRepromptService,
|
||||
private organizationService: OrganizationService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private vaultSettingsService: VaultSettingsService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.inSidebar = BrowserPopupUtils.inSidebar(window);
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// 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.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (this.isLoading) {
|
||||
window.setTimeout(() => {
|
||||
// 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.load();
|
||||
}, 500);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.syncService.syncInProgress) {
|
||||
await this.load();
|
||||
await this.setCallout();
|
||||
} else {
|
||||
this.loadedTimeout = window.setTimeout(async () => {
|
||||
if (!this.isLoading) {
|
||||
await this.load();
|
||||
await this.setCallout();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
this.search$
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
switchMap(() => {
|
||||
return from(this.searchVault());
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
const autofillOnPageLoadOrgPolicy = await firstValueFrom(
|
||||
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
|
||||
);
|
||||
const autofillOnPageLoadPolicyToastHasDisplayed = await firstValueFrom(
|
||||
this.autofillSettingsService.autofillOnPageLoadPolicyToastHasDisplayed$,
|
||||
);
|
||||
|
||||
// If the org "autofill on page load" policy is set, set the user setting to match it
|
||||
// @TODO override user setting instead of overwriting
|
||||
if (autofillOnPageLoadOrgPolicy === true) {
|
||||
await this.autofillSettingsService.setAutofillOnPageLoad(true);
|
||||
|
||||
if (!autofillOnPageLoadPolicyToastHasDisplayed) {
|
||||
this.platformUtilsService.showToast(
|
||||
"info",
|
||||
null,
|
||||
this.i18nService.t("autofillPageLoadPolicyActivated"),
|
||||
);
|
||||
|
||||
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(true);
|
||||
}
|
||||
}
|
||||
|
||||
// If the org policy is ever disabled after being enabled, reset the toast notification
|
||||
if (!autofillOnPageLoadOrgPolicy && autofillOnPageLoadPolicyToastHasDisplayed) {
|
||||
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(false);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
window.clearTimeout(this.loadedTimeout);
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
// 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(["/add-cipher"], {
|
||||
queryParams: {
|
||||
name: this.hostname,
|
||||
uri: this.url,
|
||||
selectedVault: this.vaultFilterService.getVaultFilter().selectedOrganizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
viewCipher(cipher: CipherView) {
|
||||
// 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 } });
|
||||
}
|
||||
|
||||
async fillCipher(cipher: CipherView, closePopupDelay?: number) {
|
||||
if (
|
||||
cipher.reprompt !== CipherRepromptType.None &&
|
||||
!(await this.passwordRepromptService.showPasswordPrompt())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.totpCode = null;
|
||||
if (this.totpTimeout != null) {
|
||||
window.clearTimeout(this.totpTimeout);
|
||||
}
|
||||
|
||||
if (this.pageDetails == null || this.pageDetails.length === 0) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.totpCode = await this.autofillService.doAutoFill({
|
||||
tab: this.tab,
|
||||
cipher: cipher,
|
||||
pageDetails: this.pageDetails,
|
||||
doc: window.document,
|
||||
fillNewPassword: true,
|
||||
allowTotpAutofill: true,
|
||||
});
|
||||
if (this.totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
|
||||
}
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
if (!closePopupDelay) {
|
||||
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
|
||||
BrowserApi.closePopup(window);
|
||||
} else {
|
||||
// Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard
|
||||
setTimeout(() => BrowserApi.closePopup(window), 50);
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => BrowserApi.closePopup(window), closePopupDelay);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
this.ngZone.run(() => {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async searchVault() {
|
||||
if (!(await this.searchService.isSearchable(this.searchText))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
|
||||
}
|
||||
|
||||
closeOnEsc(e: KeyboardEvent) {
|
||||
// If input not empty, use browser default behavior of clearing input instead
|
||||
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
protected async load() {
|
||||
this.isLoading = false;
|
||||
this.tab = await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
if (this.tab != null) {
|
||||
this.url = this.tab.url;
|
||||
} else {
|
||||
this.loginCiphers = [];
|
||||
this.isLoading = this.loaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.pageDetails = [];
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
|
||||
this.hostname = Utils.getHostname(this.url);
|
||||
const otherTypes: CipherType[] = [];
|
||||
const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$));
|
||||
const dontShowIdentities = !(await firstValueFrom(
|
||||
this.vaultSettingsService.showIdentitiesCurrentTab$,
|
||||
));
|
||||
this.showOrganizations = await this.organizationService.hasOrganizations();
|
||||
if (!dontShowCards) {
|
||||
otherTypes.push(CipherType.Card);
|
||||
}
|
||||
if (!dontShowIdentities) {
|
||||
otherTypes.push(CipherType.Identity);
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
this.url,
|
||||
otherTypes.length > 0 ? otherTypes : null,
|
||||
);
|
||||
|
||||
this.loginCiphers = [];
|
||||
this.cardCiphers = [];
|
||||
this.identityCiphers = [];
|
||||
|
||||
ciphers.forEach((c) => {
|
||||
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
|
||||
switch (c.type) {
|
||||
case CipherType.Login:
|
||||
this.loginCiphers.push(c);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.cardCiphers.push(c);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.identityCiphers.push(c);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.loginCiphers.length) {
|
||||
this.loginCiphers = this.loginCiphers.sort((a, b) =>
|
||||
this.cipherService.sortCiphersByLastUsedThenName(a, b),
|
||||
);
|
||||
}
|
||||
|
||||
this.isLoading = this.loaded = true;
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
// 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(["autofill"]);
|
||||
}
|
||||
|
||||
async dismissCallout() {
|
||||
await this.autofillSettingsService.setAutofillOnPageLoadCalloutIsDismissed(true);
|
||||
this.showHowToAutofill = false;
|
||||
}
|
||||
|
||||
private async setCallout() {
|
||||
const inlineMenuVisibilityIsOff =
|
||||
(await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$)) ===
|
||||
AutofillOverlayVisibility.Off;
|
||||
|
||||
this.showHowToAutofill =
|
||||
this.loginCiphers.length > 0 &&
|
||||
inlineMenuVisibilityIsOff &&
|
||||
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)) &&
|
||||
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadCalloutIsDismissed$));
|
||||
|
||||
if (this.showHowToAutofill) {
|
||||
const autofillCommand = await this.platformUtilsService.getAutofillKeyboardShortcut();
|
||||
await this.setAutofillCalloutText(autofillCommand);
|
||||
}
|
||||
}
|
||||
|
||||
private setAutofillCalloutText(command: string) {
|
||||
if (command) {
|
||||
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithCommand", command);
|
||||
} else {
|
||||
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "passwordHistory" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right"></div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box list full-list" *ngIf="history && history.length">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
||||
<div class="row-main">
|
||||
<div class="row-main-content">
|
||||
<span
|
||||
class="text monospaced no-ellipsis"
|
||||
[innerHTML]="h.password | colorPassword"
|
||||
></span>
|
||||
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(h.password)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-items" *ngIf="!history || !history.length">
|
||||
<p>{{ "noPasswordsInList" | i18n }}</p>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Location } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-password-history",
|
||||
templateUrl: "password-history.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class PasswordHistoryComponent extends BasePasswordHistoryComponent implements OnInit {
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
accountService: AccountService,
|
||||
private location: Location,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
super(cipherService, platformUtilsService, i18nService, accountService, window);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
await this.init();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<ng-container *ngIf="organizations$ | async as organizations">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "moveToOrganization" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="form.loading || !canSave"
|
||||
*ngIf="organizations && organizations.length"
|
||||
>
|
||||
<span [hidden]="form.loading">{{ "move" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-lg bwi-spin"
|
||||
[hidden]="!form.loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box">
|
||||
<div class="box-content" *ngIf="!organizations || !organizations.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noOrganizationsList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="organizations && organizations.length">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="organization">{{ "organization" | i18n }}</label>
|
||||
<select
|
||||
id="organization"
|
||||
name="OrganizationId"
|
||||
aria-describedby="organizationHelp"
|
||||
[(ngModel)]="organizationId"
|
||||
(change)="filterCollections()"
|
||||
>
|
||||
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="organizationHelp" class="box-footer">
|
||||
{{ "moveToOrgDesc" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="organizations && organizations.length">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content" *ngIf="!collections || !collections.length">
|
||||
<div class="box-content-row padded no-hover">
|
||||
{{ "noCollectionsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content" *ngIf="collections && collections.length">
|
||||
<div
|
||||
class="box-content-row box-content-row-checkbox"
|
||||
*ngFor="let c of collections; let i = index"
|
||||
appBoxRow
|
||||
>
|
||||
<label for="collection_{{ i }}">{{ c.name }}</label>
|
||||
<input
|
||||
id="collection_{{ i }}"
|
||||
type="checkbox"
|
||||
[(ngModel)]="c.checked"
|
||||
name="Collection[{{ i }}].Checked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</ng-container>
|
||||
</form>
|
||||
@@ -1,72 +0,0 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-share",
|
||||
templateUrl: "share.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class ShareComponent extends BaseShareComponent implements OnInit {
|
||||
constructor(
|
||||
collectionService: CollectionService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
logService: LogService,
|
||||
cipherService: CipherService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
organizationService: OrganizationService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
collectionService,
|
||||
platformUtilsService,
|
||||
i18nService,
|
||||
cipherService,
|
||||
logService,
|
||||
organizationService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.onSharedCipher.subscribe(() => {
|
||||
// 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", { cipherId: this.cipherId }]);
|
||||
});
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.cipherId = params.cipherId;
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
const success = await super.submit();
|
||||
if (success) {
|
||||
this.cancel();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
// 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"], {
|
||||
replaceUrl: true,
|
||||
queryParams: { cipherId: this.cipher.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
<app-header>
|
||||
<div class="left">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
|
||||
<div class="search center">
|
||||
<input
|
||||
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||
placeholder="{{ 'searchVault' | i18n }}"
|
||||
id="search"
|
||||
[(ngModel)]="searchText"
|
||||
(input)="search(200)"
|
||||
autocomplete="off"
|
||||
appAutofocus
|
||||
(keydown)="closeOnEsc($event)"
|
||||
/>
|
||||
<i class="bwi bwi-search"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1" cdk-scrollable>
|
||||
<app-vault-select
|
||||
(onVaultSelectionChanged)="vaultFilterChanged()"
|
||||
class="select-index-top"
|
||||
></app-vault-select>
|
||||
<div class="no-items" *ngIf="(!ciphers || !ciphers.length) && !showSearching()">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded"></i>
|
||||
<ng-container *ngIf="loaded">
|
||||
<img class="no-items-image" aria-hidden="true" />
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
<button type="button" (click)="addCipher()" class="btn block primary link">
|
||||
{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="ciphers && ciphers.length && !showSearching()">
|
||||
<div class="box list" *ngIf="favoriteCiphers">
|
||||
<h2 class="box-header">
|
||||
{{ "favorites" | i18n }}
|
||||
<span class="flex-right">{{ favoriteCiphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let favoriteCipher of favoriteCiphers"
|
||||
[cipher]="favoriteCipher"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<h2 class="box-header">
|
||||
{{ "types" | i18n }}
|
||||
<span class="flex-right">4</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.Login)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-globe"></i></div>
|
||||
<span class="text">{{ "typeLogin" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">
|
||||
{{ typeCounts.get(cipherType.Login) || 0 }}
|
||||
</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.Card)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-credit-card"></i></div>
|
||||
<span class="text">{{ "typeCard" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.Card) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.Identity)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-id-card"></i></div>
|
||||
<span class="text">{{ "typeIdentity" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.Identity) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectType(cipherType.SecureNote)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-sticky-note"></i></div>
|
||||
<span class="text">{{ "typeSecureNote" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.SecureNote) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
*ngIf="isSshKeysEnabled"
|
||||
(click)="selectType(cipherType.SshKey)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-key"></i></div>
|
||||
<span class="text">{{ "typeSshKey" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ typeCounts.get(cipherType.SshKey) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="nestedFolders?.length">
|
||||
<h2 class="box-header">
|
||||
{{ "folders" | i18n }}
|
||||
<span class="flex-right">{{ folderCount }}</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let f of nestedFolders"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectFolder(f.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-folder"></i>
|
||||
</div>
|
||||
<span class="text">{{ f.node.name }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ folderCounts.get(f.node.id) || 0 }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="showCollections && nestedCollections && nestedCollections.length">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
<span class="flex-right">{{ nestedCollections.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let nestedCollection of nestedCollections"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectCollection(nestedCollection.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-collection"></i></div>
|
||||
<span class="text">{{ nestedCollection.node.name }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{
|
||||
collectionCounts.get(nestedCollection.node.id) || 0
|
||||
}}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="showNoFolderCiphers">
|
||||
<h2 class="box-header">
|
||||
{{ "noneFolder" | i18n }}
|
||||
<div class="flex-right">{{ noFolderCiphers.length }}</div>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*ngFor="let noFolderCipher of noFolderCiphers"
|
||||
[cipher]="noFolderCipher"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="deletedCount">
|
||||
<h2 class="box-header">
|
||||
{{ "trash" | i18n }}
|
||||
<span class="flex-right">{{ deletedCount }}</span>
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button type="button" class="box-content-row" appStopClick (click)="selectTrash()">
|
||||
<div class="row-main">
|
||||
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-trash"></i></div>
|
||||
<span class="text">{{ "trash" | i18n }}</span>
|
||||
</div>
|
||||
<span class="row-sub-label">{{ deletedCount }}</span>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showSearching()">
|
||||
<div class="no-items" *ngIf="!ciphers || !ciphers.length">
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
</div>
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="55"
|
||||
minBufferPx="400"
|
||||
maxBufferPx="600"
|
||||
*ngIf="ciphers && ciphers.length > 0"
|
||||
>
|
||||
<div class="box list full-list">
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*cdkVirtualFor="let searchedCipher of ciphers"
|
||||
[cipher]="searchedCipher"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
>
|
||||
</app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</main>
|
||||
@@ -1,482 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
|
||||
import { first, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
})
|
||||
export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
get showNoFolderCiphers(): boolean {
|
||||
return (
|
||||
this.noFolderCiphers != null &&
|
||||
this.noFolderCiphers.length < this.noFolderListSize &&
|
||||
this.collections.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
get folderCount(): number {
|
||||
return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1);
|
||||
}
|
||||
folders: FolderView[];
|
||||
nestedFolders: TreeNode<FolderView>[];
|
||||
collections: CollectionView[];
|
||||
nestedCollections: TreeNode<CollectionView>[];
|
||||
loaded = false;
|
||||
cipherType = CipherType;
|
||||
ciphers: CipherView[];
|
||||
favoriteCiphers: CipherView[];
|
||||
noFolderCiphers: CipherView[];
|
||||
folderCounts = new Map<string, number>();
|
||||
collectionCounts = new Map<string, number>();
|
||||
typeCounts = new Map<CipherType, number>();
|
||||
state: BrowserGroupingsComponentState;
|
||||
showLeftHeader = true;
|
||||
searchPending = false;
|
||||
searchTypeSearch = false;
|
||||
deletedCount = 0;
|
||||
vaultFilter: VaultFilter;
|
||||
selectedOrganization: string = null;
|
||||
showCollections = true;
|
||||
|
||||
isSshKeysEnabled = false;
|
||||
|
||||
private loadedTimeout: number;
|
||||
private selectedTimeout: number;
|
||||
private preventSelected = false;
|
||||
private noFolderListSize = 100;
|
||||
private searchTimeout: any = null;
|
||||
private hasSearched = false;
|
||||
private hasLoadedAllCiphers = false;
|
||||
private allCiphers: CipherView[] = null;
|
||||
private destroy$ = new Subject<void>();
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
private isSearchable: boolean = false;
|
||||
|
||||
get searchText() {
|
||||
return this._searchText$.value;
|
||||
}
|
||||
set searchText(value: string) {
|
||||
this._searchText$.next(value);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private cipherService: CipherService,
|
||||
private router: Router,
|
||||
private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private route: ActivatedRoute,
|
||||
private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private searchService: SearchService,
|
||||
private location: Location,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private vaultBrowserStateService: VaultBrowserStateService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.noFolderListSize = 100;
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.showLeftHeader = !(
|
||||
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
|
||||
);
|
||||
await this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null);
|
||||
|
||||
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||
// 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.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
window.setTimeout(() => {
|
||||
// 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.load();
|
||||
}, 500);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
|
||||
const restoredScopeState = await this.restoreState();
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
|
||||
if (this.state?.searchText) {
|
||||
this.searchText = this.state.searchText;
|
||||
} else if (params.searchText) {
|
||||
this.searchText = params.searchText;
|
||||
this.location.replaceState("vault");
|
||||
}
|
||||
|
||||
if (!this.syncService.syncInProgress) {
|
||||
// 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.load();
|
||||
} else {
|
||||
this.loadedTimeout = window.setTimeout(() => {
|
||||
if (!this.loaded) {
|
||||
// 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.load();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
if (!this.syncService.syncInProgress || restoredScopeState) {
|
||||
// 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
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
|
||||
}
|
||||
});
|
||||
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
this.isSearchable = isSearchable;
|
||||
});
|
||||
|
||||
this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.loadedTimeout != null) {
|
||||
window.clearTimeout(this.loadedTimeout);
|
||||
}
|
||||
if (this.selectedTimeout != null) {
|
||||
window.clearTimeout(this.selectedTimeout);
|
||||
}
|
||||
// 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.saveState();
|
||||
this.broadcasterService.unsubscribe(ComponentId);
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
|
||||
this.updateSelectedOrg();
|
||||
await this.loadCollectionsAndFolders();
|
||||
await this.loadCiphers();
|
||||
|
||||
if (this.showNoFolderCiphers && this.nestedFolders.length > 0) {
|
||||
// Remove "No Folder" from folder listing
|
||||
this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1);
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async loadCiphers() {
|
||||
this.allCiphers = await this.cipherService.getAllDecrypted();
|
||||
if (!this.hasLoadedAllCiphers) {
|
||||
this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText));
|
||||
}
|
||||
await this.search(null);
|
||||
this.getCounts();
|
||||
}
|
||||
|
||||
async loadCollections() {
|
||||
const allCollections = await this.vaultFilterService.buildCollections(
|
||||
this.selectedOrganization,
|
||||
);
|
||||
this.collections = allCollections.fullList;
|
||||
this.nestedCollections = allCollections.nestedList;
|
||||
}
|
||||
|
||||
async loadFolders() {
|
||||
const allFolders = await firstValueFrom(
|
||||
this.vaultFilterService.buildNestedFolders(this.selectedOrganization),
|
||||
);
|
||||
this.folders = allFolders.fullList;
|
||||
this.nestedFolders = allFolders.nestedList;
|
||||
}
|
||||
|
||||
async search(timeout: number = null) {
|
||||
this.searchPending = false;
|
||||
if (this.searchTimeout != null) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
const filterDeleted = (c: CipherView) => !c.isDeleted;
|
||||
if (timeout == null) {
|
||||
this.hasSearched = this.isSearchable;
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.searchText,
|
||||
filterDeleted,
|
||||
this.allCiphers,
|
||||
);
|
||||
this.ciphers = this.ciphers.filter(
|
||||
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.searchPending = true;
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
this.hasSearched = this.isSearchable;
|
||||
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
|
||||
await this.loadCiphers();
|
||||
} else {
|
||||
this.ciphers = await this.searchService.searchCiphers(
|
||||
this.searchText,
|
||||
filterDeleted,
|
||||
this.allCiphers,
|
||||
);
|
||||
}
|
||||
this.ciphers = this.ciphers.filter(
|
||||
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
|
||||
);
|
||||
this.searchPending = false;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
async selectType(type: CipherType) {
|
||||
// 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(["/ciphers"], { queryParams: { type: type } });
|
||||
}
|
||||
|
||||
async selectFolder(folder: FolderView) {
|
||||
// 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(["/ciphers"], { queryParams: { folderId: folder.id || "none" } });
|
||||
}
|
||||
|
||||
async selectCollection(collection: CollectionView) {
|
||||
// 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(["/ciphers"], { queryParams: { collectionId: collection.id } });
|
||||
}
|
||||
|
||||
async selectTrash() {
|
||||
// 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(["/ciphers"], { queryParams: { deleted: true } });
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
this.selectedTimeout = window.setTimeout(() => {
|
||||
if (!this.preventSelected) {
|
||||
// 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.preventSelected = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
async launchCipher(cipher: CipherView) {
|
||||
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTimeout != null) {
|
||||
window.clearTimeout(this.selectedTimeout);
|
||||
}
|
||||
this.preventSelected = true;
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||
// 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
|
||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
async addCipher() {
|
||||
// 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(["/add-cipher"], {
|
||||
queryParams: { selectedVault: this.vaultFilter.selectedOrganizationId },
|
||||
});
|
||||
}
|
||||
|
||||
async vaultFilterChanged() {
|
||||
if (this.showSearching) {
|
||||
await this.search();
|
||||
}
|
||||
this.updateSelectedOrg();
|
||||
await this.loadCollectionsAndFolders();
|
||||
this.getCounts();
|
||||
}
|
||||
|
||||
updateSelectedOrg() {
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
if (this.vaultFilter.selectedOrganizationId != null) {
|
||||
this.selectedOrganization = this.vaultFilter.selectedOrganizationId;
|
||||
} else {
|
||||
this.selectedOrganization = null;
|
||||
}
|
||||
}
|
||||
|
||||
getCounts() {
|
||||
let favoriteCiphers: CipherView[] = null;
|
||||
let noFolderCiphers: CipherView[] = null;
|
||||
const folderCounts = new Map<string, number>();
|
||||
const collectionCounts = new Map<string, number>();
|
||||
const typeCounts = new Map<CipherType, number>();
|
||||
|
||||
this.deletedCount = this.allCiphers.filter(
|
||||
(c) => c.isDeleted && !this.vaultFilterService.filterCipherForSelectedVault(c),
|
||||
).length;
|
||||
|
||||
this.ciphers?.forEach((c) => {
|
||||
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
|
||||
if (c.isDeleted) {
|
||||
return;
|
||||
}
|
||||
if (c.favorite) {
|
||||
if (favoriteCiphers == null) {
|
||||
favoriteCiphers = [];
|
||||
}
|
||||
favoriteCiphers.push(c);
|
||||
}
|
||||
|
||||
if (c.folderId == null) {
|
||||
if (noFolderCiphers == null) {
|
||||
noFolderCiphers = [];
|
||||
}
|
||||
noFolderCiphers.push(c);
|
||||
}
|
||||
|
||||
if (typeCounts.has(c.type)) {
|
||||
typeCounts.set(c.type, typeCounts.get(c.type) + 1);
|
||||
} else {
|
||||
typeCounts.set(c.type, 1);
|
||||
}
|
||||
|
||||
if (folderCounts.has(c.folderId)) {
|
||||
folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1);
|
||||
} else {
|
||||
folderCounts.set(c.folderId, 1);
|
||||
}
|
||||
|
||||
if (c.collectionIds != null) {
|
||||
c.collectionIds.forEach((colId) => {
|
||||
if (collectionCounts.has(colId)) {
|
||||
collectionCounts.set(colId, collectionCounts.get(colId) + 1);
|
||||
} else {
|
||||
collectionCounts.set(colId, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.favoriteCiphers = favoriteCiphers;
|
||||
this.noFolderCiphers = noFolderCiphers;
|
||||
this.typeCounts = typeCounts;
|
||||
this.folderCounts = folderCounts;
|
||||
this.collectionCounts = collectionCounts;
|
||||
}
|
||||
|
||||
showSearching() {
|
||||
return this.hasSearched || (!this.searchPending && this.isSearchable);
|
||||
}
|
||||
|
||||
closeOnEsc(e: KeyboardEvent) {
|
||||
// If input not empty, use browser default behavior of clearing input instead
|
||||
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadCollectionsAndFolders() {
|
||||
this.showCollections = !this.vaultFilter.myVaultOnly;
|
||||
await this.loadFolders();
|
||||
await this.loadCollections();
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
this.state = Object.assign(new BrowserGroupingsComponentState(), {
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window),
|
||||
searchText: this.searchText,
|
||||
favoriteCiphers: this.favoriteCiphers,
|
||||
noFolderCiphers: this.noFolderCiphers,
|
||||
ciphers: this.ciphers,
|
||||
collectionCounts: this.collectionCounts,
|
||||
folderCounts: this.folderCounts,
|
||||
typeCounts: this.typeCounts,
|
||||
folders: this.folders,
|
||||
collections: this.collections,
|
||||
deletedCount: this.deletedCount,
|
||||
});
|
||||
await this.vaultBrowserStateService.setBrowserGroupingsComponentState(this.state);
|
||||
}
|
||||
|
||||
private async restoreState(): Promise<boolean> {
|
||||
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
|
||||
if (this.state == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.favoriteCiphers != null) {
|
||||
this.favoriteCiphers = this.state.favoriteCiphers;
|
||||
}
|
||||
if (this.state.noFolderCiphers != null) {
|
||||
this.noFolderCiphers = this.state.noFolderCiphers;
|
||||
}
|
||||
if (this.state.ciphers != null) {
|
||||
this.ciphers = this.state.ciphers;
|
||||
}
|
||||
if (this.state.collectionCounts != null) {
|
||||
this.collectionCounts = this.state.collectionCounts;
|
||||
}
|
||||
if (this.state.folderCounts != null) {
|
||||
this.folderCounts = this.state.folderCounts;
|
||||
}
|
||||
if (this.state.typeCounts != null) {
|
||||
this.typeCounts = this.state.typeCounts;
|
||||
}
|
||||
if (this.state.folders != null) {
|
||||
this.folders = this.state.folders;
|
||||
}
|
||||
if (this.state.collections != null) {
|
||||
this.collections = this.state.collections;
|
||||
}
|
||||
if (this.state.deletedCount != null) {
|
||||
this.deletedCount = this.state.deletedCount;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="back()">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
|
||||
<div class="search">
|
||||
<input
|
||||
type="{{ searchTypeSearch ? 'search' : 'text' }}"
|
||||
placeholder="{{ searchPlaceholder || ('searchVault' | i18n) }}"
|
||||
id="search"
|
||||
[(ngModel)]="searchText"
|
||||
(input)="search(200)"
|
||||
autocomplete="off"
|
||||
appAutofocus
|
||||
/>
|
||||
<i class="bwi bwi-search" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" [ngClass]="{ 'stacked-boxes': showGroupings() }">
|
||||
<ng-container *ngIf="showGroupings()">
|
||||
<app-vault-select
|
||||
*ngIf="showVaultFilter"
|
||||
(onVaultSelectionChanged)="changeVaultSelection()"
|
||||
></app-vault-select>
|
||||
<div class="box list" *ngIf="nestedFolders && nestedFolders.length">
|
||||
<h2 class="box-header">
|
||||
{{ "folders" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let f of nestedFolders"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectFolder(f.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-folder" aria-hidden="true"></i>
|
||||
</div>
|
||||
<span class="text">{{ f.node.name }}</span>
|
||||
</div>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="nestedCollections && nestedCollections.length">
|
||||
<h2 class="box-header">
|
||||
{{ "collections" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let c of nestedCollections"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="selectCollection(c.node)"
|
||||
>
|
||||
<div class="row-main">
|
||||
<div class="icon">
|
||||
<i class="bwi bwi-fw bwi-lg bwi-collection" aria-hidden="true"></i>
|
||||
</div>
|
||||
<span class="text">{{ c.node.name }}</span>
|
||||
</div>
|
||||
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="ciphers">
|
||||
<div *ngIf="!ciphers.length">
|
||||
<app-vault-select
|
||||
*ngIf="showVaultFilter && !showGroupings()"
|
||||
(onVaultSelectionChanged)="changeVaultSelection()"
|
||||
></app-vault-select>
|
||||
<div class="no-items" *ngIf="!nestedFolders?.length && !nestedCollections?.length">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
|
||||
<ng-container *ngIf="loaded">
|
||||
<img class="no-items-image" aria-hidden="true" />
|
||||
<p>{{ "noItemsInList" | i18n }}</p>
|
||||
<button type="button" (click)="addCipher()" class="btn block primary link">
|
||||
{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<cdk-virtual-scroll-viewport
|
||||
itemSize="55"
|
||||
minBufferPx="400"
|
||||
maxBufferPx="600"
|
||||
*ngIf="ciphers.length"
|
||||
#virtualScrollViewport
|
||||
><app-vault-select
|
||||
*ngIf="showVaultFilter && !showGroupings()"
|
||||
(onVaultSelectionChanged)="changeVaultSelection()"
|
||||
></app-vault-select>
|
||||
<div class="box list only-list">
|
||||
<h2 class="box-header">
|
||||
{{ groupingTitle }}
|
||||
<span class="flex-right">{{ isSearching() ? ciphers.length : ciphers.length }}</span>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<app-cipher-row
|
||||
*cdkVirtualFor="let c of ciphers; let last = last"
|
||||
[cipher]="c"
|
||||
[last]="last"
|
||||
title="{{ 'viewItem' | i18n }}"
|
||||
(onSelected)="selectCipher($event)"
|
||||
(launchEvent)="launchCipher($event)"
|
||||
></app-cipher-row>
|
||||
</div>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</ng-container>
|
||||
</main>
|
||||
@@ -1,316 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { BrowserComponentState } from "../../../../models/browserComponentState";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
const ComponentId = "VaultItemsComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-items",
|
||||
templateUrl: "vault-items.component.html",
|
||||
})
|
||||
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnInit, OnDestroy {
|
||||
groupingTitle: string;
|
||||
state: BrowserComponentState;
|
||||
folderId: string = null;
|
||||
collectionId: string = null;
|
||||
type: CipherType = null;
|
||||
nestedFolders: TreeNode<FolderView>[];
|
||||
nestedCollections: TreeNode<CollectionView>[];
|
||||
searchTypeSearch = false;
|
||||
showOrganizations = false;
|
||||
vaultFilter: VaultFilter;
|
||||
deleted = true;
|
||||
noneFolder = false;
|
||||
showVaultFilter = false;
|
||||
|
||||
private selectedTimeout: number;
|
||||
private preventSelected = false;
|
||||
private applySavedState = true;
|
||||
private scrollingContainer = "cdk-virtual-scroll-viewport";
|
||||
|
||||
constructor(
|
||||
searchService: SearchService,
|
||||
private organizationService: OrganizationService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private stateService: VaultBrowserStateService,
|
||||
private i18nService: I18nService,
|
||||
private collectionService: CollectionService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
cipherService: CipherService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
) {
|
||||
super(searchService, cipherService);
|
||||
this.applySavedState =
|
||||
(window as any).previousPopupUrl != null &&
|
||||
!(window as any).previousPopupUrl.startsWith("/ciphers");
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.searchTypeSearch = !this.platformUtilsService.isSafari();
|
||||
this.showOrganizations = await this.organizationService.hasOrganizations();
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (this.applySavedState) {
|
||||
this.state = await this.stateService.getBrowserVaultItemsComponentState();
|
||||
if (this.state?.searchText) {
|
||||
this.searchText = this.state.searchText;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.deleted) {
|
||||
this.showVaultFilter = true;
|
||||
this.groupingTitle = this.i18nService.t("trash");
|
||||
this.searchPlaceholder = this.i18nService.t("searchTrash");
|
||||
await this.load(this.buildFilter(), true);
|
||||
} else if (params.type) {
|
||||
this.showVaultFilter = true;
|
||||
this.searchPlaceholder = this.i18nService.t("searchType");
|
||||
this.type = parseInt(params.type, null);
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
this.groupingTitle = this.i18nService.t("logins");
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.groupingTitle = this.i18nService.t("cards");
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.groupingTitle = this.i18nService.t("identities");
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.groupingTitle = this.i18nService.t("secureNotes");
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
this.groupingTitle = this.i18nService.t("sshKeys");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
await this.load(this.buildFilter());
|
||||
} else if (params.folderId) {
|
||||
this.showVaultFilter = true;
|
||||
this.folderId = params.folderId === "none" ? null : params.folderId;
|
||||
this.searchPlaceholder = this.i18nService.t("searchFolder");
|
||||
if (this.folderId != null) {
|
||||
this.showOrganizations = false;
|
||||
const folderNode = await this.vaultFilterService.getFolderNested(this.folderId);
|
||||
if (folderNode != null && folderNode.node != null) {
|
||||
this.groupingTitle = folderNode.node.name;
|
||||
this.nestedFolders =
|
||||
folderNode.children != null && folderNode.children.length > 0
|
||||
? folderNode.children
|
||||
: null;
|
||||
}
|
||||
} else {
|
||||
this.noneFolder = true;
|
||||
this.groupingTitle = this.i18nService.t("noneFolder");
|
||||
}
|
||||
await this.load(this.buildFilter());
|
||||
} else if (params.collectionId) {
|
||||
this.showVaultFilter = false;
|
||||
this.collectionId = params.collectionId;
|
||||
this.searchPlaceholder = this.i18nService.t("searchCollection");
|
||||
const collectionNode = await this.collectionService.getNested(this.collectionId);
|
||||
if (collectionNode != null && collectionNode.node != null) {
|
||||
this.groupingTitle = collectionNode.node.name;
|
||||
this.nestedCollections =
|
||||
collectionNode.children != null && collectionNode.children.length > 0
|
||||
? collectionNode.children
|
||||
: null;
|
||||
}
|
||||
await this.load(
|
||||
(c) => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1,
|
||||
);
|
||||
} else {
|
||||
this.showVaultFilter = true;
|
||||
this.groupingTitle = this.i18nService.t("allItems");
|
||||
await this.load(this.buildFilter());
|
||||
}
|
||||
|
||||
if (this.applySavedState && this.state != null) {
|
||||
// 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
|
||||
BrowserPopupUtils.setContentScrollY(window, this.state.scrollY, {
|
||||
delay: 0,
|
||||
containerSelector: this.scrollingContainer,
|
||||
});
|
||||
}
|
||||
await this.stateService.setBrowserVaultItemsComponentState(null);
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||
// 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.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
window.setTimeout(() => {
|
||||
// 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.refresh();
|
||||
}, 500);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
// 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.saveState();
|
||||
this.broadcasterService.unsubscribe(ComponentId);
|
||||
}
|
||||
|
||||
selectCipher(cipher: CipherView) {
|
||||
this.selectedTimeout = window.setTimeout(() => {
|
||||
if (!this.preventSelected) {
|
||||
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, collectionId: this.collectionId },
|
||||
});
|
||||
}
|
||||
this.preventSelected = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
selectFolder(folder: FolderView) {
|
||||
if (folder.id != null) {
|
||||
// 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(["/ciphers"], { queryParams: { folderId: folder.id } });
|
||||
}
|
||||
}
|
||||
|
||||
selectCollection(collection: CollectionView) {
|
||||
// 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(["/ciphers"], { queryParams: { collectionId: collection.id } });
|
||||
}
|
||||
|
||||
async launchCipher(cipher: CipherView) {
|
||||
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedTimeout != null) {
|
||||
window.clearTimeout(this.selectedTimeout);
|
||||
}
|
||||
this.preventSelected = true;
|
||||
await this.cipherService.updateLastLaunchedDate(cipher.id);
|
||||
// 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
|
||||
BrowserApi.createNewTab(cipher.login.launchUri);
|
||||
if (BrowserPopupUtils.inPopup(window)) {
|
||||
BrowserApi.closePopup(window);
|
||||
}
|
||||
}
|
||||
|
||||
addCipher() {
|
||||
if (this.deleted) {
|
||||
return false;
|
||||
}
|
||||
super.addCipher();
|
||||
// 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(["/add-cipher"], {
|
||||
queryParams: {
|
||||
folderId: this.folderId,
|
||||
type: this.type,
|
||||
collectionId: this.collectionId,
|
||||
selectedVault: this.vaultFilter.selectedOrganizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
back() {
|
||||
(window as any).routeDirection = "b";
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
showGroupings() {
|
||||
return (
|
||||
!this.isSearching() &&
|
||||
((this.nestedFolders && this.nestedFolders.length) ||
|
||||
(this.nestedCollections && this.nestedCollections.length))
|
||||
);
|
||||
}
|
||||
|
||||
async changeVaultSelection() {
|
||||
this.vaultFilter = this.vaultFilterService.getVaultFilter();
|
||||
await this.load(this.buildFilter(), this.deleted);
|
||||
}
|
||||
|
||||
private buildFilter(): (cipher: CipherView) => boolean {
|
||||
return (cipher) => {
|
||||
let cipherPassesFilter = true;
|
||||
if (this.deleted && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.isDeleted;
|
||||
}
|
||||
if (this.type != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.type === this.type;
|
||||
}
|
||||
if (this.folderId != null && this.folderId != "none" && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.folderId === this.folderId;
|
||||
}
|
||||
if (this.noneFolder) {
|
||||
cipherPassesFilter = cipher.folderId == null;
|
||||
}
|
||||
if (this.collectionId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter =
|
||||
cipher.collectionIds != null && cipher.collectionIds.indexOf(this.collectionId) > -1;
|
||||
}
|
||||
if (this.vaultFilter.selectedOrganizationId != null && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === this.vaultFilter.selectedOrganizationId;
|
||||
}
|
||||
if (this.vaultFilter.myVaultOnly && cipherPassesFilter) {
|
||||
cipherPassesFilter = cipher.organizationId === null;
|
||||
}
|
||||
return cipherPassesFilter;
|
||||
};
|
||||
}
|
||||
|
||||
private async saveState() {
|
||||
this.state = {
|
||||
scrollY: BrowserPopupUtils.getContentScrollY(window, this.scrollingContainer),
|
||||
searchText: this.searchText,
|
||||
};
|
||||
await this.stateService.setBrowserVaultItemsComponentState(this.state);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<ng-container *ngIf="loaded && organizations$ | async as organizations">
|
||||
<div class="content org-filter-content" *ngIf="loaded && shouldShow(organizations)">
|
||||
<ng-container *ngIf="selectedVault$ | async as vaultFilterDisplay">
|
||||
<button
|
||||
type="button"
|
||||
#toggleVaults
|
||||
class="org-filter"
|
||||
(click)="openOverlay()"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="cdk-overlay-container"
|
||||
[attr.aria-expanded]="isOpen"
|
||||
[attr.aria-label]="vaultFilterDisplay"
|
||||
>
|
||||
<span class="org-filter-text-container">
|
||||
<span class="org-filter-text-name">{{ vaultFilterDisplay }}</span
|
||||
>
|
||||
<span
|
||||
><i
|
||||
class="bwi bwi-sm"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
|
||||
></i></span
|
||||
></span>
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-template class="vault-select-container" #vaultSelectorTemplate>
|
||||
<div
|
||||
class="vault-select"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<button type="button" appStopClick (click)="selectAllVaults()">
|
||||
<div class="vault-select-org-text-container">
|
||||
<i class="bwi bwi-fw bwi-filter vault-select-prefix-icon" aria-hidden="true"></i>
|
||||
<span class="vault-select-org-name">{{ "allVaults" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="!enforcePersonalOwnership"
|
||||
appStopClick
|
||||
(click)="selectMyVault()"
|
||||
>
|
||||
<div class="vault-select-org-text-container">
|
||||
<i class="bwi bwi-fw bwi-user vault-select-prefix-icon" aria-hidden="true"></i>
|
||||
<span class="vault-select-org-name">{{ "myVault" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
*ngFor="let organization of organizations"
|
||||
appStopClick
|
||||
(click)="selectOrganization(organization)"
|
||||
>
|
||||
<div class="vault-select-org-text-container">
|
||||
<i
|
||||
*ngIf="organization.productTierType !== 1"
|
||||
class="bwi bwi-fw bwi-business vault-select-prefix-icon"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
*ngIf="organization.productTierType === 1"
|
||||
class="bwi bwi-fw bwi-family vault-select-prefix-icon"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
|
||||
<span class="vault-select-org-name">{{ organization.name }}</span
|
||||
><i
|
||||
*ngIf="!organization.enabled"
|
||||
class="bwi bwi-fw bwi-exclamation-triangle text-danger vault-select-suffix-icon"
|
||||
attr.aria-label="{{ 'organizationIsDisabled' | i18n }}"
|
||||
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
|
||||
></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -1,227 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition, Overlay, OverlayRef } from "@angular/cdk/overlay";
|
||||
import { TemplatePortal } from "@angular/cdk/portal";
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
concatMap,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
Subject,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { VaultFilterService } from "../../../services/vault-filter.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-select",
|
||||
templateUrl: "vault-select.component.html",
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
"void",
|
||||
style({
|
||||
opacity: 0,
|
||||
}),
|
||||
),
|
||||
transition(
|
||||
"void => open",
|
||||
animate(
|
||||
"100ms linear",
|
||||
style({
|
||||
opacity: 1,
|
||||
}),
|
||||
),
|
||||
),
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class VaultSelectComponent implements OnInit, OnDestroy {
|
||||
@Output() onVaultSelectionChanged = new EventEmitter();
|
||||
|
||||
@ViewChild("toggleVaults", { read: ElementRef })
|
||||
buttonRef: ElementRef<HTMLButtonElement>;
|
||||
@ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef<HTMLElement>;
|
||||
|
||||
private _selectedVault = new BehaviorSubject<string | null>(null);
|
||||
|
||||
isOpen = false;
|
||||
loaded = false;
|
||||
organizations$: Observable<Organization[]>;
|
||||
selectedVault$: Observable<string | null> = this._selectedVault.asObservable();
|
||||
|
||||
enforcePersonalOwnership = false;
|
||||
overlayPosition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "start",
|
||||
originY: "bottom",
|
||||
overlayX: "start",
|
||||
overlayY: "top",
|
||||
},
|
||||
];
|
||||
|
||||
private overlayRef: OverlayRef;
|
||||
private _destroy = new Subject<void>();
|
||||
|
||||
shouldShow(organizations: Organization[]): boolean {
|
||||
return (
|
||||
(organizations.length > 0 && !this.enforcePersonalOwnership) ||
|
||||
(organizations.length > 1 && this.enforcePersonalOwnership)
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private i18nService: I18nService,
|
||||
private overlay: Overlay,
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private organizationService: OrganizationService,
|
||||
private policyService: PolicyService,
|
||||
) {}
|
||||
|
||||
@HostListener("document:keydown.escape", ["$event"])
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
if (this.isOpen) {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.organizations$ = this.organizationService.memberOrganizations$
|
||||
.pipe(takeUntil(this._destroy))
|
||||
.pipe(map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))));
|
||||
|
||||
combineLatest([
|
||||
this.organizations$,
|
||||
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
|
||||
])
|
||||
.pipe(
|
||||
concatMap(async ([organizations, enforcePersonalOwnership]) => {
|
||||
this.enforcePersonalOwnership = enforcePersonalOwnership;
|
||||
|
||||
if (this.shouldShow(organizations)) {
|
||||
if (this.enforcePersonalOwnership && !this.vaultFilterService.vaultFilter.myVaultOnly) {
|
||||
const firstOrganization = organizations[0];
|
||||
this._selectedVault.next(firstOrganization.name);
|
||||
this.vaultFilterService.setVaultFilter(firstOrganization.id);
|
||||
} else if (this.vaultFilterService.vaultFilter.myVaultOnly) {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
|
||||
} else if (this.vaultFilterService.vaultFilter.selectedOrganizationId != null) {
|
||||
const selectedOrganization = organizations.find(
|
||||
(o) => o.id === this.vaultFilterService.vaultFilter.selectedOrganizationId,
|
||||
);
|
||||
this._selectedVault.next(selectedOrganization.name);
|
||||
} else {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(takeUntil(this._destroy))
|
||||
.subscribe();
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._destroy.next();
|
||||
this._destroy.complete();
|
||||
this._selectedVault.complete();
|
||||
}
|
||||
|
||||
openOverlay() {
|
||||
const viewPortHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
|
||||
const positionStrategyBuilder = this.overlay.position();
|
||||
|
||||
const positionStrategy = positionStrategyBuilder
|
||||
.flexibleConnectedTo(this.buttonRef.nativeElement)
|
||||
.withFlexibleDimensions(true)
|
||||
.withPush(true)
|
||||
.withViewportMargin(10)
|
||||
.withGrowAfterOpen(true)
|
||||
.withPositions(this.overlayPosition);
|
||||
|
||||
this.overlayRef = this.overlay.create({
|
||||
hasBackdrop: true,
|
||||
positionStrategy,
|
||||
maxHeight: viewPortHeight - 160,
|
||||
backdropClass: "cdk-overlay-transparent-backdrop",
|
||||
scrollStrategy: this.overlay.scrollStrategies.close(),
|
||||
});
|
||||
|
||||
const templatePortal = new TemplatePortal(this.templateRef, this.viewContainerRef);
|
||||
this.overlayRef.attach(templatePortal);
|
||||
this.isOpen = true;
|
||||
|
||||
// Handle closing
|
||||
merge(
|
||||
this.overlayRef.outsidePointerEvents(),
|
||||
this.overlayRef.backdropClick(),
|
||||
this.overlayRef.detachments(),
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
).subscribe(() => {
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.overlayRef) {
|
||||
this.overlayRef.dispose();
|
||||
this.overlayRef = undefined;
|
||||
}
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
selectOrganization(organization: Organization) {
|
||||
if (!organization.enabled) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
null,
|
||||
this.i18nService.t("disabledOrganizationFilterError"),
|
||||
);
|
||||
} else {
|
||||
this._selectedVault.next(organization.name);
|
||||
this.vaultFilterService.setVaultFilter(organization.id);
|
||||
this.onVaultSelectionChanged.emit();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
selectAllVaults() {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
|
||||
this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults);
|
||||
this.onVaultSelectionChanged.emit();
|
||||
this.close();
|
||||
}
|
||||
selectMyVault() {
|
||||
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
|
||||
this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault);
|
||||
this.onVaultSelectionChanged.emit();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
<ng-container>
|
||||
<h2 class="box-header">
|
||||
{{ "customFields" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
|
||||
<div class="row-main">
|
||||
<span
|
||||
*ngIf="field.type != fieldType.Linked"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, field.value)"
|
||||
>{{ field.name }}</span
|
||||
>
|
||||
<span *ngIf="field.type === fieldType.Linked" class="row-label">{{ field.name }}</span>
|
||||
<div *ngIf="field.type === fieldType.Text">
|
||||
{{ field.value || " " }}
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Hidden">
|
||||
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
|
||||
<span
|
||||
*ngIf="field.showValue && !field.showCount"
|
||||
class="monospaced show-whitespace"
|
||||
[innerHTML]="field.value | colorPassword"
|
||||
></span>
|
||||
<span
|
||||
*ngIf="field.showValue && field.showCount"
|
||||
[innerHTML]="field.value | colorPasswordCount"
|
||||
></span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Boolean">
|
||||
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-square" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ field.value }}</span>
|
||||
</div>
|
||||
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
|
||||
<div class="icon icon-small">
|
||||
<i
|
||||
class="bwi bwi-link"
|
||||
aria-hidden="true"
|
||||
appA11yTitle="{{ 'linkedValue' | i18n }}"
|
||||
></i>
|
||||
<span class="sr-only">{{ "linkedValue" | i18n }}</span>
|
||||
</div>
|
||||
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons action-buttons-fixed">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ field.name }}"
|
||||
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword && field.showValue"
|
||||
(click)="toggleFieldCount(field)"
|
||||
[attr.aria-pressed]="field.showCount"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ field.name }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword"
|
||||
(click)="toggleFieldValue(field)"
|
||||
[attr.aria-pressed]="field.showValue"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !field.showValue, 'bwi-eye-slash': field.showValue }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'copyValue' | i18n }} {{ field.name }}"
|
||||
appA11yTitle="{{ 'copyValue' | i18n }}"
|
||||
*ngIf="
|
||||
field.value &&
|
||||
field.type !== fieldType.Boolean &&
|
||||
field.type !== fieldType.Linked &&
|
||||
!(field.type === fieldType.Hidden && !cipher.viewPassword)
|
||||
"
|
||||
(click)="
|
||||
copy(field.value, 'value', field.type === fieldType.Hidden ? 'H_Field' : 'Field')
|
||||
"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ViewCustomFieldsComponent as BaseViewCustomFieldsComponent } from "@bitwarden/angular/vault/components/view-custom-fields.component";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-view-custom-fields",
|
||||
templateUrl: "view-custom-fields.component.html",
|
||||
})
|
||||
export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent {
|
||||
constructor(eventCollectionService: EventCollectionService) {
|
||||
super(eventCollectionService);
|
||||
}
|
||||
}
|
||||
@@ -1,719 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "viewItem" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right" *ngIf="cipher">
|
||||
<button type="button" (click)="edit()" *ngIf="!cipher.isDeleted">
|
||||
{{ "edit" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" *ngIf="cipher">
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
{{ "itemInformation" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<label
|
||||
for="name"
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.name)"
|
||||
>{{ "name" | i18n }}</label
|
||||
>
|
||||
<input id="name" type="text" [value]="cipher.name" readonly aria-readonly="true" />
|
||||
</div>
|
||||
<!-- Login -->
|
||||
<div *ngIf="cipher.login">
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username">
|
||||
<div class="row-main">
|
||||
<label
|
||||
for="loginUsername"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.login.username)"
|
||||
>{{ "username" | i18n }}
|
||||
</label>
|
||||
<input
|
||||
id="loginUsername"
|
||||
type="text"
|
||||
[value]="cipher.login.username"
|
||||
readonly
|
||||
aria-readonly="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyUsername' | i18n }}"
|
||||
(click)="copy(cipher.login.username, 'username', 'Username')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.password">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.login.password)"
|
||||
>{{ "password" | i18n }}</span
|
||||
>
|
||||
<div *ngIf="!showPassword" class="monospaced">
|
||||
{{ cipher.login.maskedPassword }}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="showPassword && !showPasswordCount"
|
||||
class="monospaced password-wrapper"
|
||||
[appCopyText]="cipher.login.password"
|
||||
[innerHTML]="cipher.login.password | colorPassword"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="showPassword && showPasswordCount"
|
||||
[innerHTML]="cipher.login.password | colorPasswordCount"
|
||||
></div>
|
||||
</div>
|
||||
<div class="action-buttons action-buttons-fixed">
|
||||
<button
|
||||
type="button"
|
||||
#checkPasswordBtn
|
||||
class="row-btn btn"
|
||||
appA11yTitle="{{ 'checkPassword' | i18n }}"
|
||||
(click)="checkPassword()"
|
||||
[appApiAction]="checkPasswordPromise"
|
||||
[disabled]="$any(checkPasswordBtn).loading"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-check-circle"
|
||||
[hidden]="$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-spinner bwi-spin"
|
||||
[hidden]="!$any(checkPasswordBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ 'password' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
|
||||
(click)="togglePasswordCount()"
|
||||
*ngIf="showPassword"
|
||||
[attr.aria-pressed]="showPasswordCount"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'password' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
*ngIf="cipher.viewPassword"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')"
|
||||
*ngIf="cipher.viewPassword"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Passkey-->
|
||||
<div
|
||||
class="box"
|
||||
*ngIf="cipher.login.hasFido2Credentials"
|
||||
tabindex="0"
|
||||
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
|
||||
>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row text-muted">
|
||||
<span class="row-label">{{ "typePasskey" | i18n }}</span>
|
||||
{{ fido2CredentialCreationDateValue }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="box-content-row box-content-row-flex totp"
|
||||
[ngClass]="{ low: totpLow }"
|
||||
*ngIf="cipher.login.totp && totpCode"
|
||||
>
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, totpCode)"
|
||||
>{{ "verificationCodeTotp" | i18n }}</span
|
||||
>
|
||||
<span class="totp-code">{{ totpCodeFormatted }}</span>
|
||||
</div>
|
||||
<span class="totp-countdown" aria-hidden="true">
|
||||
<span class="totp-sec">{{ totpSec }}</span>
|
||||
<svg>
|
||||
<g>
|
||||
<circle
|
||||
class="totp-circle inner"
|
||||
r="12.6"
|
||||
cy="16"
|
||||
cx="16"
|
||||
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
|
||||
></circle>
|
||||
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
title="{{ 'copyVerificationCode' | i18n }}"
|
||||
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ "copyValue" | i18n }}</span>
|
||||
<span
|
||||
class="sr-only exists-only-on-parent-focus"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>{{ totpSec }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex totp" *ngIf="showPremiumRequiredTotp">
|
||||
<div class="row-main">
|
||||
<span class="row-label">{{ "verificationCodeTotp" | i18n }}</span>
|
||||
<span class="row-label">
|
||||
<a routerLink="/premium">
|
||||
{{ "premiumSubcriptionRequired" | i18n }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<div *ngIf="cipher.card">
|
||||
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.cardholderName)"
|
||||
>{{ "cardholderName" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.cardholderName }}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.number">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.number)"
|
||||
>{{ "number" | i18n }}</span
|
||||
>
|
||||
<span [hidden]="showCardNumber" class="monospaced">{{
|
||||
cipher.card.maskedNumber | creditCardNumber: cipher.card.brand
|
||||
}}</span>
|
||||
<span [hidden]="!showCardNumber" class="monospaced">{{
|
||||
cipher.card.number | creditCardNumber: cipher.card.brand
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'number' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardNumber()"
|
||||
[attr.aria-pressed]="showCardNumber"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyNumber' | i18n }}"
|
||||
(click)="copy(cipher.card.number, 'number', 'Card Number')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.brand">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.brand)"
|
||||
>{{ "brand" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.brand }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.card.expiration">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.expiration)"
|
||||
>{{ "expiration" | i18n }}</span
|
||||
>
|
||||
{{ cipher.card.expiration }}
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.code">
|
||||
<div class="row-main">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.card.code)"
|
||||
>{{ "securityCode" | i18n }}</span
|
||||
>
|
||||
<span [hidden]="showCardCode" class="monospaced">{{ cipher.card.maskedCode }}</span>
|
||||
<span [hidden]="!showCardCode" class="monospaced">{{ cipher.card.code }}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'securityCode' | i18n }}"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="toggleCardCode()"
|
||||
[attr.aria-pressed]="showCardCode"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
|
||||
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Identity -->
|
||||
<div *ngIf="cipher.identity">
|
||||
<div class="box-content-row" *ngIf="cipher.identity.fullName">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.fullName)"
|
||||
>{{ "identityName" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.fullName }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.username">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.username)"
|
||||
>{{ "username" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.username }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.company">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.company)"
|
||||
>{{ "company" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.company }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.ssn">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.ssn)"
|
||||
>{{ "ssn" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.ssn }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.passportNumber)"
|
||||
>{{ "passportNumber" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.passportNumber }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.licenseNumber)"
|
||||
>{{ "licenseNumber" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.licenseNumber }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.email">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.email)"
|
||||
>{{ "email" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.email }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.identity.phone">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.identity.phone)"
|
||||
>{{ "phone" | i18n }}</span
|
||||
>
|
||||
{{ cipher.identity.phone }}
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row"
|
||||
*ngIf="cipher.identity.address1 || cipher.identity.city || cipher.identity.country"
|
||||
>
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="
|
||||
setTextDataOnDrag(
|
||||
$event,
|
||||
(cipher.identity.address1 ? cipher.identity.address1 + '\n' : '') +
|
||||
(cipher.identity.address2 ? cipher.identity.address2 + '\n' : '') +
|
||||
(cipher.identity.address3 ? cipher.identity.address3 + '\n' : '') +
|
||||
(cipher.identity.fullAddressPart2
|
||||
? cipher.identity.fullAddressPart2 + '\n'
|
||||
: '') +
|
||||
(cipher.identity.country ? cipher.identity.country : '')
|
||||
)
|
||||
"
|
||||
>{{ "address" | i18n }}</span
|
||||
>
|
||||
<div *ngIf="cipher.identity.address1">{{ cipher.identity.address1 }}</div>
|
||||
<div *ngIf="cipher.identity.address2">{{ cipher.identity.address2 }}</div>
|
||||
<div *ngIf="cipher.identity.address3">{{ cipher.identity.address3 }}</div>
|
||||
<div *ngIf="cipher.identity.fullAddressPart2">{{ cipher.identity.fullAddressPart2 }}</div>
|
||||
<div *ngIf="cipher.identity.country">{{ cipher.identity.country }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SshKey -->
|
||||
<div *ngIf="cipher.sshKey">
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.privateKey)"
|
||||
>
|
||||
{{ "sshPrivateKey" | i18n }}
|
||||
</span>
|
||||
<div [innerText]="cipher.sshKey.maskedPrivateKey" class="monospaced"></div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.publicKey)"
|
||||
>
|
||||
{{ "sshPublicKey" | i18n }}</span
|
||||
>
|
||||
{{ cipher.sshKey.publicKey }}
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="cipher.sshKey.keyFingerprint" style="overflow: hidden">
|
||||
<span
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.keyFingerprint)"
|
||||
>
|
||||
{{ "sshFingerprint" | i18n }}</span
|
||||
>
|
||||
{{ cipher.sshKey.keyFingerprint }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.login && cipher.login.hasUris">
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
*ngFor="let u of cipher.login.uris; let i = index"
|
||||
>
|
||||
<div class="row-main">
|
||||
<label
|
||||
for="hostOrUri{{ i }}"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, u.uri)"
|
||||
*ngIf="!u.isWebsite"
|
||||
>{{ "uri" | i18n }}</label
|
||||
>
|
||||
<label
|
||||
for="hostOrUri{{ i }}"
|
||||
class="row-label draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, u.uri)"
|
||||
*ngIf="u.isWebsite"
|
||||
>{{ "website" | i18n }}</label
|
||||
>
|
||||
<span title="{{ u.uri }}">
|
||||
<input
|
||||
id="hostOrUri{{ i }}"
|
||||
type="text"
|
||||
[value]="u.hostOrUri"
|
||||
readonly
|
||||
aria-readonly="true"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'launch' | i18n }} {{ u.uri }}"
|
||||
appA11yTitle="{{ 'launch' | i18n }}"
|
||||
*ngIf="u.canLaunch"
|
||||
(click)="launch(u, cipher.id)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
attr.aria-label="{{ 'copyUri' | i18n }} {{ u.uri }}"
|
||||
appA11yTitle="{{ 'copyUri' | i18n }}"
|
||||
(click)="copy(u.uri, u.isWebsite ? 'website' : 'uri', 'URI')"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.folderId && folder">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<label
|
||||
for="folderName"
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, folder.name)"
|
||||
>{{ "folder" | i18n }}</label
|
||||
>
|
||||
<input id="folderName" type="text" name="folderName" [value]="folder.name" readonly />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.notes">
|
||||
<h2 class="box-header">
|
||||
<label
|
||||
for="notes"
|
||||
class="draggable"
|
||||
draggable="true"
|
||||
(dragstart)="setTextDataOnDrag($event, cipher.notes)"
|
||||
>{{ "notes" | i18n }}</label
|
||||
>
|
||||
</h2>
|
||||
<div class="box-content">
|
||||
<div class="box-content-row">
|
||||
<textarea
|
||||
id="notes"
|
||||
[value]="cipher.notes"
|
||||
rows="6"
|
||||
readonly
|
||||
aria-readonly="true"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="cipher.hasFields">
|
||||
<app-vault-view-custom-fields
|
||||
[cipher]="cipher"
|
||||
[promptPassword]="promptPassword.bind(this)"
|
||||
[copy]="copy.bind(this)"
|
||||
></app-vault-view-custom-fields>
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
*ngIf="cipher.hasAttachments && (canAccessPremium || cipher.organizationId) && showAttachments"
|
||||
>
|
||||
<h2 class="box-header">
|
||||
{{ "attachments" | i18n }}
|
||||
</h2>
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
*ngFor="let attachment of cipher.attachments"
|
||||
appStopClick
|
||||
(click)="downloadAttachment(attachment)"
|
||||
>
|
||||
<span class="row-main">{{ attachment.fileName }}</span>
|
||||
<small class="row-sub-label">{{ attachment.sizeName }}</small>
|
||||
<i
|
||||
class="bwi bwi-download bwi-fw row-sub-icon"
|
||||
*ngIf="!$any(attachment).downloading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-fw bwi-spin row-sub-icon"
|
||||
*ngIf="$any(attachment).downloading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="fillCipher()"
|
||||
*ngIf="
|
||||
cipher.type !== cipherType.SecureNote &&
|
||||
!cipher.isDeleted &&
|
||||
(!this.inPopout || this.loadAction)
|
||||
"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-pencil-square bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "autoFill" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="fillCipherAndSave()"
|
||||
*ngIf="cipher.type === cipherType.Login && !cipher.isDeleted && !inPopout"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-bookmark bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "autoFillAndSave" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="clone()"
|
||||
*ngIf="!cipher.organizationId && !cipher.isDeleted"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-files bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "cloneItem" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="share()"
|
||||
*ngIf="!cipher.organizationId"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "moveToOrganization" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="restore()"
|
||||
*ngIf="cipher.isDeleted"
|
||||
>
|
||||
<div class="row-main text-primary">
|
||||
<div class="icon text-primary" aria-hidden="true">
|
||||
<i class="bwi bwi-undo bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ "restoreItem" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="delete()"
|
||||
*ngIf="canDeleteCipher$ | async"
|
||||
>
|
||||
<div class="row-main text-danger">
|
||||
<div class="icon text-danger" aria-hidden="true">
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw"></i>
|
||||
</div>
|
||||
<span>{{ (cipher.isDeleted ? "permanentlyDeleteItem" : "deleteItem") | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-footer">
|
||||
<div>
|
||||
<b class="font-weight-semibold">{{ "dateUpdated" | i18n }}:</b>
|
||||
{{ cipher.revisionDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.creationDate">
|
||||
<b class="font-weight-semibold">{{ "dateCreated" | i18n }}:</b>
|
||||
{{ cipher.creationDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.passwordRevisionDisplayDate">
|
||||
<b class="font-weight-semibold">{{ "datePasswordUpdated" | i18n }}:</b>
|
||||
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
|
||||
</div>
|
||||
<div *ngIf="cipher.hasPasswordHistory">
|
||||
<b class="font-weight-semibold">{{ "passwordHistory" | i18n }}:</b>
|
||||
<button
|
||||
type="button"
|
||||
routerLink="/cipher-password-history"
|
||||
[queryParams]="{ cipherId: cipher.id }"
|
||||
appStopClick
|
||||
title="{{ 'passwordHistory' | i18n }}"
|
||||
>
|
||||
{{ cipher.passwordHistory.length }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,443 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DatePipe, Location } from "@angular/common";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnInit, OnDestroy } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
|
||||
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
|
||||
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
||||
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
||||
import { closeViewVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
|
||||
|
||||
const BroadcasterSubscriptionId = "ChildViewComponent";
|
||||
|
||||
export const AUTOFILL_ID = "autofill";
|
||||
export const SHOW_AUTOFILL_BUTTON = "show-autofill-button";
|
||||
export const COPY_USERNAME_ID = "copy-username";
|
||||
export const COPY_PASSWORD_ID = "copy-password";
|
||||
export const COPY_VERIFICATION_CODE_ID = "copy-totp";
|
||||
|
||||
type CopyAction =
|
||||
| typeof COPY_USERNAME_ID
|
||||
| typeof COPY_PASSWORD_ID
|
||||
| typeof COPY_VERIFICATION_CODE_ID;
|
||||
type LoadAction = typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON | CopyAction;
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-view",
|
||||
templateUrl: "view.component.html",
|
||||
})
|
||||
export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy {
|
||||
showAttachments = true;
|
||||
pageDetails: any[] = [];
|
||||
tab: any;
|
||||
senderTabId?: number;
|
||||
loadAction?: LoadAction;
|
||||
private static readonly copyActions = new Set([
|
||||
COPY_USERNAME_ID,
|
||||
COPY_PASSWORD_ID,
|
||||
COPY_VERIFICATION_CODE_ID,
|
||||
]);
|
||||
uilocation?: "popout" | "popup" | "sidebar" | "tab";
|
||||
loadPageDetailsTimeout: number;
|
||||
inPopout = false;
|
||||
cipherType = CipherType;
|
||||
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
|
||||
private collectPageDetailsSubscription: Subscription;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
folderService: FolderService,
|
||||
totpService: TotpServiceAbstraction,
|
||||
tokenService: TokenService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
auditService: AuditService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
broadcasterService: BroadcasterService,
|
||||
ngZone: NgZone,
|
||||
changeDetectorRef: ChangeDetectorRef,
|
||||
stateService: StateService,
|
||||
eventCollectionService: EventCollectionService,
|
||||
private autofillService: AutofillService,
|
||||
private messagingService: MessagingService,
|
||||
apiService: ApiService,
|
||||
passwordRepromptService: PasswordRepromptService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
accountService: AccountService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
folderService,
|
||||
totpService,
|
||||
tokenService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
auditService,
|
||||
window,
|
||||
broadcasterService,
|
||||
ngZone,
|
||||
changeDetectorRef,
|
||||
eventCollectionService,
|
||||
apiService,
|
||||
passwordRepromptService,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
datePipe,
|
||||
accountService,
|
||||
billingAccountProfileStateService,
|
||||
cipherAuthorizationService,
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
this.loadAction = value?.action;
|
||||
this.senderTabId = parseInt(value?.senderTabId, 10) || undefined;
|
||||
this.uilocation = value?.uilocation;
|
||||
});
|
||||
|
||||
this.inPopout = this.uilocation === "popout" || BrowserPopupUtils.inPopout(window);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
this.cipherId = params.cipherId;
|
||||
}
|
||||
|
||||
if (params.collectionId) {
|
||||
this.collectionId = params.collectionId;
|
||||
}
|
||||
|
||||
if (!params.cipherId) {
|
||||
// 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.close();
|
||||
}
|
||||
|
||||
await this.load();
|
||||
});
|
||||
|
||||
super.ngOnInit();
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// 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.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "tabChanged":
|
||||
case "windowChanged":
|
||||
if (this.loadPageDetailsTimeout != null) {
|
||||
window.clearTimeout(this.loadPageDetailsTimeout);
|
||||
}
|
||||
this.loadPageDetailsTimeout = window.setTimeout(() => this.loadPageDetails(), 500);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
super.ngOnDestroy();
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
}
|
||||
|
||||
async load() {
|
||||
await super.load();
|
||||
await this.loadPageDetails();
|
||||
await this.handleLoadAction();
|
||||
}
|
||||
|
||||
async edit() {
|
||||
if (this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (!(await super.edit())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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(["/edit-cipher"], {
|
||||
queryParams: {
|
||||
cipherId: this.cipher.id,
|
||||
type: this.cipher.type,
|
||||
isNew: false,
|
||||
collectionId: this.collectionId,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async clone() {
|
||||
if (this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(await super.clone())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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(["/clone-cipher"], {
|
||||
queryParams: {
|
||||
cloneMode: true,
|
||||
cipherId: this.cipher.id,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async share() {
|
||||
if (!(await super.share())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.cipher.organizationId == null) {
|
||||
// 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(["/share-cipher"], {
|
||||
replaceUrl: true,
|
||||
queryParams: { cipherId: this.cipher.id },
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async fillCipher() {
|
||||
const didAutofill = await this.doAutofill();
|
||||
if (didAutofill) {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess"));
|
||||
}
|
||||
|
||||
return didAutofill;
|
||||
}
|
||||
|
||||
async fillCipherAndSave() {
|
||||
const didAutofill = await this.doAutofill();
|
||||
|
||||
if (didAutofill) {
|
||||
if (this.tab == null) {
|
||||
throw new Error("No tab found.");
|
||||
}
|
||||
|
||||
if (this.cipher.login.uris == null) {
|
||||
this.cipher.login.uris = [];
|
||||
} else {
|
||||
if (this.cipher.login.uris.some((uri) => uri.uri === this.tab.url)) {
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("autoFillSuccessAndSavedUri"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const loginUri = new LoginUriView();
|
||||
loginUri.uri = this.tab.url;
|
||||
this.cipher.login.uris.push(loginUri);
|
||||
|
||||
try {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const cipher: Cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
|
||||
await this.cipherService.updateWithServer(cipher);
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("autoFillSuccessAndSavedUri"),
|
||||
);
|
||||
this.messagingService.send("editedCipher");
|
||||
} catch {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restore() {
|
||||
if (!this.cipher.isDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (await super.restore()) {
|
||||
// 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.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (await super.delete()) {
|
||||
this.messagingService.send("deletedCipher");
|
||||
// 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.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async close() {
|
||||
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
|
||||
if (this.inPopout && sessionData.isFido2Session) {
|
||||
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) &&
|
||||
this.senderTabId
|
||||
) {
|
||||
// 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
|
||||
BrowserApi.focusTab(this.senderTabId);
|
||||
// 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
|
||||
closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
private async loadPageDetails() {
|
||||
this.collectPageDetailsSubscription?.unsubscribe();
|
||||
this.pageDetails = [];
|
||||
this.tab = this.senderTabId
|
||||
? await BrowserApi.getTab(this.senderTabId)
|
||||
: await BrowserApi.getTabFromCurrentWindow();
|
||||
|
||||
if (!this.tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.collectPageDetailsSubscription = this.autofillService
|
||||
.collectPageDetailsFromTab$(this.tab)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
|
||||
}
|
||||
|
||||
private async doAutofill() {
|
||||
const originalTabURL = this.tab.url?.length && new URL(this.tab.url);
|
||||
|
||||
if (!(await this.promptPassword())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTabURL = this.tab.url?.length && new URL(this.tab.url);
|
||||
|
||||
const originalTabHostPath =
|
||||
originalTabURL && `${originalTabURL.origin}${originalTabURL.pathname}`;
|
||||
const currentTabHostPath = currentTabURL && `${currentTabURL.origin}${currentTabURL.pathname}`;
|
||||
|
||||
const tabUrlChanged = originalTabHostPath !== currentTabHostPath;
|
||||
|
||||
if (this.pageDetails == null || this.pageDetails.length === 0 || tabUrlChanged) {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.totpCode = await this.autofillService.doAutoFill({
|
||||
tab: this.tab,
|
||||
cipher: this.cipher,
|
||||
pageDetails: this.pageDetails,
|
||||
doc: window.document,
|
||||
fillNewPassword: true,
|
||||
allowTotpAutofill: true,
|
||||
});
|
||||
if (this.totpCode != null) {
|
||||
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
|
||||
}
|
||||
} catch {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
|
||||
this.changeDetectorRef.detectChanges();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async handleLoadAction() {
|
||||
if (!this.loadAction || this.loadAction === SHOW_AUTOFILL_BUTTON) {
|
||||
return;
|
||||
}
|
||||
|
||||
let loadActionSuccess = false;
|
||||
if (this.loadAction === AUTOFILL_ID) {
|
||||
loadActionSuccess = await this.fillCipher();
|
||||
}
|
||||
|
||||
if (ViewComponent.copyActions.has(this.loadAction)) {
|
||||
const { username, password } = this.cipher.login;
|
||||
const copyParams: Record<CopyAction, Record<string, string>> = {
|
||||
[COPY_USERNAME_ID]: { value: username, type: "username", name: "Username" },
|
||||
[COPY_PASSWORD_ID]: { value: password, type: "password", name: "Password" },
|
||||
[COPY_VERIFICATION_CODE_ID]: {
|
||||
value: this.totpCode,
|
||||
type: "verificationCodeTotp",
|
||||
name: "TOTP",
|
||||
},
|
||||
};
|
||||
const { value, type, name } = copyParams[this.loadAction as CopyAction];
|
||||
loadActionSuccess = await this.copy(value, type, name);
|
||||
}
|
||||
|
||||
if (this.inPopout) {
|
||||
setTimeout(() => this.close(), loadActionSuccess ? 1000 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/tabs/settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "appearance" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="theme">{{ "theme" | i18n }}</label>
|
||||
<select
|
||||
id="theme"
|
||||
name="Theme"
|
||||
aria-describedby="themeHelp"
|
||||
[(ngModel)]="theme"
|
||||
(change)="saveTheme()"
|
||||
>
|
||||
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="themeHelp" class="box-footer">
|
||||
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
|
||||
<input
|
||||
id="badge"
|
||||
type="checkbox"
|
||||
aria-describedby="badgeHelp"
|
||||
(change)="updateBadgeCounter()"
|
||||
[(ngModel)]="enableBadgeCounter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
|
||||
<input
|
||||
id="favicon"
|
||||
type="checkbox"
|
||||
aria-describedby="faviconHelp"
|
||||
(change)="updateFavicon()"
|
||||
[(ngModel)]="enableFavicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="faviconHelp" class="box-footer">
|
||||
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="routing">{{ "enableAnimations" | i18n }}</label>
|
||||
<input
|
||||
id="routing"
|
||||
type="checkbox"
|
||||
(change)="updateRoutingAnimation()"
|
||||
[(ngModel)]="enableRoutingAnimation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,75 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { enableAccountSwitching } from "../../../platform/flags";
|
||||
|
||||
@Component({
|
||||
selector: "vault-appearance",
|
||||
templateUrl: "appearance.component.html",
|
||||
})
|
||||
export class AppearanceComponent implements OnInit {
|
||||
enableFavicon = false;
|
||||
enableBadgeCounter = true;
|
||||
theme: ThemeType;
|
||||
themeOptions: any[];
|
||||
accountSwitcherEnabled = false;
|
||||
enableRoutingAnimation: boolean;
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
private themeStateService: ThemeStateService,
|
||||
private animationControlService: AnimationControlService,
|
||||
) {
|
||||
this.themeOptions = [
|
||||
{ name: i18nService.t("default"), value: ThemeType.System },
|
||||
{ name: i18nService.t("light"), value: ThemeType.Light },
|
||||
{ name: i18nService.t("dark"), value: ThemeType.Dark },
|
||||
{ name: "Nord", value: ThemeType.Nord },
|
||||
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
||||
];
|
||||
|
||||
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.enableRoutingAnimation = await firstValueFrom(
|
||||
this.animationControlService.enableRoutingAnimation$,
|
||||
);
|
||||
|
||||
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||
|
||||
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
|
||||
|
||||
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||
}
|
||||
|
||||
async updateRoutingAnimation() {
|
||||
await this.animationControlService.setEnableRoutingAnimation(this.enableRoutingAnimation);
|
||||
}
|
||||
|
||||
async updateFavicon() {
|
||||
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
|
||||
}
|
||||
|
||||
async updateBadgeCounter() {
|
||||
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
|
||||
this.messagingService.send("bgUpdateContextMenu");
|
||||
}
|
||||
|
||||
async saveTheme() {
|
||||
await this.themeStateService.setSelectedTheme(this.theme);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/folders">{{ "cancel" | i18n }}</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ title }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="submit" [disabled]="form.loading">
|
||||
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1" *ngIf="folder">
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="name">{{ "name" | i18n }}</label>
|
||||
<input id="name" type="text" formControlName="name" [appAutofocus]="!editMode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box list" *ngIf="editMode">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row"
|
||||
appStopClick
|
||||
(click)="delete()"
|
||||
[appApiAction]="deletePromise"
|
||||
#deleteBtn
|
||||
>
|
||||
<div class="row-main text-danger">
|
||||
<div class="icon text-danger" aria-hidden="true">
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
||||
[hidden]="!$any(deleteBtn).loading"
|
||||
></i>
|
||||
</div>
|
||||
<span>{{ "deleteFolder" | i18n }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</form>
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "app-folder-add-edit",
|
||||
templateUrl: "folder-add-edit.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class FolderAddEditComponent extends BaseFolderAddEditComponent implements OnInit {
|
||||
constructor(
|
||||
folderService: FolderService,
|
||||
folderApiService: FolderApiServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
keyService: KeyService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
logService: LogService,
|
||||
dialogService: DialogService,
|
||||
formBuilder: FormBuilder,
|
||||
) {
|
||||
super(
|
||||
folderService,
|
||||
folderApiService,
|
||||
accountService,
|
||||
keyService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
dialogService,
|
||||
formBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.queryParams.pipe(first()).subscribe(async (params) => {
|
||||
if (params.folderId) {
|
||||
this.folderId = params.folderId;
|
||||
}
|
||||
await this.init();
|
||||
});
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
if (await super.submit()) {
|
||||
// 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(["/folders"]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async delete(): Promise<boolean> {
|
||||
const confirmed = await super.delete();
|
||||
if (confirmed) {
|
||||
// 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(["/folders"]);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/vault-settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "folders" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<button type="button" (click)="addFolder()" appA11yTitle="{{ 'addFolder' | i18n }}">
|
||||
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<ng-container *ngIf="folders$ | async as folders">
|
||||
<div class="box list full-list" *ngIf="folders.length; else noFoldersTemplate">
|
||||
<div class="box-content">
|
||||
<button
|
||||
type="button"
|
||||
appStopClick
|
||||
(click)="folderSelected(f)"
|
||||
class="box-content-row padded"
|
||||
*ngFor="let f of folders"
|
||||
>
|
||||
{{ f.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #noFoldersTemplate>
|
||||
<div class="no-items">
|
||||
<p>{{ "noFolders" | i18n }}</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</main>
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { filter, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-folders",
|
||||
templateUrl: "folders.component.html",
|
||||
})
|
||||
export class FoldersComponent {
|
||||
folders$: Observable<FolderView[]>;
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
constructor(
|
||||
private folderService: FolderService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.folders$ = this.activeUserId$.pipe(
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) => this.folderService.folderViews$(userId)),
|
||||
map((folders) => {
|
||||
// Remove the last folder, which is the "no folder" option folder
|
||||
if (folders.length > 0) {
|
||||
return folders.slice(0, folders.length - 1);
|
||||
}
|
||||
return folders;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
folderSelected(folder: FolderView) {
|
||||
// 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(["/edit-folder"], { queryParams: { folderId: folder.id } });
|
||||
}
|
||||
|
||||
addFolder() {
|
||||
// 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(["/add-folder"]);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/vault-settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "sync" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right"></div>
|
||||
</header>
|
||||
<main tabindex="-1">
|
||||
<div class="content center-content">
|
||||
<button
|
||||
type="button"
|
||||
class="btn block primary"
|
||||
aria-describedby="lastSyncHint"
|
||||
(click)="sync()"
|
||||
#syncBtn
|
||||
[disabled]="$any(syncBtn).loading"
|
||||
[appApiAction]="syncPromise"
|
||||
>
|
||||
<span [hidden]="$any(syncBtn).loading">{{ "syncVaultNow" | i18n }}</span>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-lg bwi-spin"
|
||||
[hidden]="!$any(syncBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<p id="lastSyncHint" class="text-center text-muted small">
|
||||
{{ "lastSync" | i18n }} {{ lastSync }}
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,46 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
|
||||
@Component({
|
||||
selector: "app-sync",
|
||||
templateUrl: "sync.component.html",
|
||||
})
|
||||
export class SyncComponent implements OnInit {
|
||||
lastSync = "--";
|
||||
syncPromise: Promise<any>;
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.setLastSync();
|
||||
}
|
||||
|
||||
async sync() {
|
||||
this.syncPromise = this.syncService.fullSync(true);
|
||||
const success = await this.syncPromise;
|
||||
if (success) {
|
||||
await this.setLastSync();
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingComplete"));
|
||||
} else {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("syncingFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async setLastSync() {
|
||||
const last = await this.syncService.getLastSync();
|
||||
if (last != null) {
|
||||
this.lastSync = last.toLocaleDateString() + " " + last.toLocaleTimeString();
|
||||
} else {
|
||||
this.lastSync = this.i18nService.t("never");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<app-header>
|
||||
<div class="left">
|
||||
<button type="button" routerLink="/tabs/settings">
|
||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||
<span>{{ "back" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="center">
|
||||
<span class="title">{{ "vault" | i18n }}</span>
|
||||
</h1>
|
||||
<div class="right">
|
||||
<app-pop-out></app-pop-out>
|
||||
</div>
|
||||
</app-header>
|
||||
<main tabindex="-1">
|
||||
<div class="box list">
|
||||
<div class="box-content single-line">
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
routerLink="/folders"
|
||||
>
|
||||
<div class="row-main">{{ "folders" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
appStopClick
|
||||
(click)="import()"
|
||||
>
|
||||
<div class="row-main">{{ "importItems" | i18n }}</div>
|
||||
<i
|
||||
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
routerLink="/export"
|
||||
>
|
||||
<div class="row-main">{{ "exportVault" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="box-content-row box-content-row-flex text-default"
|
||||
routerLink="/sync"
|
||||
>
|
||||
<div class="row-main">{{ "sync" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
@Component({
|
||||
selector: "vault-settings",
|
||||
templateUrl: "vault-settings.component.html",
|
||||
})
|
||||
export class VaultSettingsComponent {
|
||||
constructor(
|
||||
public messagingService: MessagingService,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
async import() {
|
||||
await this.router.navigate(["/import"]);
|
||||
if (await BrowserApi.isPopupOpen()) {
|
||||
await BrowserPopupUtils.openCurrentPagePopout(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user