1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[SG-168] Passwordless login web MVP (#3424)

* passwordless login page redesign

* passwordless login page redesign

* restyled login form to use tailwind

* restyled login form to use tailwind

* moved texts on login device template to locales

* made reactive form changes for clients

* added request model

* made more changes

* added implmentation to auth request api

* fixed refrencing issue

* renamed model property

* Added resend notification functionality

* Added new file

* login with device first draft

* login with device first draft

* login with device first draft

* login with device first draft

* connection to anonymous hub

* connection to anonymous hub

* refactored confirm login response

* removed comment

* cleaned up login

* changed uptyped form builder

* changed uptyped form builder

* [SG-168] Update login strategy with passwordless login credentials.

* [SG-168] Removed logs. Changed inputs for passwordless logic strategy. Removed tokenRequestPasswordless it is using the same as password.

* code cleanup

* code cleanup

* removed login with device from self hosted

* fixed PR comments

* added module for login

* fixed post request bug

* added feature flag

* added feature flag

* added feature flag

Co-authored-by: André Bispo <abispo@bitwarden.com>
This commit is contained in:
Gbubemi Smith
2022-09-26 23:26:10 +01:00
committed by GitHub
parent debaef2941
commit 22a878792e
38 changed files with 907 additions and 240 deletions

View File

@@ -10,5 +10,7 @@
"port": 8080,
"allowedHosts": "auto"
},
"flags": {}
"flags": {
"showPasswordless": false
}
}

View File

@@ -16,6 +16,7 @@
"proxyEvents": "https://events.bitwarden.com"
},
"flags": {
"showTrial": true
"showTrial": true,
"showPasswordless": false
}
}

View File

@@ -10,6 +10,7 @@
"proxyNotifications": "http://localhost:61840"
},
"flags": {
"showTrial": true
"showTrial": true,
"showPasswordless": true
}
}

View File

@@ -10,6 +10,7 @@
"proxyEvents": "https://events.qa.bitwarden.pw"
},
"flags": {
"showTrial": true
"showTrial": true,
"showPasswordless": true
}
}

View File

@@ -7,6 +7,7 @@
"port": 8081
},
"flags": {
"showTrial": false
"showTrial": false,
"showPasswordless": false
}
}

View File

