diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 22e0eba1b4f..6d1b88df2bc 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -636,6 +636,15 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "totpHelperWithCapture": { + "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + }, "copyTOTP": { "message": "Copy Authenticator key (TOTP)" }, @@ -3625,6 +3634,12 @@ } } }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, "cardDetails": { "message": "Card details" }, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index f5875601189..42c9578d69e 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -42,6 +42,12 @@ "cardholderName": { "message": "Cardholder name" }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, "number": { "message": "Number" }, @@ -138,6 +144,15 @@ "authenticatorKeyTotp": { "message": "Authenticator key (TOTP)" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "totpHelperWithCapture": { + "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + }, "folder": { "message": "Folder" }, diff --git a/libs/vault/src/cipher-form/cipher-form-container.ts b/libs/vault/src/cipher-form/cipher-form-container.ts index a002e39d3e0..5b1b9bcf19c 100644 --- a/libs/vault/src/cipher-form/cipher-form-container.ts +++ b/libs/vault/src/cipher-form/cipher-form-container.ts @@ -5,6 +5,7 @@ import { AdditionalOptionsSectionComponent } from "./components/additional-optio import { CardDetailsSectionComponent } from "./components/card-details-section/card-details-section.component"; import { IdentitySectionComponent } from "./components/identity/identity.component"; import { ItemDetailsSectionComponent } from "./components/item-details/item-details-section.component"; +import { LoginDetailsSectionComponent } from "./components/login-details-section/login-details-section.component"; /** * The complete form for a cipher. Includes all the sub-forms from their respective section components. @@ -13,6 +14,7 @@ import { ItemDetailsSectionComponent } from "./components/item-details/item-deta export type CipherForm = { itemDetails?: ItemDetailsSectionComponent["itemDetailsForm"]; additionalOptions?: AdditionalOptionsSectionComponent["additionalOptionsForm"]; + loginDetails?: LoginDetailsSectionComponent["loginDetailsForm"]; cardDetails?: CardDetailsSectionComponent["cardDetailsForm"]; identityDetails?: IdentitySectionComponent["identityForm"]; }; diff --git a/libs/vault/src/cipher-form/components/cipher-form.component.html b/libs/vault/src/cipher-form/components/cipher-form.component.html index 669f3c8b963..60dbd91fc36 100644 --- a/libs/vault/src/cipher-form/components/cipher-form.component.html +++ b/libs/vault/src/cipher-form/components/cipher-form.component.html @@ -6,6 +6,10 @@ [originalCipherView]="originalCipherView" > + + + + + {{ "loginCredentials" | i18n }} + + + + + + {{ "username" | i18n }} + + + + + {{ "password" | i18n }} + + + + + + {{ "typePasskey" | i18n }} + + + + + + + {{ "authenticatorKey" | i18n }} + + + {{ (canCaptureTotp ? "totpHelperWithCapture" : "totpHelper") | i18n }} + + + + + + + + diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts new file mode 100644 index 00000000000..d683587d1d8 --- /dev/null +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -0,0 +1,147 @@ +import { DatePipe, NgIf } from "@angular/common"; +import { Component, inject, OnInit } 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { + AsyncActionsModule, + CardComponent, + FormFieldModule, + IconButtonModule, + PopoverModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, +} from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; + +import { CipherFormContainer } from "../../cipher-form-container"; + +@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, + ], +}) +export class LoginDetailsSectionComponent implements OnInit { + loginDetailsForm = this.formBuilder.group({ + username: [""], + password: [""], + totp: [""], + }); + + /** + * Whether the TOTP field can be captured from the current tab. Only available in the web extension. + * @protected + */ + protected get canCaptureTotp() { + return false; //BrowserApi.isWebExtensionsApi && this.loginDetailsForm.controls.totp.enabled; + } + + private datePipe = inject(DatePipe); + + private loginView: LoginView; + + get hasPasskey(): boolean { + return this.loginView?.hasFido2Credentials; + } + + get fido2CredentialCreationDateValue(): string { + const dateCreated = this.i18nService.t("dateCreated"); + const creationDate = this.datePipe.transform( + this.loginView?.fido2Credentials?.[0]?.creationDate, + "short", + ); + return `${dateCreated} ${creationDate}`; + } + + get viewHiddenFields() { + if (this.cipherFormContainer.originalCipherView) { + return this.cipherFormContainer.originalCipherView.viewPassword; + } + return true; + } + + constructor( + private cipherFormContainer: CipherFormContainer, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private generatorService: PasswordGenerationServiceAbstraction, + ) { + 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) => { + const patchedLogin = Object.assign(this.loginView, { + username: value.username, + password: value.password, + totp: value.totp, + } as LoginView); + + this.cipherFormContainer.patchCipher({ + login: patchedLogin, + }); + }); + } + + 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.loginView = existingLogin; + this.loginDetailsForm.patchValue({ + username: this.loginView.username, + password: this.loginView.password, + totp: this.loginView.totp, + }); + + if (!this.viewHiddenFields) { + this.loginDetailsForm.controls.password.disable(); + this.loginDetailsForm.controls.totp.disable(); + } + } + + private async initNewCipher() { + this.loginView = new LoginView(); + + this.loginDetailsForm.controls.password.patchValue(await this.generateNewPassword()); + } + + captureTotpFromTab = async () => {}; + removePasskey = async () => {}; + + private async generateNewPassword() { + const [options] = await this.generatorService.getOptions(); + return await this.generatorService.generatePassword(options); + } +}
{{ (canCaptureTotp ? "totpHelperWithCapture" : "totpHelper") | i18n }}