1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 00:03:56 +00:00

[PM-8524] Introduce login details section component

This commit is contained in:
Shane Melton
2024-07-09 15:13:42 -07:00
parent 9dda29fb9c
commit faeda21021
7 changed files with 261 additions and 0 deletions

View File

@@ -636,6 +636,15 @@
"totpCapture": { "totpCapture": {
"message": "Scan authenticator QR code from current webpage" "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": { "copyTOTP": {
"message": "Copy Authenticator key (TOTP)" "message": "Copy Authenticator key (TOTP)"
}, },
@@ -3625,6 +3634,12 @@
} }
} }
}, },
"loginCredentials": {
"message": "Login credentials"
},
"authenticatorKey": {
"message": "Authenticator key"
},
"cardDetails": { "cardDetails": {
"message": "Card details" "message": "Card details"
}, },

View File

@@ -42,6 +42,12 @@
"cardholderName": { "cardholderName": {
"message": "Cardholder name" "message": "Cardholder name"
}, },
"loginCredentials": {
"message": "Login credentials"
},
"authenticatorKey": {
"message": "Authenticator key"
},
"number": { "number": {
"message": "Number" "message": "Number"
}, },
@@ -138,6 +144,15 @@
"authenticatorKeyTotp": { "authenticatorKeyTotp": {
"message": "Authenticator key (TOTP)" "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": { "folder": {
"message": "Folder" "message": "Folder"
}, },

View File

@@ -5,6 +5,7 @@ import { AdditionalOptionsSectionComponent } from "./components/additional-optio
import { CardDetailsSectionComponent } from "./components/card-details-section/card-details-section.component"; import { CardDetailsSectionComponent } from "./components/card-details-section/card-details-section.component";
import { IdentitySectionComponent } from "./components/identity/identity.component"; import { IdentitySectionComponent } from "./components/identity/identity.component";
import { ItemDetailsSectionComponent } from "./components/item-details/item-details-section.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. * 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 = { export type CipherForm = {
itemDetails?: ItemDetailsSectionComponent["itemDetailsForm"]; itemDetails?: ItemDetailsSectionComponent["itemDetailsForm"];
additionalOptions?: AdditionalOptionsSectionComponent["additionalOptionsForm"]; additionalOptions?: AdditionalOptionsSectionComponent["additionalOptionsForm"];
loginDetails?: LoginDetailsSectionComponent["loginDetailsForm"];
cardDetails?: CardDetailsSectionComponent["cardDetailsForm"]; cardDetails?: CardDetailsSectionComponent["cardDetailsForm"];
identityDetails?: IdentitySectionComponent["identityForm"]; identityDetails?: IdentitySectionComponent["identityForm"];
}; };

View File

@@ -6,6 +6,10 @@
[originalCipherView]="originalCipherView" [originalCipherView]="originalCipherView"
></vault-item-details-section> ></vault-item-details-section>
<vault-login-details-section
*ngIf="config.cipherType === CipherType.Login"
></vault-login-details-section>
<vault-identity-section <vault-identity-section
*ngIf="config.cipherType === CipherType.Identity" *ngIf="config.cipherType === CipherType.Identity"
[disabled]="config.mode === 'partial-edit'" [disabled]="config.mode === 'partial-edit'"

View File

@@ -39,6 +39,7 @@ import { AdditionalOptionsSectionComponent } from "./additional-options/addition
import { CardDetailsSectionComponent } from "./card-details-section/card-details-section.component"; import { CardDetailsSectionComponent } from "./card-details-section/card-details-section.component";
import { IdentitySectionComponent } from "./identity/identity.component"; import { IdentitySectionComponent } from "./identity/identity.component";
import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component"; import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component";
import { LoginDetailsSectionComponent } from "./login-details-section/login-details-section.component";
@Component({ @Component({
selector: "vault-cipher-form", selector: "vault-cipher-form",
@@ -64,6 +65,7 @@ import { ItemDetailsSectionComponent } from "./item-details/item-details-section
IdentitySectionComponent, IdentitySectionComponent,
NgIf, NgIf,
AdditionalOptionsSectionComponent, AdditionalOptionsSectionComponent,
LoginDetailsSectionComponent,
], ],
}) })
export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer { export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer {

View File

@@ -0,0 +1,76 @@
<bit-section [formGroup]="loginDetailsForm">
<bit-section-header>
<h2 bitTypography="h5">
{{ "loginCredentials" | i18n }}
</h2>
</bit-section-header>
<bit-card>
<bit-form-field>
<bit-label>{{ "username" | i18n }}</bit-label>
<input bitInput formControlName="username" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "password" | i18n }}</bit-label>
<input bitInput formControlName="password" type="password" #passwordInput />
<button
type="button"
bitIconButton
bitSuffix
*ngIf="viewHiddenFields"
data-testid="toggle-password-visibility"
bitPasswordInputToggle
[passwordInput]="passwordInput"
></button>
</bit-form-field>
<bit-form-field *ngIf="hasPasskey">
<bit-label>{{ "typePasskey" | i18n }}</bit-label>
<input bitInput disabled [value]="fido2CredentialCreationDateValue" />
<button
type="button"
bitIconButton="bwi-minus-circle"
buttonType="danger"
bitSuffix
*ngIf="loginDetailsForm.enabled"
[bitAction]="removePasskey"
data-testid="remove-passkey-button"
[appA11yTitle]="'removePasskey' | i18n"
></button>
</bit-form-field>
<bit-form-field>
<bit-label>
{{ "authenticatorKey" | i18n }}
<button
bitIconButton="bwi-question-circle"
type="button"
size="small"
[bitPopoverTriggerFor]="totpPopover"
></button>
<bit-popover #totpPopover [title]="'totpHelperTitle' | i18n">
<p>{{ (canCaptureTotp ? "totpHelperWithCapture" : "totpHelper") | i18n }}</p>
</bit-popover>
</bit-label>
<input bitInput formControlName="totp" type="password" #totpInput />
<button
type="button"
bitIconButton
bitSuffix
*ngIf="viewHiddenFields"
data-testid="toggle-totp-visibility"
bitPasswordInputToggle
[passwordInput]="totpInput"
></button>
<button
type="button"
bitIconButton="bwi-camera"
bitSuffix
*ngIf="canCaptureTotp"
[bitAction]="captureTotpFromTab"
[appA11yTitle]="'totpCapture' | i18n"
></button>
</bit-form-field>
</bit-card>
</bit-section>

View File

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