@@ -1,102 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<img class="mb-2 logo logo-themed" alt="Bitwarden" />
<p class="lead text-center mx-4 mb-4">{{ "loginOrCreateNewAccount" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="showResetPasswordAutoEnrollWarning"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
inputmode="email"
appInputVerbatim="false"
/>
</div>
<div class="form-group">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="text-monospace form-control"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
<small class="form-text">
<a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a>
</small>
</div>
<div class="form-check mb-3">
<input
type="checkbox"
class="form-check-input"
id="rememberEmail"
name="RememberEmail"
[(ngModel)]="rememberEmail"
/>
<label class="form-check-label" for="rememberEmail">{{ "rememberEmail" | i18n }}</label>
</div>
<div class="mb-n3" [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }} </span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
>
<i class="bwi bwi-pencil-square" aria-hidden="true"></i>
{{ "createAccount" | i18n }}
</a>
</div>
<div class="d-flex">
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,44 @@
<div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
>
<div>
<img class="logo logo-themed" alt="Bitwarden" />
<p class="tw-mx-4 tw-mt-3 tw-mb-4 tw-text-center tw-text-xl">
{{ "loginOrCreateNewAccount" | i18n }}
</p>
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 class="tw-mb-6 tw-text-xl tw-font-semibold">{{ "logInInitiated" | i18n }}</h2>
<div class="tw-text-light">
<p class="tw-mb-6">{{ "notificationSentDevice" | i18n }}</p>
<p class="tw-mb-6">
{{ "fingerprintMatchInfo" | i18n }}
</p>
</div>
<div class="tw-mb-6">
<h4 class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</h4>
<p>
<code>{{ passwordlessRequest?.fingerprintPhrase }}</code>
</p>
</div>
<div class="tw-my-10" *ngIf="showResendNotification">
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
"resendNotification" | i18n
}}</a>
</div>
<hr />
<div class="tw-text-light tw-mt-3">
{{ "loginWithDevciceEnabledInfo" | i18n }}
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,175 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { CaptchaProtectedComponent } from "@bitwarden/angular/components/captchaProtected.component";
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { AuthRequestType } from "@bitwarden/common/enums/authRequestType";
import { Utils } from "@bitwarden/common/misc/utils";
import { PasswordlessLogInCredentials } from "@bitwarden/common/models/domain/logInCredentials";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/models/request/passwordlessCreateAuthRequest";
import { AuthRequestResponse } from "@bitwarden/common/models/response/authRequestResponse";
@Component({
selector: "app-login-with-device",
templateUrl: "login-with-device.component.html",
})
export class LoginWithDeviceComponent
extends CaptchaProtectedComponent
implements OnInit, OnDestroy
{
private destroy$ = new Subject<void>();
email: string;
showResendNotification = false;
passwordlessRequest: PasswordlessCreateAuthRequest;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLogin: () => Promise<any>;
onSuccessfulLoginNavigate: () => Promise<any>;
protected twoFactorRoute = "2fa";
protected successRoute = "vault";
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
constructor(
private router: Router,
private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private appIdService: AppIdService,
private passwordGenerationService: PasswordGenerationService,
private apiService: ApiService,
private authService: AuthService,
private logService: LogService,
private stateService: StateService,
environmentService: EnvironmentService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private anonymousHubService: AnonymousHubService
) {
super(environmentService, i18nService, platformUtilsService);
const navigation = this.router.getCurrentNavigation();
if (navigation) {
this.email = navigation.extras?.state?.email;
}
//gets signalR push notification
this.authService
.getPushNotifcationObs$()
.pipe(takeUntil(this.destroy$))
.subscribe((id) => {
this.confirmResponse(id);
});
}
async ngOnInit() {
if (!this.email) {
this.router.navigate(["/login"]);
return;
}
this.startPasswordlessLogin();
}
async startPasswordlessLogin() {
this.showResendNotification = false;
try {
await this.buildAuthRequest();
const reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
if (reqResponse.id) {
this.anonymousHubService.createHubConnection(reqResponse.id);
}
} catch (e) {
this.logService.error(e);
}
setTimeout(() => {
this.showResendNotification = true;
}, 12000);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.anonymousHubService.stopHubConnection();
}
private async confirmResponse(requestId: string) {
try {
const response = await this.apiService.getAuthResponse(
requestId,
this.passwordlessRequest.accessCode
);
if (!response.requestApproved) {
return;
}
const credentials = await this.buildLoginCredntials(requestId, response);
await this.authService.logIn(credentials);
if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin();
}
if (this.onSuccessfulLoginNavigate != null) {
this.onSuccessfulLoginNavigate();
} else {
this.router.navigate([this.successRoute]);
}
} catch (error) {
this.logService.error(error);
}
}
private async buildAuthRequest() {
this.authRequestKeyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const fingerprint = await (
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair[0])
).join("-");
const deviceIdentifier = await this.appIdService.getAppId();
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair[0]);
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
this.email,
deviceIdentifier,
publicKey,
AuthRequestType.AuthenticateAndUnlock,
accessCode,
fingerprint
);
}
private async buildLoginCredntials(
requestId: string,
response: AuthRequestResponse
): Promise<PasswordlessLogInCredentials> {
const decKey = await this.cryptoService.rsaDecrypt(response.key, this.authRequestKeyPair[1]);
const decMasterPasswordHash = await this.cryptoService.rsaDecrypt(
response.masterPasswordHash,
this.authRequestKeyPair[1]
);
const key = new SymmetricCryptoKey(decKey);
const localHashedPassword = Utils.fromBufferToUtf8(decMasterPasswordHash);
return new PasswordlessLogInCredentials(
this.email,
this.passwordlessRequest.accessCode,
requestId,
key,
localHashedPassword
);
}
}

