mirror of
https://github.com/bitwarden/browser.git
synced 2024-09-27 04:03:00 +02:00
0a595ea95e
* create domain settings state provider * replace callsites for defaultUriMatch and neverDomains with DomainSettingsService equivalents * replace callsites for equivalentDomains with DomainSettingsService equivalents and clean up unused AccountSettingsSettings * add migrations for domain settings state * do not use enum for URI match strategy constants and types * add getUrlEquivalentDomains test * PR suggestions/cleanup * refactor getUrlEquivalentDomains to return an observable Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> * update tests * add UriMatchStrategy docs notes * service class renames * use service abstraction at callsites previously using service class directly --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com>
730 lines
26 KiB
TypeScript
730 lines
26 KiB
TypeScript
import { DatePipe } from "@angular/common";
|
|
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
|
import { concatMap, Observable, Subject, takeUntil } from "rxjs";
|
|
|
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
|
import {
|
|
isMember,
|
|
OrganizationService,
|
|
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
|
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
|
import { EventType } from "@bitwarden/common/enums";
|
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
|
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
|
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
|
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
|
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
|
import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view";
|
|
import { DialogService } from "@bitwarden/components";
|
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
|
|
|
@Directive()
|
|
export class AddEditComponent implements OnInit, OnDestroy {
|
|
@Input() cloneMode = false;
|
|
@Input() folderId: string = null;
|
|
@Input() cipherId: string;
|
|
@Input() type: CipherType;
|
|
@Input() collectionIds: string[];
|
|
@Input() organizationId: string = null;
|
|
@Output() onSavedCipher = new EventEmitter<CipherView>();
|
|
@Output() onDeletedCipher = new EventEmitter<CipherView>();
|
|
@Output() onRestoredCipher = new EventEmitter<CipherView>();
|
|
@Output() onCancelled = new EventEmitter<CipherView>();
|
|
@Output() onEditAttachments = new EventEmitter<CipherView>();
|
|
@Output() onShareCipher = new EventEmitter<CipherView>();
|
|
@Output() onEditCollections = new EventEmitter<CipherView>();
|
|
@Output() onGeneratePassword = new EventEmitter();
|
|
@Output() onGenerateUsername = new EventEmitter();
|
|
|
|
editMode = false;
|
|
cipher: CipherView;
|
|
folders$: Observable<FolderView[]>;
|
|
collections: CollectionView[] = [];
|
|
title: string;
|
|
formPromise: Promise<any>;
|
|
deletePromise: Promise<any>;
|
|
restorePromise: Promise<any>;
|
|
checkPasswordPromise: Promise<number>;
|
|
showPassword = false;
|
|
showTotpSeed = false;
|
|
showCardNumber = false;
|
|
showCardCode = false;
|
|
cipherType = CipherType;
|
|
typeOptions: any[];
|
|
cardBrandOptions: any[];
|
|
cardExpMonthOptions: any[];
|
|
identityTitleOptions: any[];
|
|
uriMatchOptions: any[];
|
|
ownershipOptions: any[] = [];
|
|
autofillOnPageLoadOptions: any[];
|
|
currentDate = new Date();
|
|
allowPersonal = true;
|
|
reprompt = false;
|
|
canUseReprompt = true;
|
|
organization: Organization;
|
|
|
|
protected componentName = "";
|
|
protected destroy$ = new Subject<void>();
|
|
protected writeableCollections: CollectionView[];
|
|
private personalOwnershipPolicyAppliesToActiveUser: boolean;
|
|
private previousCipherId: string;
|
|
|
|
protected flexibleCollectionsV1Enabled = false;
|
|
|
|
get fido2CredentialCreationDateValue(): string {
|
|
const dateCreated = this.i18nService.t("dateCreated");
|
|
const creationDate = this.datePipe.transform(
|
|
this.cipher?.login?.fido2Credentials?.[0]?.creationDate,
|
|
"short",
|
|
);
|
|
return `${dateCreated} ${creationDate}`;
|
|
}
|
|
|
|
constructor(
|
|
protected cipherService: CipherService,
|
|
protected folderService: FolderService,
|
|
protected i18nService: I18nService,
|
|
protected platformUtilsService: PlatformUtilsService,
|
|
protected auditService: AuditService,
|
|
protected stateService: StateService,
|
|
protected collectionService: CollectionService,
|
|
protected messagingService: MessagingService,
|
|
protected eventCollectionService: EventCollectionService,
|
|
protected policyService: PolicyService,
|
|
protected logService: LogService,
|
|
protected passwordRepromptService: PasswordRepromptService,
|
|
private organizationService: OrganizationService,
|
|
protected sendApiService: SendApiService,
|
|
protected dialogService: DialogService,
|
|
protected win: Window,
|
|
protected datePipe: DatePipe,
|
|
protected configService: ConfigServiceAbstraction,
|
|
) {
|
|
this.typeOptions = [
|
|
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
|
{ name: i18nService.t("typeCard"), value: CipherType.Card },
|
|
{ name: i18nService.t("typeIdentity"), value: CipherType.Identity },
|
|
{ name: i18nService.t("typeSecureNote"), value: CipherType.SecureNote },
|
|
];
|
|
this.cardBrandOptions = [
|
|
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
|
{ name: "Visa", value: "Visa" },
|
|
{ name: "Mastercard", value: "Mastercard" },
|
|
{ name: "American Express", value: "Amex" },
|
|
{ name: "Discover", value: "Discover" },
|
|
{ name: "Diners Club", value: "Diners Club" },
|
|
{ name: "JCB", value: "JCB" },
|
|
{ name: "Maestro", value: "Maestro" },
|
|
{ name: "UnionPay", value: "UnionPay" },
|
|
{ name: "RuPay", value: "RuPay" },
|
|
{ name: i18nService.t("other"), value: "Other" },
|
|
];
|
|
this.cardExpMonthOptions = [
|
|
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
|
{ name: "01 - " + i18nService.t("january"), value: "1" },
|
|
{ name: "02 - " + i18nService.t("february"), value: "2" },
|
|
{ name: "03 - " + i18nService.t("march"), value: "3" },
|
|
{ name: "04 - " + i18nService.t("april"), value: "4" },
|
|
{ name: "05 - " + i18nService.t("may"), value: "5" },
|
|
{ name: "06 - " + i18nService.t("june"), value: "6" },
|
|
{ name: "07 - " + i18nService.t("july"), value: "7" },
|
|
{ name: "08 - " + i18nService.t("august"), value: "8" },
|
|
{ name: "09 - " + i18nService.t("september"), value: "9" },
|
|
{ name: "10 - " + i18nService.t("october"), value: "10" },
|
|
{ name: "11 - " + i18nService.t("november"), value: "11" },
|
|
{ name: "12 - " + i18nService.t("december"), value: "12" },
|
|
];
|
|
this.identityTitleOptions = [
|
|
{ name: "-- " + i18nService.t("select") + " --", value: null },
|
|
{ name: i18nService.t("mr"), value: i18nService.t("mr") },
|
|
{ name: i18nService.t("mrs"), value: i18nService.t("mrs") },
|
|
{ name: i18nService.t("ms"), value: i18nService.t("ms") },
|
|
{ name: i18nService.t("mx"), value: i18nService.t("mx") },
|
|
{ name: i18nService.t("dr"), value: i18nService.t("dr") },
|
|
];
|
|
this.uriMatchOptions = [
|
|
{ name: i18nService.t("defaultMatchDetection"), value: null },
|
|
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
|
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
|
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
|
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
|
{ name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
|
|
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
|
|
];
|
|
this.autofillOnPageLoadOptions = [
|
|
{ name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null },
|
|
{ name: i18nService.t("autoFillOnPageLoadYes"), value: true },
|
|
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false },
|
|
];
|
|
}
|
|
|
|
async ngOnInit() {
|
|
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
|
FeatureFlag.FlexibleCollectionsV1,
|
|
false,
|
|
);
|
|
this.writeableCollections = await this.loadCollections();
|
|
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
|
|
|
this.policyService
|
|
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
|
.pipe(
|
|
concatMap(async (policyAppliesToActiveUser) => {
|
|
this.personalOwnershipPolicyAppliesToActiveUser = policyAppliesToActiveUser;
|
|
await this.init();
|
|
}),
|
|
takeUntil(this.destroy$),
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
async init() {
|
|
if (this.ownershipOptions.length) {
|
|
this.ownershipOptions = [];
|
|
}
|
|
if (this.personalOwnershipPolicyAppliesToActiveUser) {
|
|
this.allowPersonal = false;
|
|
} else {
|
|
const myEmail = await this.stateService.getEmail();
|
|
this.ownershipOptions.push({ name: myEmail, value: null });
|
|
}
|
|
|
|
const orgs = await this.organizationService.getAll();
|
|
orgs
|
|
.filter(isMember)
|
|
.sort(Utils.getSortFunction(this.i18nService, "name"))
|
|
.forEach((o) => {
|
|
if (o.enabled && o.status === OrganizationUserStatusType.Confirmed) {
|
|
this.ownershipOptions.push({ name: o.name, value: o.id });
|
|
}
|
|
});
|
|
if (!this.allowPersonal && this.organizationId == undefined) {
|
|
this.organizationId = this.defaultOwnerId;
|
|
}
|
|
}
|
|
|
|
async load() {
|
|
this.editMode = this.cipherId != null;
|
|
if (this.editMode) {
|
|
this.editMode = true;
|
|
if (this.cloneMode) {
|
|
this.cloneMode = true;
|
|
this.title = this.i18nService.t("addItem");
|
|
} else {
|
|
this.title = this.i18nService.t("editItem");
|
|
}
|
|
} else {
|
|
this.title = this.i18nService.t("addItem");
|
|
}
|
|
|
|
const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo();
|
|
|
|
if (this.cipher == null) {
|
|
if (this.editMode) {
|
|
const cipher = await this.loadCipher();
|
|
this.cipher = await cipher.decrypt(
|
|
await this.cipherService.getKeyForCipherKeyDecryption(cipher),
|
|
);
|
|
|
|
// Adjust Cipher Name if Cloning
|
|
if (this.cloneMode) {
|
|
this.cipher.name += " - " + this.i18nService.t("clone");
|
|
// If not allowing personal ownership, update cipher's org Id to prompt downstream changes
|
|
if (this.cipher.organizationId == null && !this.allowPersonal) {
|
|
this.cipher.organizationId = this.organizationId;
|
|
}
|
|
}
|
|
} else {
|
|
this.cipher = new CipherView();
|
|
this.cipher.organizationId = this.organizationId == null ? null : this.organizationId;
|
|
this.cipher.folderId = this.folderId;
|
|
this.cipher.type = this.type == null ? CipherType.Login : this.type;
|
|
this.cipher.login = new LoginView();
|
|
this.cipher.login.uris = [new LoginUriView()];
|
|
this.cipher.card = new CardView();
|
|
this.cipher.identity = new IdentityView();
|
|
this.cipher.secureNote = new SecureNoteView();
|
|
this.cipher.secureNote.type = SecureNoteType.Generic;
|
|
this.cipher.reprompt = CipherRepromptType.None;
|
|
}
|
|
}
|
|
|
|
if (this.cipher != null && (!this.editMode || loadedAddEditCipherInfo || this.cloneMode)) {
|
|
await this.organizationChanged();
|
|
if (
|
|
this.collectionIds != null &&
|
|
this.collectionIds.length > 0 &&
|
|
this.collections.length > 0
|
|
) {
|
|
this.collections.forEach((c) => {
|
|
if (this.collectionIds.indexOf(c.id) > -1) {
|
|
(c as any).checked = true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// We don't want to copy passkeys when we clone a cipher
|
|
if (this.cloneMode && this.cipher?.login?.hasFido2Credentials) {
|
|
this.cipher.login.fido2Credentials = null;
|
|
}
|
|
|
|
this.folders$ = this.folderService.folderViews$;
|
|
|
|
if (this.editMode && this.previousCipherId !== this.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.eventCollectionService.collect(EventType.Cipher_ClientViewed, this.cipherId);
|
|
}
|
|
this.previousCipherId = this.cipherId;
|
|
this.reprompt = this.cipher.reprompt !== CipherRepromptType.None;
|
|
if (this.reprompt) {
|
|
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
|
|
}
|
|
}
|
|
|
|
async submit(): Promise<boolean> {
|
|
if (this.cipher.isDeleted) {
|
|
return this.restore();
|
|
}
|
|
|
|
if (this.cipher.name == null || this.cipher.name === "") {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("errorOccurred"),
|
|
this.i18nService.t("nameRequired"),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
(!this.editMode || this.cloneMode) &&
|
|
!this.allowPersonal &&
|
|
this.cipher.organizationId == null
|
|
) {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("errorOccurred"),
|
|
this.i18nService.t("personalOwnershipSubmitError"),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
(!this.editMode || this.cloneMode) &&
|
|
this.cipher.type === CipherType.Login &&
|
|
this.cipher.login.uris != null &&
|
|
this.cipher.login.uris.length === 1 &&
|
|
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
|
|
) {
|
|
this.cipher.login.uris = [];
|
|
}
|
|
|
|
// Allows saving of selected collections during "Add" and "Clone" flows
|
|
if ((!this.editMode || this.cloneMode) && this.cipher.organizationId != null) {
|
|
this.cipher.collectionIds =
|
|
this.collections == null
|
|
? []
|
|
: this.collections.filter((c) => (c as any).checked).map((c) => c.id);
|
|
}
|
|
|
|
// Clear current Cipher Id if exists to trigger "Add" cipher flow
|
|
if (this.cloneMode) {
|
|
this.cipher.id = null;
|
|
}
|
|
|
|
const cipher = await this.encryptCipher();
|
|
try {
|
|
this.formPromise = this.saveCipher(cipher);
|
|
await this.formPromise;
|
|
this.cipher.id = cipher.id;
|
|
this.platformUtilsService.showToast(
|
|
"success",
|
|
null,
|
|
this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"),
|
|
);
|
|
this.onSavedCipher.emit(this.cipher);
|
|
this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher");
|
|
return true;
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
addUri() {
|
|
if (this.cipher.type !== CipherType.Login) {
|
|
return;
|
|
}
|
|
|
|
if (this.cipher.login.uris == null) {
|
|
this.cipher.login.uris = [];
|
|
}
|
|
|
|
this.cipher.login.uris.push(new LoginUriView());
|
|
}
|
|
|
|
removeUri(uri: LoginUriView) {
|
|
if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) {
|
|
return;
|
|
}
|
|
|
|
const i = this.cipher.login.uris.indexOf(uri);
|
|
if (i > -1) {
|
|
this.cipher.login.uris.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
onCardNumberChange(): void {
|
|
this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number);
|
|
}
|
|
|
|
getCardExpMonthDisplay() {
|
|
return this.cardExpMonthOptions.find((x) => x.value == this.cipher.card.expMonth)?.name;
|
|
}
|
|
|
|
trackByFunction(index: number, item: any) {
|
|
return index;
|
|
}
|
|
|
|
cancel() {
|
|
this.onCancelled.emit(this.cipher);
|
|
}
|
|
|
|
attachments() {
|
|
this.onEditAttachments.emit(this.cipher);
|
|
}
|
|
|
|
share() {
|
|
this.onShareCipher.emit(this.cipher);
|
|
}
|
|
|
|
editCollections() {
|
|
this.onEditCollections.emit(this.cipher);
|
|
}
|
|
|
|
async delete(): Promise<boolean> {
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "deleteItem" },
|
|
content: {
|
|
key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation",
|
|
},
|
|
type: "warning",
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
this.deletePromise = this.deleteCipher();
|
|
await this.deletePromise;
|
|
this.platformUtilsService.showToast(
|
|
"success",
|
|
null,
|
|
this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"),
|
|
);
|
|
this.onDeletedCipher.emit(this.cipher);
|
|
this.messagingService.send(
|
|
this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher",
|
|
);
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async restore(): Promise<boolean> {
|
|
if (!this.cipher.isDeleted) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
this.restorePromise = this.restoreCipher();
|
|
await this.restorePromise;
|
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem"));
|
|
this.onRestoredCipher.emit(this.cipher);
|
|
this.messagingService.send("restoredCipher");
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async generateUsername(): Promise<boolean> {
|
|
if (this.cipher.login?.username?.length) {
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "overwriteUsername" },
|
|
content: { key: "overwriteUsernameConfirmation" },
|
|
type: "warning",
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.onGenerateUsername.emit();
|
|
return true;
|
|
}
|
|
|
|
async generatePassword(): Promise<boolean> {
|
|
if (this.cipher.login?.password?.length) {
|
|
const confirmed = await this.dialogService.openSimpleDialog({
|
|
title: { key: "overwritePassword" },
|
|
content: { key: "overwritePasswordConfirmation" },
|
|
type: "warning",
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
this.onGeneratePassword.emit();
|
|
return true;
|
|
}
|
|
|
|
togglePassword() {
|
|
this.showPassword = !this.showPassword;
|
|
|
|
if (this.editMode && this.showPassword) {
|
|
document.getElementById("loginPassword")?.focus();
|
|
|
|
// 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_ClientToggledPasswordVisible,
|
|
this.cipherId,
|
|
);
|
|
}
|
|
}
|
|
|
|
toggleTotpSeed() {
|
|
this.showTotpSeed = !this.showTotpSeed;
|
|
|
|
if (this.editMode && this.showTotpSeed) {
|
|
document.getElementById("loginTotp")?.focus();
|
|
|
|
// 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_ClientToggledTOTPSeedVisible,
|
|
this.cipherId,
|
|
);
|
|
}
|
|
}
|
|
|
|
async toggleCardNumber() {
|
|
this.showCardNumber = !this.showCardNumber;
|
|
if (this.showCardNumber) {
|
|
// 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_ClientToggledCardNumberVisible,
|
|
this.cipherId,
|
|
);
|
|
}
|
|
}
|
|
|
|
toggleCardCode() {
|
|
this.showCardCode = !this.showCardCode;
|
|
document.getElementById("cardCode").focus();
|
|
if (this.editMode && this.showCardCode) {
|
|
// 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_ClientToggledCardCodeVisible,
|
|
this.cipherId,
|
|
);
|
|
}
|
|
}
|
|
|
|
toggleUriOptions(uri: LoginUriView) {
|
|
const u = uri as any;
|
|
u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions;
|
|
}
|
|
|
|
loginUriMatchChanged(uri: LoginUriView) {
|
|
const u = uri as any;
|
|
u.showOptions = u.showOptions == null ? true : u.showOptions;
|
|
}
|
|
|
|
async organizationChanged() {
|
|
if (this.writeableCollections != null) {
|
|
this.writeableCollections.forEach((c) => ((c as any).checked = false));
|
|
}
|
|
if (this.cipher.organizationId != null) {
|
|
this.collections = this.writeableCollections.filter(
|
|
(c) => c.organizationId === this.cipher.organizationId,
|
|
);
|
|
const org = await this.organizationService.get(this.cipher.organizationId);
|
|
if (org != null) {
|
|
this.cipher.organizationUseTotp = org.useTotp;
|
|
}
|
|
} else {
|
|
this.collections = [];
|
|
}
|
|
}
|
|
|
|
async checkPassword() {
|
|
if (this.checkPasswordPromise != null) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this.cipher.login == null ||
|
|
this.cipher.login.password == null ||
|
|
this.cipher.login.password === ""
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password);
|
|
const matches = await this.checkPasswordPromise;
|
|
this.checkPasswordPromise = null;
|
|
|
|
if (matches > 0) {
|
|
this.platformUtilsService.showToast(
|
|
"warning",
|
|
null,
|
|
this.i18nService.t("passwordExposed", matches.toString()),
|
|
);
|
|
} else {
|
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe"));
|
|
}
|
|
}
|
|
|
|
repromptChanged() {
|
|
this.reprompt = !this.reprompt;
|
|
if (this.reprompt) {
|
|
this.cipher.reprompt = CipherRepromptType.Password;
|
|
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[2].value;
|
|
} else {
|
|
this.cipher.reprompt = CipherRepromptType.None;
|
|
this.cipher.login.autofillOnPageLoad = this.autofillOnPageLoadOptions[0].value;
|
|
}
|
|
}
|
|
|
|
protected async loadCollections() {
|
|
const allCollections = await this.collectionService.getAllDecrypted();
|
|
return allCollections.filter((c) => !c.readOnly);
|
|
}
|
|
|
|
protected loadCipher() {
|
|
return this.cipherService.get(this.cipherId);
|
|
}
|
|
|
|
protected encryptCipher() {
|
|
return this.cipherService.encrypt(this.cipher);
|
|
}
|
|
|
|
protected saveCipher(cipher: Cipher) {
|
|
const isNotClone = this.editMode && !this.cloneMode;
|
|
let orgAdmin = this.organization?.isAdmin;
|
|
|
|
if (this.flexibleCollectionsV1Enabled) {
|
|
// Flexible Collections V1 restricts admins, check the organization setting via canEditAllCiphers
|
|
orgAdmin = this.organization?.canEditAllCiphers(true);
|
|
}
|
|
|
|
return this.cipher.id == null
|
|
? this.cipherService.createWithServer(cipher, orgAdmin)
|
|
: this.cipherService.updateWithServer(cipher, orgAdmin, isNotClone);
|
|
}
|
|
|
|
protected deleteCipher() {
|
|
const asAdmin = this.organization?.canEditAnyCollection;
|
|
return this.cipher.isDeleted
|
|
? this.cipherService.deleteWithServer(this.cipher.id, asAdmin)
|
|
: this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
|
|
}
|
|
|
|
protected restoreCipher() {
|
|
const asAdmin = this.organization?.canEditAnyCollection;
|
|
return this.cipherService.restoreWithServer(this.cipher.id, asAdmin);
|
|
}
|
|
|
|
get defaultOwnerId(): string | null {
|
|
return this.ownershipOptions[0].value;
|
|
}
|
|
|
|
async loadAddEditCipherInfo(): Promise<boolean> {
|
|
const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo();
|
|
const loadedSavedInfo = addEditCipherInfo != null;
|
|
|
|
if (loadedSavedInfo) {
|
|
this.cipher = addEditCipherInfo.cipher;
|
|
this.collectionIds = addEditCipherInfo.collectionIds;
|
|
|
|
if (!this.editMode && !this.allowPersonal && this.cipher.organizationId == null) {
|
|
// This is a new cipher and personal ownership isn't allowed, so we need to set the default owner
|
|
this.cipher.organizationId = this.defaultOwnerId;
|
|
}
|
|
}
|
|
|
|
await this.stateService.setAddEditCipherInfo(null);
|
|
|
|
return loadedSavedInfo;
|
|
}
|
|
|
|
async copy(value: string, typeI18nKey: string, aType: string): Promise<boolean> {
|
|
if (value == null) {
|
|
return false;
|
|
}
|
|
|
|
const copyOptions = this.win != null ? { window: this.win } : null;
|
|
this.platformUtilsService.copyToClipboard(value, copyOptions);
|
|
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, this.cipherId);
|
|
} 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, this.cipherId);
|
|
} else if (aType === "H_Field") {
|
|
// 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, this.cipherId);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|