username generator (#2468)

* username generator

* pass usernameWebsite

* update jslib ref

* update jslib ref

* update jslib ref

* update jslib ref

* Update jslib to point to jslib master

* Updated package-lock.json after running npm i

* add missing translations

* pr feedback

Co-authored-by: Daniel James Smith <djsmith@web.de>
This commit is contained in:
Kyle Spearrin 2022-03-30 17:59:58 -04:00 committed by GitHub
parent 4607e9d0ba
commit bf081e0322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 532 additions and 191 deletions

2
jslib

@ -1 +1 @@
Subproject commit 9950fb42a15bad434a4b404419ff4a87af67a27b Subproject commit fa73c13b8c9ed35cbb9909e342b143fa8a57f1a0

34
package-lock.json generated
View File

@ -121,7 +121,7 @@
"big-integer": "1.6.48", "big-integer": "1.6.48",
"browser-hrtime": "^1.1.8", "browser-hrtime": "^1.1.8",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"node-forge": "^0.10.0", "node-forge": "^1.2.1",
"papaparse": "^5.3.0", "papaparse": "^5.3.0",
"rxjs": "^7.4.0", "rxjs": "^7.4.0",
"tldjs": "^2.3.1", "tldjs": "^2.3.1",
@ -130,7 +130,7 @@
"devDependencies": { "devDependencies": {
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/node": "^16.11.12", "@types/node": "^16.11.12",
"@types/node-forge": "^0.9.7", "@types/node-forge": "^1.0.1",
"@types/papaparse": "^5.2.5", "@types/papaparse": "^5.2.5",
"@types/tldjs": "^2.3.0", "@types/tldjs": "^2.3.0",
"@types/zxcvbn": "^4.4.1", "@types/zxcvbn": "^4.4.1",
@ -1037,9 +1037,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node-forge": { "node_modules/@types/node-forge": {
"version": "0.9.10", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.9.10.tgz", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.0.1.tgz",
"integrity": "sha512-+BbPlhZeYs/WETWftQi2LeRx9VviWSwawNo+Pid5qNrSZHb60loYjpph3OrbwXMMseadu9rE9NeK34r4BHT+QQ==", "integrity": "sha512-96ELNKv9tQJ19afdBUiM5iDw7OYEc53iUc51gAPR2aGaqRsO1DBROjqgZRjZa1tkPj7TnEOR0EnyAX6iryGkzA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@ -8281,11 +8281,11 @@
} }
}, },
"node_modules/node-forge": { "node_modules/node-forge": {
"version": "0.10.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
"engines": { "engines": {
"node": ">= 6.0.0" "node": ">= 6.13.0"
} }
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
@ -12566,14 +12566,14 @@
"@microsoft/signalr-protocol-msgpack": "5.0.10", "@microsoft/signalr-protocol-msgpack": "5.0.10",
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/node": "^16.11.12", "@types/node": "^16.11.12",
"@types/node-forge": "^0.9.7", "@types/node-forge": "^1.0.1",
"@types/papaparse": "^5.2.5", "@types/papaparse": "^5.2.5",
"@types/tldjs": "^2.3.0", "@types/tldjs": "^2.3.0",
"@types/zxcvbn": "^4.4.1", "@types/zxcvbn": "^4.4.1",
"big-integer": "1.6.48", "big-integer": "1.6.48",
"browser-hrtime": "^1.1.8", "browser-hrtime": "^1.1.8",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"node-forge": "^0.10.0", "node-forge": "^1.2.1",
"papaparse": "^5.3.0", "papaparse": "^5.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.4.0", "rxjs": "^7.4.0",
@ -12894,9 +12894,9 @@
"dev": true "dev": true
}, },
"@types/node-forge": { "@types/node-forge": {
"version": "0.9.10", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.9.10.tgz", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.0.1.tgz",
"integrity": "sha512-+BbPlhZeYs/WETWftQi2LeRx9VviWSwawNo+Pid5qNrSZHb60loYjpph3OrbwXMMseadu9rE9NeK34r4BHT+QQ==", "integrity": "sha512-96ELNKv9tQJ19afdBUiM5iDw7OYEc53iUc51gAPR2aGaqRsO1DBROjqgZRjZa1tkPj7TnEOR0EnyAX6iryGkzA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@ -18471,9 +18471,9 @@
} }
}, },
"node-forge": { "node-forge": {
"version": "0.10.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
"integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA=="
}, },
"node-releases": { "node-releases": {
"version": "2.0.1", "version": "2.0.1",

View File

@ -542,6 +542,12 @@
"overwritePasswordConfirmation": { "overwritePasswordConfirmation": {
"message": "Are you sure you want to overwrite the current password?" "message": "Are you sure you want to overwrite the current password?"
}, },
"overwriteUsername": {
"message": "Overwrite Username"
},
"overwriteUsernameConfirmation": {
"message": "Are you sure you want to overwrite the current username?"
},
"searchFolder": { "searchFolder": {
"message": "Search folder" "message": "Search folder"
}, },
@ -1233,7 +1239,12 @@
"message": "This password was not found in any known data breaches. It should be safe to use." "message": "This password was not found in any known data breaches. It should be safe to use."
}, },
"baseDomain": { "baseDomain": {
"message": "Base domain" "message": "Base domain",
"description": "Domain name. Ex. website.com"
},
"domainName": {
"message": "Domain Name",
"description": "Domain name. Ex. website.com"
}, },
"host": { "host": {
"message": "Host", "message": "Host",
@ -1887,5 +1898,44 @@
}, },
"error": { "error": {
"message": "Error" "message": "Error"
},
"regenerateUsername": {
"message": "Regenerate Username"
},
"generateUsername": {
"message": "Generate Username"
},
"usernameType": {
"message": "Username Type"
},
"plusAddressedEmail": {
"message": "Plus Addressed Email"
},
"plusAddressedEmailDesc": {
"message": "Use your email provider's sub-addressing capabilities."
},
"catchallEmail": {
"message": "Catch-all Email"
},
"catchallEmailDesc": {
"message": "Use your domain's configured catch-all inbox."
},
"random": {
"message": "Random"
},
"randomWord": {
"message": "Random Word"
},
"websiteName": {
"message": "Website Name"
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
},
"passwordType": {
"message": "Password Type"
},
"service": {
"message": "Service"
} }
} }

