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

View File

@ -542,6 +542,12 @@
"overwritePasswordConfirmation": {
"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": {
"message": "Search folder"
},
@ -1233,7 +1239,12 @@
"message": "This password was not found in any known data breaches. It should be safe to use."
},
"baseDomain": {
"message": "Base domain"
"message": "Base domain",
"description": "Domain name. Ex. website.com"
},
"domainName": {
"message": "Domain Name",
"description": "Domain name. Ex. website.com"
},
"host": {
"message": "Host",
@ -1887,5 +1898,44 @@
},
"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 { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.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 { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
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 { TwoFactorService } from "jslib-common/services/twoFactor.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 { BrowserApi } from "../browser/browserApi";
@ -136,6 +138,7 @@ export default class MainBackground {
keyConnectorService: KeyConnectorServiceAbstraction;
userVerificationService: UserVerificationServiceAbstraction;
twoFactorService: TwoFactorServiceAbstraction;
usernameGenerationService: UsernameGenerationServiceAbstraction;
onUpdatedRan: boolean;
onReplacedRan: boolean;
@ -200,7 +203,7 @@ export default class MainBackground {
}
);
this.i18nService = new I18nService(BrowserApi.getUILanguage(window));
this.cryptoFunctionService = new WebCryptoFunctionService(window, this.platformUtilsService);
this.cryptoFunctionService = new WebCryptoFunctionService(window);
this.cryptoService = new BrowserCryptoService(
this.cryptoFunctionService,
this.platformUtilsService,
@ -475,6 +478,10 @@ export default class MainBackground {
this.twoFactorService,
this.i18nService
);
this.usernameGenerationService = new UsernameGenerationService(
this.cryptoService,
this.stateService
);
}
async bootstrap() {

View File

@ -1,4 +1,4 @@
<ng-container>
<ng-container *ngIf="show">
<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>
</button>

View File

@ -6,7 +6,7 @@
</button>
</div>
<h1 class="center">
<span class="title">{{ "passGen" | i18n }}</span>
<span class="title">{{ "generator" | i18n }}</span>
</h1>
<div class="right">
<button type="button" appBlurClick (click)="select()" *ngIf="showSelect">
@ -15,58 +15,69 @@
</div>
</header>
<content>
<app-callout type="info" *ngIf="enforcedPolicyOptions?.inEffect()">
<app-callout type="info" *ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'">
{{ "passwordGeneratorPolicyInEffect" | i18n }}
</app-callout>
<div class="password-block">
<div class="password-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div>
</div>
<div class="box list">
<div class="box-content single-line">
<div class="generated-block" *ngIf="type === 'password'">
<div class="generated-wrapper" [innerHTML]="password | colorPassword" appSelectCopy></div>
<div class="action-buttons">
<button
type="button"
class="box-content-row text-primary"
class="row-btn"
appStopClick
appBlurClick
(click)="regenerate()"
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy()"
>
{{ "regeneratePassword" | i18n }}
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row text-primary"
appStopClick
appBlurClick
(click)="copy()"
appA11yTitle="{{ 'regeneratePassword' | i18n }}"
(click)="regenerate()"
>
{{ "copyPassword" | i18n }}
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<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 class="generated-block" *ngIf="type === 'username'">
<div class="generated-wrapper" [innerHTML]="username | colorPassword" appSelectCopy></div>
<div class="action-buttons">
<button
type="button"
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 class="box">
<h2 class="box-header">
{{ "options" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row">
<label class="sr-only radio-header">{{ "type" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of passTypeOptions">
<label class="radio-header">{{ "whatWouldYouLikeToGenerate" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of typeOptions">
<input
type="radio"
[(ngModel)]="options.type"
[(ngModel)]="type"
name="Type_{{ o.value }}"
id="type_{{ o.value }}"
[value]="o.value"
(change)="saveOptions()"
[checked]="options.type === o.value"
(change)="typeChanged()"
[checked]="type === o.value"
[disabled]="showSelect"
/>
<label for="type_{{ o.value }}">
{{ o.name }}
@ -75,152 +86,344 @@
</div>
</div>
</div>
<div class="box" *ngIf="options.type === 'passphrase'">
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="num-words">{{ "numWords" | i18n }}</label>
<input
id="num-words"
type="number"
min="3"
max="20"
(change)="saveOptions()"
[(ngModel)]="options.numWords"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input
id="word-separator"
type="text"
maxlength="1"
(input)="saveOptions()"
[(ngModel)]="options.wordSeparator"
/>
</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"
/>
<ng-container *ngIf="type === 'password'">
<div class="box">
<h2 class="box-header">
{{ "options" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row">
<label class="radio-header">{{ "passwordType" | i18n }}</label>
<div class="radio-group text-default" appBoxRow *ngFor="let o of passTypeOptions">
<input
type="radio"
[(ngModel)]="passwordOptions.type"
name="PasswordType_{{ o.value }}"
id="passwordtype_{{ o.value }}"
[value]="o.value"
(change)="savePasswordOptions()"
[checked]="passwordOptions.type === o.value"
/>
<label for="passwordtype_{{ o.value }}">
{{ o.name }}
</label>
</div>
</div>
</div>
</div>
</div>
<ng-container *ngIf="options.type === 'password'">
<div class="box">
<div class="box" *ngIf="passwordOptions.type === 'passphrase'">
<div class="box-content">
<div class="box-content-row box-content-row-slider" appBoxRow>
<label for="length">{{ "length" | i18n }}</label>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="num-words">{{ "numWords" | i18n }}</label>
<input
id="length"
id="num-words"
type="number"
min="5"
max="128"
[(ngModel)]="options.length"
(change)="saveOptions()"
min="3"
max="20"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.numWords"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
<input
id="lengthRange"
type="range"
min="5"
max="128"
step="1"
[(ngModel)]="options.length"
(change)="sliderChanged()"
(input)="sliderInput()"
id="word-separator"
type="text"
maxlength="1"
(input)="savePasswordOptions()"
[(ngModel)]="passwordOptions.wordSeparator"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="uppercase">A-Z</label>
<label for="capitalize">{{ "capitalize" | i18n }}</label>
<input
id="uppercase"
id="capitalize"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'uppercase' | i18n }}"
[disabled]="enforcedPolicyOptions.useUppercase"
[(ngModel)]="options.uppercase"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.capitalize"
[disabled]="enforcedPasswordPolicyOptions?.capitalize"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="lowercase">a-z</label>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="lowercase"
id="include-number"
type="checkbox"
(change)="saveOptions()"
attr.aria-label="{{ 'lowercase' | i18n }}"
[disabled]="enforcedPolicyOptions.useLowercase"
[(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"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.includeNumber"
[disabled]="enforcedPasswordPolicyOptions?.includeNumber"
/>
</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">
<h2 class="box-header">
{{ "options" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-number">{{ "minNumbers" | i18n }}</label>
<div class="box-content-row">
<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
id="min-number"
type="number"
min="0"
max="9"
(change)="saveOptions()"
[(ngModel)]="options.minNumber"
id="subaddress-email"
type="text"
name="SubaddressEmail"
[(ngModel)]="usernameOptions.subaddressEmail"
(blur)="saveUsernameOptions()"
/>
</div>
<div class="box-content-row box-content-row-input" appBoxRow>
<label for="min-special">{{ "minSpecial" | i18n }}</label>
<div class="box-content-row" *ngIf="subaddressOptions.length > 1">
<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
id="min-special"
type="number"
min="0"
max="9"
(change)="saveOptions()"
[(ngModel)]="options.minSpecial"
id="subaddress-website"
type="text"
name="SubaddressWebsite"
[value]="usernameOptions.website"
disabled
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 class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "avoidAmbChar" | i18n }}</label>
<label for="include-number">{{ "includeNumber" | i18n }}</label>
<input
id="ambiguous"
id="include-number"
type="checkbox"
(change)="saveOptions()"
[(ngModel)]="avoidAmbiguous"
(change)="saveUsernameOptions()"
[(ngModel)]="usernameOptions.wordIncludeNumber"
/>
</div>
</div>

View File

@ -1,11 +1,13 @@
import { Location } from "@angular/common";
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { PasswordGeneratorComponent as BasePasswordGeneratorComponent } from "jslib-angular/components/password-generator.component";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.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";
@Component({
@ -13,30 +15,52 @@ import { CipherView } from "jslib-common/models/view/cipherView";
templateUrl: "password-generator.component.html",
})
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
private addEditCipherInfo: any;
private cipherState: CipherView;
constructor(
passwordGenerationService: PasswordGenerationService,
usernameGenerationService: UsernameGenerationService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
private stateService: StateService,
stateService: StateService,
route: ActivatedRoute,
private location: Location
) {
super(passwordGenerationService, platformUtilsService, i18nService, window);
super(
passwordGenerationService,
usernameGenerationService,
platformUtilsService,
stateService,
i18nService,
route,
window
);
}
async ngOnInit() {
await super.ngOnInit();
const addEditCipherInfo = await this.stateService.getAddEditCipherInfo();
if (addEditCipherInfo != null) {
this.cipherState = addEditCipherInfo.cipher;
this.addEditCipherInfo = await this.stateService.getAddEditCipherInfo();
if (this.addEditCipherInfo != null) {
this.cipherState = this.addEditCipherInfo.cipher;
}
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() {
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();
}

View File

@ -677,5 +677,14 @@
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-family: $font-family-monospace;
margin: 20px;
display: flex;
.password-wrapper {
text-align: center;
.generated-wrapper {
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 { TwoFactorService } from "jslib-common/abstractions/twoFactor.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 { ThemeType } from "jslib-common/enums/themeType";
import { AuthService } from "jslib-common/services/auth.service";
@ -311,6 +312,11 @@ export function initFactory(
useFactory: getBgService<StateServiceAbstraction>("stateService"),
deps: [],
},
{
provide: UsernameGenerationService,
useFactory: getBgService<UsernameGenerationService>("usernameGenerationService"),
deps: [],
},
{
provide: BaseStateServiceAbstraction,
useExisting: StateServiceAbstraction,

View File

@ -34,16 +34,30 @@
</div>
<!-- Login -->
<div *ngIf="cipher.type === cipherType.Login">
<div class="box-content-row" appBoxRow>
<label for="loginUsername">{{ "username" | i18n }}</label>
<input
id="loginUsername"
type="text"
name="Login.Username"
[(ngModel)]="cipher.login.username"
inputmode="email"
appInputVerbatim
/>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginUsername">{{ "username" | i18n }}</label>
<input
id="loginUsername"
type="text"
name="Login.Username"
[(ngModel)]="cipher.login.username"
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 class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">

View File

@ -182,17 +182,20 @@ export class AddEditComponent extends BaseAddEditComponent {
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> {
const confirmed = await super.generatePassword();
if (confirmed) {
this.stateService.setAddEditCipherInfo({
cipher: this.cipher,
collectionIds:
this.collections == null
? []
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
});
this.router.navigate(["generator"]);
await this.saveCipherState();
this.router.navigate(["generator"], { queryParams: { type: "password" } });
}
return confirmed;
}
@ -217,4 +220,14 @@ export class AddEditComponent extends BaseAddEditComponent {
(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),
});
}
}