View File

@@ -0,0 +1,121 @@
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
class="tw-container tw-mx-auto"
[formGroup]="formGroup"
>
<div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
>
<div>
<img class="logo logo-themed" alt="Bitwarden" />
<p class="tw-mx-4 tw-mt-3 tw-mb-4 tw-text-center tw-text-xl">
{{ "loginOrCreateNewAccount" | i18n }}
</p>
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<bit-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="showResetPasswordAutoEnrollWarning"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</bit-callout>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
<input id="login_input_email" bitInput type="email" formControlName="email" />
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
id="login_input_master-password"
bitInput
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="masterPassword"
/>
<button type="button" bitSuffix bitButton (click)="togglePassword()">
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<bit-hint>
<a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a>
</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3 tw-flex tw-items-start">
<div class="tw-flex tw-h-6 tw-items-center">
<input
id="login_input_remember-email"
class="tw-w-4 tw-rounded tw-border"
bitInput
type="checkbox"
formControlName="rememberEmail"
/>
</div>
<bit-label class="ml-2">
{{ "rememberEmail" | i18n }}
</bit-label>
</div>
<hr />
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="tw-mb-3 tw-flex tw-space-x-4">
<button
bitButton
buttonType="primary"
type="submit"
class="tw-inline-block tw-w-1/2"
[disabled]="form.loading"
>
<span> <i class="bwi bwi-sign-in"></i> {{ "logIn" | i18n }} </span>
</button>
<a
bitButton
buttonType="secondary"
routerLink="/register"
class="tw-inline-block tw-w-1/2"
>
<i class="bwi bwi-pencil-square"></i>
{{ "createAccount" | i18n }}
</a>
</div>
<div class="tw-mb-3" *ngIf="!selfHosted && showPasswordless">
<button
bitButton
type="button"
buttonType="secondary"
class="tw-w-full"
(click)="startPasswordlessLogin()"
[disabled]="form.loading"
>
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
</button>
</div>
<div class="tw-mb-3">
<a routerLink="/sso" bitButton buttonType="secondary" class="tw-w-full">
<i class="bwi bwi-provider tw-mr-2"></i>
{{ "enterpriseSingleSignOn" | i18n }}
</a>
</div>
</div>
</div>
</div>
</form>

View File

