mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
* don't display totp capture when in popout * add canCaptureTotp method * dry up logic * add unit tests * fix failing tests * add missing mock to cipher-form story
277 lines
8.4 KiB
TypeScript
277 lines
8.4 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
import { DatePipe, NgIf } from "@angular/common";
|
|
import { Component, inject, OnInit, Optional } from "@angular/core";
|
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
|
import { map } from "rxjs";
|
|
|
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
|
import { EventType } from "@bitwarden/common/enums";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
|
|
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
|
import {
|
|
AsyncActionsModule,
|
|
CardComponent,
|
|
FormFieldModule,
|
|
IconButtonModule,
|
|
LinkModule,
|
|
PopoverModule,
|
|
SectionComponent,
|
|
SectionHeaderComponent,
|
|
ToastService,
|
|
TypographyModule,
|
|
} from "@bitwarden/components";
|
|
|
|
import { CipherFormGenerationService } from "../../abstractions/cipher-form-generation.service";
|
|
import { TotpCaptureService } from "../../abstractions/totp-capture.service";
|
|
import { CipherFormContainer } from "../../cipher-form-container";
|
|
import { AutofillOptionsComponent } from "../autofill-options/autofill-options.component";
|
|
|
|
@Component({
|
|
selector: "vault-login-details-section",
|
|
templateUrl: "./login-details-section.component.html",
|
|
standalone: true,
|
|
imports: [
|
|
SectionComponent,
|
|
ReactiveFormsModule,
|
|
SectionHeaderComponent,
|
|
TypographyModule,
|
|
JslibModule,
|
|
CardComponent,
|
|
FormFieldModule,
|
|
IconButtonModule,
|
|
AsyncActionsModule,
|
|
NgIf,
|
|
PopoverModule,
|
|
AutofillOptionsComponent,
|
|
LinkModule,
|
|
],
|
|
})
|
|
export class LoginDetailsSectionComponent implements OnInit {
|
|
EventType = EventType;
|
|
loginDetailsForm = this.formBuilder.group({
|
|
username: [""],
|
|
password: [""],
|
|
totp: [""],
|
|
});
|
|
|
|
/**
|
|
* Flag indicating whether a new password has been generated for the current form.
|
|
*/
|
|
newPasswordGenerated: boolean;
|
|
|
|
/**
|
|
* Whether the TOTP field can be captured from the current tab. Only available in the browser extension and
|
|
* when not in a popout window.
|
|
*/
|
|
get canCaptureTotp() {
|
|
return (
|
|
!!this.totpCaptureService?.canCaptureTotp(window) &&
|
|
this.loginDetailsForm.controls.totp.enabled
|
|
);
|
|
}
|
|
|
|
private datePipe = inject(DatePipe);
|
|
|
|
/**
|
|
* A local reference to the Fido2 credentials for an existing login being edited.
|
|
* These cannot be created in the form and thus have no form control.
|
|
* @private
|
|
*/
|
|
private existingFido2Credentials?: Fido2CredentialView[];
|
|
|
|
get hasPasskey(): boolean {
|
|
return this.existingFido2Credentials != null && this.existingFido2Credentials.length > 0;
|
|
}
|
|
|
|
get fido2CredentialCreationDateValue(): string {
|
|
const dateCreated = this.i18nService.t("dateCreated");
|
|
const creationDate = this.datePipe.transform(
|
|
this.existingFido2Credentials?.[0]?.creationDate,
|
|
"short",
|
|
);
|
|
return `${dateCreated} ${creationDate}`;
|
|
}
|
|
|
|
get viewHiddenFields() {
|
|
if (this.cipherFormContainer.originalCipherView) {
|
|
return this.cipherFormContainer.originalCipherView.viewPassword;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
get initialValues() {
|
|
return this.cipherFormContainer.config.initialValues;
|
|
}
|
|
|
|
constructor(
|
|
private cipherFormContainer: CipherFormContainer,
|
|
private formBuilder: FormBuilder,
|
|
private i18nService: I18nService,
|
|
private generationService: CipherFormGenerationService,
|
|
private auditService: AuditService,
|
|
private toastService: ToastService,
|
|
private eventCollectionService: EventCollectionService,
|
|
@Optional() private totpCaptureService?: TotpCaptureService,
|
|
) {
|
|
this.cipherFormContainer.registerChildForm("loginDetails", this.loginDetailsForm);
|
|
|
|
this.loginDetailsForm.valueChanges
|
|
.pipe(
|
|
takeUntilDestroyed(),
|
|
// getRawValue() is used as fields can be disabled when passwords are hidden
|
|
map(() => this.loginDetailsForm.getRawValue()),
|
|
)
|
|
.subscribe((value) => {
|
|
this.cipherFormContainer.patchCipher((cipher) => {
|
|
Object.assign(cipher.login, {
|
|
username: value.username,
|
|
password: value.password,
|
|
totp: value.totp?.trim(),
|
|
} as LoginView);
|
|
|
|
return cipher;
|
|
});
|
|
});
|
|
}
|
|
|
|
async ngOnInit() {
|
|
if (this.cipherFormContainer.originalCipherView?.login) {
|
|
this.initFromExistingCipher(this.cipherFormContainer.originalCipherView.login);
|
|
} else {
|
|
await this.initNewCipher();
|
|
}
|
|
|
|
if (this.cipherFormContainer.config.mode === "partial-edit") {
|
|
this.loginDetailsForm.disable();
|
|
}
|
|
}
|
|
|
|
private initFromExistingCipher(existingLogin: LoginView) {
|
|
this.loginDetailsForm.patchValue({
|
|
username: this.initialValues?.username ?? existingLogin.username,
|
|
password: this.initialValues?.password ?? existingLogin.password,
|
|
totp: existingLogin.totp,
|
|
});
|
|
|
|
this.existingFido2Credentials = existingLogin.fido2Credentials;
|
|
|
|
if (!this.viewHiddenFields) {
|
|
this.loginDetailsForm.controls.password.disable();
|
|
this.loginDetailsForm.controls.totp.disable();
|
|
}
|
|
}
|
|
|
|
private async initNewCipher() {
|
|
this.loginDetailsForm.patchValue({
|
|
username: this.initialValues?.username || "",
|
|
password: this.initialValues?.password || "",
|
|
});
|
|
}
|
|
|
|
/** Logs the givin event when in edit mode */
|
|
logVisibleEvent = async (passwordVisible: boolean, event: EventType) => {
|
|
const { mode, originalCipher } = this.cipherFormContainer.config;
|
|
|
|
const isEdit = ["edit", "partial-edit"].includes(mode);
|
|
|
|
if (!passwordVisible || !isEdit || !originalCipher) {
|
|
return;
|
|
}
|
|
|
|
await this.eventCollectionService.collect(
|
|
event,
|
|
originalCipher.id,
|
|
false,
|
|
originalCipher.organizationId,
|
|
);
|
|
};
|
|
|
|
captureTotp = async () => {
|
|
if (!this.canCaptureTotp) {
|
|
return;
|
|
}
|
|
try {
|
|
const totp = await this.totpCaptureService.captureTotpSecret();
|
|
if (totp) {
|
|
this.loginDetailsForm.controls.totp.patchValue(totp);
|
|
this.toastService.showToast({
|
|
variant: "success",
|
|
title: null,
|
|
message: this.i18nService.t("totpCaptureSuccess"),
|
|
});
|
|
}
|
|
} catch {
|
|
this.toastService.showToast({
|
|
variant: "error",
|
|
title: this.i18nService.t("errorOccurred"),
|
|
message: this.i18nService.t("totpCaptureError"),
|
|
});
|
|
}
|
|
};
|
|
|
|
removePasskey = async () => {
|
|
// Fido2Credentials do not have a form control, so update directly
|
|
this.existingFido2Credentials = null;
|
|
this.cipherFormContainer.patchCipher((cipher) => {
|
|
cipher.login.fido2Credentials = null;
|
|
return cipher;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Generate a new password and update the form.
|
|
* TODO: Browser extension needs a means to cache the current form so values are not lost upon navigating to the generator.
|
|
*/
|
|
generatePassword = async () => {
|
|
const newPassword = await this.generationService.generatePassword();
|
|
|
|
if (newPassword) {
|
|
this.loginDetailsForm.controls.password.patchValue(newPassword);
|
|
this.newPasswordGenerated = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generate a new username and update the form.
|
|
* TODO: Browser extension needs a means to cache the current form so values are not lost upon navigating to the generator.
|
|
*/
|
|
generateUsername = async () => {
|
|
const newUsername = await this.generationService.generateUsername();
|
|
if (newUsername) {
|
|
this.loginDetailsForm.controls.username.patchValue(newUsername);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Checks if the password has been exposed in a data breach using the AuditService.
|
|
*/
|
|
checkPassword = async () => {
|
|
const password = this.loginDetailsForm.controls.password.value;
|
|
|
|
if (password == null || password === "") {
|
|
return;
|
|
}
|
|
|
|
const matches = await this.auditService.passwordLeaked(password);
|
|
|
|
if (matches > 0) {
|
|
this.toastService.showToast({
|
|
variant: "warning",
|
|
title: null,
|
|
message: this.i18nService.t("passwordExposed", matches.toString()),
|
|
});
|
|
} else {
|
|
this.toastService.showToast({
|
|
variant: "success",
|
|
title: null,
|
|
message: this.i18nService.t("passwordSafe"),
|
|
});
|
|
}
|
|
};
|
|
}
|