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": {
|
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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