View File

@ -31,6 +31,7 @@ import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractio
import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "jslib-common/abstractions/userVerification.service"; import { UserVerificationService as UserVerificationServiceAbstraction } from "jslib-common/abstractions/userVerification.service";
import { UsernameGenerationService as UsernameGenerationServiceAbstraction } from "jslib-common/abstractions/usernameGeneration.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType"; import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from "jslib-common/enums/cipherType"; import { CipherType } from "jslib-common/enums/cipherType";
@ -66,6 +67,7 @@ import { TokenService } from "jslib-common/services/token.service";
import { TotpService } from "jslib-common/services/totp.service"; import { TotpService } from "jslib-common/services/totp.service";
import { TwoFactorService } from "jslib-common/services/twoFactor.service"; import { TwoFactorService } from "jslib-common/services/twoFactor.service";
import { UserVerificationService } from "jslib-common/services/userVerification.service"; import { UserVerificationService } from "jslib-common/services/userVerification.service";
import { UsernameGenerationService } from "jslib-common/services/usernameGeneration.service";
import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service"; import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service";
import { BrowserApi } from "../browser/browserApi"; import { BrowserApi } from "../browser/browserApi";
@ -136,6 +138,7 @@ export default class MainBackground {
keyConnectorService: KeyConnectorServiceAbstraction; keyConnectorService: KeyConnectorServiceAbstraction;
userVerificationService: UserVerificationServiceAbstraction; userVerificationService: UserVerificationServiceAbstraction;
twoFactorService: TwoFactorServiceAbstraction; twoFactorService: TwoFactorServiceAbstraction;
usernameGenerationService: UsernameGenerationServiceAbstraction;
onUpdatedRan: boolean; onUpdatedRan: boolean;
onReplacedRan: boolean; onReplacedRan: boolean;
@ -200,7 +203,7 @@ export default class MainBackground {
} }
); );
this.i18nService = new I18nService(BrowserApi.getUILanguage(window)); this.i18nService = new I18nService(BrowserApi.getUILanguage(window));
this.cryptoFunctionService = new WebCryptoFunctionService(window, this.platformUtilsService); this.cryptoFunctionService = new WebCryptoFunctionService(window);
this.cryptoService = new BrowserCryptoService( this.cryptoService = new BrowserCryptoService(
this.cryptoFunctionService, this.cryptoFunctionService,
this.platformUtilsService, this.platformUtilsService,
@ -475,6 +478,10 @@ export default class MainBackground {
this.twoFactorService, this.twoFactorService,
this.i18nService this.i18nService
); );
this.usernameGenerationService = new UsernameGenerationService(
this.cryptoService,
this.stateService
);
} }
async bootstrap() { async bootstrap() {

View File

@ -1,4 +1,4 @@
<ng-container> <ng-container *ngIf="show">
<button type="button" (click)="expand()" appA11yTitle="{{ 'popOutNewWindow' | i18n }}"> <button type="button" (click)="expand()" appA11yTitle="{{ 'popOutNewWindow' | i18n }}">
<i class="bwi bwi-external-link bwi-rotate-270 bwi-lg bwi-fw" aria-hidden="true"></i> <i class="bwi bwi-external-link bwi-rotate-270 bwi-lg bwi-fw" aria-hidden="true"></i>
</button> </button>

View File

@ -6,7 +6,7 @@
</button> </button>
</div> </div>
<h1 class="center"> <h1 class="center">
<span class="title">{{ "passGen" | i18n }}</span> <span class="title">{{ "generator" | i18n }}</span>
</h1> </h1>
<div class="right"> <div class="right">
<button type="button" appBlurClick (click)="select()" *ngIf="showSelect"> <button type="button" appBlurClick (click)="select()" *ngIf="showSelect">
@ -15,58 +15,69 @@
</div> </div>
</header> </header>
<content> <content>
<app-callout type="info" *ngIf="enforcedPolicyOptions?.inEffect()"> <app-callout type="info" *ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'">
{{ "passwordGeneratorPolicyInEffect" | i18n }} {{ "passwordGeneratorPolicyInEffect" | i18n }}
</app-callout> </app-callout>
<div class="password-block"> <div class="generated-block" *ngIf="type === 'password'">
<div class="password-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div> <div class="generated-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div>
</div> <div class="action-buttons">
<div class="box list">
<div class="box-content single-line">
<button <button
type="button" type="button"
class="box-content-row text-primary" class="row-btn"
appStopClick appStopClick
appBlurClick appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="regenerate()" (click)="copy()"
> >
{{ "regeneratePassword" | i18n }} <i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button> </button>
<button <button
type="button" type="button"
class="box-content-row text-primary"
appStopClick appStopClick
appBlurClick appBlurClick
(click)="copy()" appA11yTitle="{{ 'regeneratePassword' | i18n }}"
(click)="regenerate()"
> >
{{ "copyPassword" | i18n }} <i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="box list"> <div class="generated-block" *ngIf="type === 'username'">
<div class="box-content single-line"> <div class="generated-wrapper" [innerHTML]="username | colorPassword" appSelectCopy></div>
<a class="box-content-row box-content-row-flex" routerLink="/generator-history"> <div class="action-buttons">
<div class="row-main">{{ "passwordHistory" | i18n }}</div> <button
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i> type="button"
</a> class="row-btn"
appStopClick
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
appStopClick
appBlurClick
appA11yTitle="{{ 'regenerateUsername' | i18n }}"
(click)="regenerate()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<div class="box"> <div class="box">
<h2 class="box-header">
{{ "options" | i18n }}
</h2>
<div class="box-content"> <div class="box-content">
<div class="box-content-row"> <div class="box-content-row">
<label class="sr-only radio-header">{{ "type" | i18n }}</label> <label class="radio-header">{{ "whatWouldYouLikeToGenerate" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of passTypeOptions"> <div class="radio-group text-default" appBoxRow *ngFor="let o of typeOptions">
<input <input
type="radio" type="radio"
[(ngModel)]="options.type" [(ngModel)]="type"
name="Type_{{ o.value }}" name="Type_{{ o.value }}"
id="type_{{ o.value }}" id="type_{{ o.value }}"
[value]="o.value" [value]="o.value"
(change)="saveOptions()" (change)="typeChanged()"
[checked]="options.type === o.value" [checked]="type === o.value"
[disabled]="showSelect"
/> />
<label for="type_{{ o.value }}"> <label for="type_{{ o.value }}">
{{ o.name }} {{ o.name }}
@ -75,152 +86,344 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box" *ngIf="options.type === 'passphrase'"> <ng-container *ngIf="type === 'password'">
<div class="box-content"> <div class="box">
<div class="box-content-row box-content-row-input" appBoxRow> <h2 class="box-header">
<label for="num-words">{{ "numWords" | i18n }}</label> {{ "options" | i18n }}
<input </h2>
id="num-words" <div class="box-content">
type="number" <div class="box-content-row">
min="3" <label class="radio-header">{{ "passwordType" | i18n }}</label>
max="20" <div class="radio-group text-default" appBoxRow *ngFor="let o of passTypeOptions">
(change)="saveOptions()" <input
[(ngModel)]="options.numWords" type="radio"
/> [(ngModel)]="passwordOptions.type"
</div> name="PasswordType_{{ o.value }}"
<div class="box-content-row box-content-row-input" appBoxRow> id="passwordtype_{{ o.value }}"
<label for="word-separator">{{ "wordSeparator" | i18n }}</label> [value]="o.value"
<input (change)="savePasswordOptions()"
id="word-separator" [checked]="passwordOptions.type === o.value"
type="text" />
maxlength="1" <label for="passwordtype_{{ o.value }}">
(input)="saveOptions()" {{ o.name }}
[(ngModel)]="options.wordSeparator" </label>
/> </div>
</div> </div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="saveOptions()"
[(ngModel)]="options.capitalize"
[disabled]="enforcedPolicyOptions?.capitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="include-number"
type="checkbox"
(change)="saveOptions()"
[(ngModel)]="options.includeNumber"
[disabled]="enforcedPolicyOptions?.includeNumber"
/>
</div> </div>
</div> </div>
</div> <div class="box" *ngIf="passwordOptions.type === 'passphrase'">
<ng-container *ngIf="options.type === 'password'">
<div class="box">
<div class="box-content"> <div class="box-content">
<div class="box-content-row box-content-row-slider" appBoxRow> <div class="box-content-row box-content-row-input" appBoxRow>
<label for="length">{{ "length" | i18n }}</label> <label for="num-words">{{ "numWords" | i18n }}</label>
<input <input
id="length" id="num-words"
type="number" type="number"
min="5" min="3"
max="128" max="20"
[(ngModel)]="options.length" (change)="savePasswordOptions()"
(change)="saveOptions()" [(ngModel)]="passwordOptions.numWords"
/> />
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input <input
id="lengthRange" id="word-separator"
type="range" type="text"
min="5" maxlength="1"
max="128" (input)="savePasswordOptions()"
step="1" [(ngModel)]="passwordOptions.wordSeparator"
[(ngModel)]="options.length"
(change)="sliderChanged()"
(input)="sliderInput()"
/> />
</div> </div>
<div class="box-content-row box-content-row-checkbox" appBoxRow> <div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label> <label for="capitalize">{{ "capitalize" | i18n }}</label>
<input <input
id="uppercase" id="capitalize"
type="checkbox" type="checkbox"
(change)="saveOptions()" (change)="savePasswordOptions()"
attr.aria-label="{{ 'uppercase' | i18n }}" [(ngModel)]="passwordOptions.capitalize"
[disabled]="enforcedPolicyOptions.useUppercase" [disabled]="enforcedPasswordPolicyOptions?.capitalize"
[(ngModel)]="options.uppercase"
/> />
</div> </div>
<div class="box-content-row box-content-row-checkbox" appBoxRow> <div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label> <label for="include-number">{{ "includeNumber" | i18n }}</label>
<input <input
id="lowercase" id="include-number"
type="checkbox" type="checkbox"
(change)="saveOptions()" (change)="savePasswordOptions()"
attr.aria-label="{{ 'lowercase' | i18n }}" [(ngModel)]="passwordOptions.includeNumber"
[disabled]="enforcedPolicyOptions.useLowercase" [disabled]="enforcedPasswordPolicyOptions?.includeNumber"
[(ngModel)]="options.lowercase"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input
id="numbers"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'numbers' | i18n }}"
[disabled]="enforcedPolicyOptions.useNumbers"
[(ngModel)]="options.number"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="special">!@#$%^&*</label>
<input
id="special"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'specialCharacters' | i18n }}"
[disabled]="enforcedPolicyOptions.useSpecial"
[(ngModel)]="options.special"
/> />
</div> </div>
</div> </div>
</div> </div>
<ng-container *ngIf="passwordOptions.type === 'password'">
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-slider" appBoxRow>
<label for="length">{{ "length" | i18n }}</label>
<input
id="length"
type="number"
min="5"
max="128"
[(ngModel)]="passwordOptions.length"
(change)="savePasswordOptions()"
/>
<input
id="lengthRange"
type="range"
min="5"
max="128"
step="1"
[(ngModel)]="passwordOptions.length"
(change)="sliderChanged()"
(input)="sliderInput()"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label>
<input
id="uppercase"
type="checkbox"
(change)="savePasswordOptions()"
attr.aria-label="{{ 'uppercase' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useUppercase"
[(ngModel)]="passwordOptions.uppercase"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label>
<input
id="lowercase"
type="checkbox"
(change)="savePasswordOptions()"
attr.aria-label="{{ 'lowercase' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useLowercase"
[(ngModel)]="passwordOptions.lowercase"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input
id="numbers"
type="checkbox"
(change)="savePasswordOptions()"
attr.aria-label="{{ 'numbers' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useNumbers"
[(ngModel)]="passwordOptions.number"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="special">!@#$%^&*</label>
<input
id="special"
type="checkbox"
(change)="savePasswordOptions()"
attr.aria-label="{{ 'specialCharacters' | i18n }}"
[disabled]="enforcedPasswordPolicyOptions.useSpecial"
[(ngModel)]="passwordOptions.special"
/>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-number">{{ "minNumbers" | i18n }}</label>
<input
id="min-number"
type="number"
min="0"
max="9"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.minNumber"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-special">{{ "minSpecial" | i18n }}</label>
<input
id="min-special"
type="number"
min="0"
max="9"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.minSpecial"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "avoidAmbChar" | i18n }}</label>
<input
id="ambiguous"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="avoidAmbiguous"
/>
</div>
</div>
</div>
</ng-container>
<div class="box list">
<div class="box-content single-line">
<a class="box-content-row box-content-row-flex" routerLink="/generator-history">
<div class="row-main">{{ "passwordHistory" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</a>
</div>
</div>
</ng-container>
<ng-container *ngIf="type === 'username'">
<div class="box"> <div class="box">
<h2 class="box-header">
{{ "options" | i18n }}
</h2>
<div class="box-content"> <div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow> <div class="box-content-row">
<label for="min-number">{{ "minNumbers" | i18n }}</label> <label class="radio-header">{{ "usernameType" | i18n }}</label>
<div
class="radio-group align-start text-default"
appBoxRow
*ngFor="let o of usernameTypeOptions"
>
<input
type="radio"
[(ngModel)]="usernameOptions.type"
name="Type_{{ o.value }}"
id="type_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.type === o.value"
/>
<label for="type_{{ o.value }}">
{{ o.name }}
<div class="small text-muted" *ngIf="o.desc">{{ o.desc }}</div>
</label>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'forwarded'">
<div class="box-content">
<div class="box-content-row">
<label class="radio-header">{{ "service" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of forwardOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.forwardedService"
name="ForwardType_{{ o.value }}"
id="forwardtype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.forwardedService === o.value"
/>
<label for="forwardtype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'subaddress'">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="subaddress-email">{{ "emailAddress" | i18n }}</label>
<input <input
id="min-number" id="subaddress-email"
type="number" type="text"
min="0" name="SubaddressEmail"
max="9" [(ngModel)]="usernameOptions.subaddressEmail"
(change)="saveOptions()" (blur)="saveUsernameOptions()"
[(ngModel)]="options.minNumber"
/> />
</div> </div>
<div class="box-content-row box-content-row-input" appBoxRow> <div class="box-content-row" *ngIf="subaddressOptions.length > 1">
<label for="min-special">{{ "minSpecial" | i18n }}</label> <label class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of subaddressOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.subaddressType"
name="SubaddressType_{{ o.value }}"
id="subaddresstype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.subaddressType === o.value"
/>
<label for="subaddresstype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="showWebsiteOption">
<label for="subaddress-website">{{ "website" | i18n }}</label>
<input <input
id="min-special" id="subaddress-website"
type="number" type="text"
min="0" name="SubaddressWebsite"
max="9" [value]="usernameOptions.website"
(change)="saveOptions()" disabled
[(ngModel)]="options.minSpecial" readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'catchall'">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="catchall-domain">{{ "domainName" | i18n }}</label>
<input
id="catchall-domain"
type="text"
name="CatchallDomain"
[(ngModel)]="usernameOptions.catchallDomain"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row" *ngIf="catchallOptions.length > 1">
<label class="radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of catchallOptions">
<input
type="radio"
[(ngModel)]="usernameOptions.catchallType"
name="CatchallType_{{ o.value }}"
id="catchalltype_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.catchallType === o.value"
/>
<label for="catchalltype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
<div class="box-content-row" appBoxRow *ngIf="showWebsiteOption">
<label for="catchall-website">{{ "website" | i18n }}</label>
<input
id="catchall-website"
type="text"
name="CatchallWebsite"
[value]="usernameOptions.website"
disabled
readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'word'">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="capitalize"
type="checkbox"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordCapitalize"
/> />
</div> </div>
<div class="box-content-row box-content-row-checkbox" appBoxRow> <div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "avoidAmbChar" | i18n }}</label> <label for="include-number">{{ "includeNumber" | i18n }}</label>
<input <input
id="ambiguous" id="include-number"
type="checkbox" type="checkbox"
(change)="saveOptions()" (change)="saveUsernameOptions()"
[(ngModel)]="avoidAmbiguous" [(ngModel)]="usernameOptions.wordIncludeNumber"
/> />
</div> </div>
</div> </div>

View File

@ -1,11 +1,13 @@
import { Location } from "@angular/common"; import { Location } from "@angular/common";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { PasswordGeneratorComponent as BasePasswordGeneratorComponent } from "jslib-angular/components/password-generator.component"; import { PasswordGeneratorComponent as BasePasswordGeneratorComponent } from "jslib-angular/components/password-generator.component";
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service"; import { StateService } from "jslib-common/abstractions/state.service";
import { UsernameGenerationService } from "jslib-common/abstractions/usernameGeneration.service";
import { CipherView } from "jslib-common/models/view/cipherView"; import { CipherView } from "jslib-common/models/view/cipherView";
@Component({ @Component({
@ -13,30 +15,52 @@ import { CipherView } from "jslib-common/models/view/cipherView";
templateUrl: "password-generator.component.html", templateUrl: "password-generator.component.html",
}) })
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent { export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
private addEditCipherInfo: any;
private cipherState: CipherView; private cipherState: CipherView;
constructor( constructor(
passwordGenerationService: PasswordGenerationService, passwordGenerationService: PasswordGenerationService,
usernameGenerationService: UsernameGenerationService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, i18nService: I18nService,
private stateService: StateService, stateService: StateService,
route: ActivatedRoute,
private location: Location private location: Location
) { ) {
super(passwordGenerationService, platformUtilsService, i18nService, window); super(
passwordGenerationService,
usernameGenerationService,
platformUtilsService,
stateService,
i18nService,
route,
window
);
} }
async ngOnInit() { async ngOnInit() {
await super.ngOnInit(); this.addEditCipherInfo = await this.stateService.getAddEditCipherInfo();
const addEditCipherInfo = await this.stateService.getAddEditCipherInfo(); if (this.addEditCipherInfo != null) {
if (addEditCipherInfo != null) { this.cipherState = this.addEditCipherInfo.cipher;
this.cipherState = addEditCipherInfo.cipher;
} }
this.showSelect = this.cipherState != null; this.showSelect = this.cipherState != null;
this.showWebsiteOption =
this.cipherState?.login?.hasUris && this.cipherState.login.uris[0].hostname != null;
if (this.showWebsiteOption) {
this.usernameWebsite = this.cipherState.login.uris[0].hostname;
}
await super.ngOnInit();
} }
select() { select() {
super.select(); super.select();
this.cipherState.login.password = this.password; if (this.type === "password") {
this.cipherState.login.password = this.password;
} else if (this.type === "username") {
this.cipherState.login.username = this.username;
}
this.addEditCipherInfo.cipher = this.cipherState;
this.stateService.setAddEditCipherInfo(this.addEditCipherInfo);
this.close(); this.close();
} }

View File

@ -677,5 +677,14 @@
color: themed("textColor"); color: themed("textColor");
} }
} }
&.align-start {
align-items: start;
margin-top: 10px;
label {
margin-top: -4px;
}
}
} }
} }