@@ -1,4 +1,5 @@
import { Component, NgZone } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
@@ -7,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
@@ -20,7 +22,9 @@ import { Policy } from "@bitwarden/common/models/domain/policy";
import { ListResponse } from "@bitwarden/common/models/response/listResponse";
import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse";
import { RouterService, StateService } from "../core";
import { flagEnabled } from "src/utils/flags";
import { RouterService, StateService } from "../../core";
@Component({
selector: "app-login",
@@ -31,6 +35,7 @@ export class LoginComponent extends BaseLoginComponent {
showResetPasswordAutoEnrollWarning = false;
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
policies: ListResponse<PolicyResponse>;
showPasswordless = false;
constructor(
authService: AuthService,
@@ -48,7 +53,9 @@ export class LoginComponent extends BaseLoginComponent {
ngZone: NgZone,
protected stateService: StateService,
private messagingService: MessagingService,
private routerService: RouterService
private routerService: RouterService,
formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService
) {
super(
authService,
@@ -60,19 +67,22 @@ export class LoginComponent extends BaseLoginComponent {
passwordGenerationService,
cryptoFunctionService,
logService,
ngZone
ngZone,
formBuilder,
formValidationErrorService
);
this.onSuccessfulLogin = async () => {
this.messagingService.send("setFullWidth");
};
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
this.showPasswordless = flagEnabled("showPasswordless");
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.email = qParams.email;
this.formGroup.get("email")?.setValue(qParams.email);
}
if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium");
@@ -91,7 +101,8 @@ export class LoginComponent extends BaseLoginComponent {
this.routerService.setPreviousUrl(route.toString());
}
await super.ngOnInit();
this.rememberEmail = await this.stateService.getRememberEmail();
const rememberEmail = await this.stateService.getRememberEmail();
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
});
const invite = await this.stateService.getOrganizationInvitation();
@@ -125,10 +136,12 @@ export class LoginComponent extends BaseLoginComponent {
}
async goAfterLogIn() {
const masterPassword = this.formGroup.get("masterPassword")?.value;
// Check master password against policy
if (this.enforcedPasswordPolicyOptions != null) {
const strengthResult = this.passwordGenerationService.passwordStrength(
this.masterPassword,
masterPassword,
this.getPasswordStrengthUserInput()
);
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
@@ -137,7 +150,7 @@ export class LoginComponent extends BaseLoginComponent {
if (
!this.policyService.evaluateMasterPassword(
masterPasswordScore,
this.masterPassword,
masterPassword,
this.enforcedPasswordPolicyOptions
)
) {
@@ -158,19 +171,34 @@ export class LoginComponent extends BaseLoginComponent {
}
async submit() {
await this.stateService.setRememberEmail(this.rememberEmail);
if (!this.rememberEmail) {
const rememberEmail = this.formGroup.get("rememberEmail")?.value;
await this.stateService.setRememberEmail(rememberEmail);
if (!rememberEmail) {
await this.stateService.setRememberedEmail(null);
}
await super.submit();
await super.submit(false);
}
async startPasswordlessLogin() {
this.formGroup.get("masterPassword")?.clearValidators();
this.formGroup.get("masterPassword")?.updateValueAndValidity();
if (!this.formGroup.valid) {
return;
}
const email = this.formGroup.get("email").value;
this.router.navigate(["/login-with-device"], { state: { email: email } });
}
private getPasswordStrengthUserInput() {
const email = this.formGroup.get("email")?.value;
let userInput: string[] = [];
const atPosition = this.email.indexOf("@");
const atPosition = email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
this.email
email
.substr(0, atPosition)
.trim()
.toLowerCase()

View File

@@ -0,0 +1,13 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared";
import { LoginWithDeviceComponent } from "./login-with-device.component";
import { LoginComponent } from "./login.component";
@NgModule({
imports: [SharedModule],
declarations: [LoginComponent, LoginWithDeviceComponent],
exports: [LoginComponent, LoginWithDeviceComponent],
})
export class LoginModule {}

View File

@@ -11,7 +11,8 @@ import { AcceptEmergencyComponent } from "./accounts/accept-emergency.component"
import { AcceptOrganizationComponent } from "./accounts/accept-organization.component";
import { HintComponent } from "./accounts/hint.component";
import { LockComponent } from "./accounts/lock.component";
import { LoginComponent } from "./accounts/login.component";
import { LoginWithDeviceComponent } from "./accounts/login/login-with-device.component";
import { LoginComponent } from "./accounts/login/login.component";
import { RecoverDeleteComponent } from "./accounts/recover-delete.component";
import { RecoverTwoFactorComponent } from "./accounts/recover-two-factor.component";
import { RegisterComponent } from "./accounts/register.component";
@@ -60,6 +61,11 @@ const routes: Routes = [
canActivate: [HomeGuard], // Redirects either to vault, login or lock page.
},
{ path: "login", component: LoginComponent, canActivate: [UnauthGuard] },
{
path: "login-with-device",
component: LoginWithDeviceComponent,
data: { titleId: "loginWithDevice" },
},
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
{
path: "register",

View File

@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core";
import { LoginModule } from "./accounts/login/login.module";
import { TrialInitiationModule } from "./accounts/trial-initiation/trial-initiation.module";
import { OrganizationCreateModule } from "./organizations/create/organization-create.module";
import { OrganizationManageModule } from "./organizations/manage/organization-manage.module";
@@ -18,6 +19,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
OrganizationManageModule,
OrganizationUserModule,
OrganizationCreateModule,
LoginModule,
],
exports: [
SharedModule,
@@ -25,6 +27,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module";
TrialInitiationModule,
VaultFilterModule,
OrganizationBadgeModule,
LoginModule,
],
bootstrap: [],
})

View File

@@ -6,7 +6,6 @@ import { AcceptEmergencyComponent } from "../accounts/accept-emergency.component
import { AcceptOrganizationComponent } from "../accounts/accept-organization.component";
import { HintComponent } from "../accounts/hint.component";
import { LockComponent } from "../accounts/lock.component";
import { LoginComponent } from "../accounts/login.component";
import { RecoverDeleteComponent } from "../accounts/recover-delete.component";
import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component";
import { RegisterFormModule } from "../accounts/register-form/register-form.module";
@@ -210,7 +209,6 @@ import { SharedModule } from ".";
FrontendLayoutComponent,
HintComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,
NavbarComponent,
NestedCheckboxComponent,
@@ -355,7 +353,6 @@ import { SharedModule } from ".";
FrontendLayoutComponent,
HintComponent,
LockComponent,
LoginComponent,
MasterPasswordPolicyComponent,
NavbarComponent,
NestedCheckboxComponent,

View File

@@ -569,15 +569,27 @@
"loginOrCreateNewAccount": {
"message": "Log in or create a new account to access your secure vault."
},
"loginWithDevice" : {
"message": "Log in with device"
},
"loginWithDevciceEnabledInfo": {
"message": "Log in with device must be enabled in the settings of the Biwarden mobile app. Need another option?"
},
"createAccount": {
"message": "Create Account"
},
"newAroundHere": {
"message": "New around here?"
},
"startTrial": {
"message": "Start Trial"
},
"logIn": {
"message": "Log In"
},
"logInInitiated": {
"message": "Log in initiated"
},
"submit": {
"message": "Submit"
},
@@ -635,7 +647,7 @@
"confirmMasterPasswordRequired": {
"message": "Master password retype is required."
},
"masterPasswordMinLength": {
"masterPasswordMinlength": {
"message": "Master password must be at least 8 characters long."
},
"masterPassDoesntMatch": {
@@ -705,6 +717,9 @@
"noOrganizationsList": {
"message": "You do not belong to any organizations. Organizations allow you to securely share items with other users."
},
"notificationSentDevice":{
"message": "A notification has been sent to your device."
},
"versionNumber": {
"message": "Version $VERSION_NUMBER$",
"placeholders": {
@@ -2532,6 +2547,9 @@
}
}
},
"viewAllLoginOptions": {
"message": "View all log in options"
},
"viewedItemId": {
"message": "Viewed item $ID$.",
"placeholders": {
@@ -3372,6 +3390,12 @@
"message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
},
"fingerprintMatchInfo": {
"message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device."
},
"fingerprintPhraseHeader": {
"message": "Fingerprint phrase"
},
"dontAskFingerprintAgain": {
"message": "Never prompt to verify fingerprint phrases for invited users (Not recommended)",
"description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing."
@@ -4372,6 +4396,9 @@
"reinviteSelected": {
"message": "Resend Invitations"
},
"resendNotification": {
"message": "Resend notification"
},
"noSelectedUsersApplicable": {
"message": "This action is not applicable to any of the selected users."
},

View File

@@ -10,6 +10,7 @@ import {
/* eslint-disable-next-line @typescript-eslint/ban-types */
export type Flags = {
showTrial?: boolean;
showPasswordless?: boolean;
} & SharedFlags;
// required to avoid linting errors when there are no flags