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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"];
|
||||
};
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
[originalCipherView]="originalCipherView"
|
||||
></vault-item-details-section>
|
||||
|
||||
<vault-login-details-section
|
||||
*ngIf="config.cipherType === CipherType.Login"
|
||||
></vault-login-details-section>
|
||||
|
||||
<vault-identity-section
|
||||
*ngIf="config.cipherType === CipherType.Identity"
|
||||
[disabled]="config.mode === 'partial-edit'"
|
||||
|
||||
@@ -39,6 +39,7 @@ import { AdditionalOptionsSectionComponent } from "./additional-options/addition
|
||||
import { CardDetailsSectionComponent } from "./card-details-section/card-details-section.component";
|
||||
import { IdentitySectionComponent } from "./identity/identity.component";
|
||||
import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component";
|
||||
import { LoginDetailsSectionComponent } from "./login-details-section/login-details-section.component";
|
||||
|
||||
@Component({
|
||||
selector: "vault-cipher-form",
|
||||
@@ -64,6 +65,7 @@ import { ItemDetailsSectionComponent } from "./item-details/item-details-section
|
||||
IdentitySectionComponent,
|
||||
NgIf,
|
||||
AdditionalOptionsSectionComponent,
|
||||
LoginDetailsSectionComponent,
|
||||
],
|
||||
})
|
||||
export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, CipherFormContainer {
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user