View File

@ -8,13 +8,28 @@ app-sync {
} }
} }
app-password-generator .password-block { app-password-generator .generated-block {
font-size: $font-size-large; font-size: $font-size-large;
font-family: $font-family-monospace; font-family: $font-family-monospace;
margin: 20px; margin: 20px;
display: flex;
.password-wrapper { .generated-wrapper {
text-align: center; text-align: left;
width: 100%;
min-width: 0;
white-space: pre-wrap;
word-break: break-all;
}
.action-buttons {
display: flex;
align-self: center;
button {
padding-left: 5px;
margin-left: 10px;
}
} }
} }

View File

@ -37,6 +37,7 @@ import { TokenService } from "jslib-common/abstractions/token.service";
import { TotpService } from "jslib-common/abstractions/totp.service"; import { TotpService } from "jslib-common/abstractions/totp.service";
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service"; import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service"; import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
import { UsernameGenerationService } from "jslib-common/abstractions/usernameGeneration.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { ThemeType } from "jslib-common/enums/themeType"; import { ThemeType } from "jslib-common/enums/themeType";
import { AuthService } from "jslib-common/services/auth.service"; import { AuthService } from "jslib-common/services/auth.service";
@ -311,6 +312,11 @@ export function initFactory(
useFactory: getBgService<StateServiceAbstraction>("stateService"), useFactory: getBgService<StateServiceAbstraction>("stateService"),
deps: [], deps: [],
}, },
{
provide: UsernameGenerationService,
useFactory: getBgService<UsernameGenerationService>("usernameGenerationService"),
deps: [],
},
{ {
provide: BaseStateServiceAbstraction, provide: BaseStateServiceAbstraction,
useExisting: StateServiceAbstraction, useExisting: StateServiceAbstraction,

View File

@ -34,16 +34,30 @@
</div> </div>
<!-- Login --> <!-- Login -->
<div *ngIf="cipher.type === cipherType.Login"> <div *ngIf="cipher.type === cipherType.Login">
<div class="box-content-row" appBoxRow> <div class="box-content-row box-content-row-flex" appBoxRow>
<label for="loginUsername">{{ "username" | i18n }}</label> <div class="row-main">
<input <label for="loginUsername">{{ "username" | i18n }}</label>
id="loginUsername" <input
type="text" id="loginUsername"
name="Login.Username" type="text"
[(ngModel)]="cipher.login.username" name="Login.Username"
inputmode="email" [(ngModel)]="cipher.login.username"
appInputVerbatim inputmode="email"
/> appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appBlurClick
appA11yTitle="{{ 'generateUsername' | i18n }}"
(click)="generateUsername()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div> </div>
<div class="box-content-row box-content-row-flex" appBoxRow> <div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main"> <div class="row-main">

View File

@ -182,17 +182,20 @@ export class AddEditComponent extends BaseAddEditComponent {
this.location.back(); this.location.back();
} }
async generateUsername(): Promise<boolean> {
const confirmed = await super.generateUsername();
if (confirmed) {
await this.saveCipherState();
this.router.navigate(["generator"], { queryParams: { type: "username" } });
}
return confirmed;
}
async generatePassword(): Promise<boolean> { async generatePassword(): Promise<boolean> {
const confirmed = await super.generatePassword(); const confirmed = await super.generatePassword();
if (confirmed) { if (confirmed) {
this.stateService.setAddEditCipherInfo({ await this.saveCipherState();
cipher: this.cipher, this.router.navigate(["generator"], { queryParams: { type: "password" } });
collectionIds:
this.collections == null
? []
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
});
this.router.navigate(["generator"]);
} }
return confirmed; return confirmed;
} }
@ -217,4 +220,14 @@ export class AddEditComponent extends BaseAddEditComponent {
(this.ownershipOptions.length > 1 || !this.allowPersonal) (this.ownershipOptions.length > 1 || !this.allowPersonal)
); );
} }
private saveCipherState() {
return this.stateService.setAddEditCipherInfo({
cipher: this.cipher,
collectionIds:
this.collections == null
? []
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
});
}
} }