1
0
mirror of https://github.com/bitwarden/desktop.git synced 2024-11-27 12:26:38 +01:00

Username generator (#1456)

* username generator implemented

* disable type when coming from add/edit

* restyle buttons to new icon-btn

* update generated-wrapper styles

* only show policy messages for passwords

* make generated-wrapper a standalone style

* Update src/app/vault/password-generator.component.html

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* aria-expanded on show options

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Kyle Spearrin 2022-03-29 15:01:57 -04:00 committed by GitHub
parent bc21703a2b
commit 9e0cc45704
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 612 additions and 214 deletions

View File

@ -3,7 +3,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-body form"> <div class="modal-body form">
<div class="box"> <div class="box">
<label class="settingsTitle">{{ "settingsTitle" | i18n: currentUserEmail }} </label> <div class="box-header">
{{ "settingsTitle" | i18n: currentUserEmail }}
</div>
<div class="box-content box-content-padded"> <div class="box-content box-content-padded">
<h2> <h2>
<button <button

View File

@ -317,10 +317,10 @@ export class AppComponent implements OnInit {
case "newFolder": case "newFolder":
await this.addFolder(); await this.addFolder();
break; break;
case "openPasswordGenerator": case "openGenerator":
// openPasswordGenerator has extended functionality if called in the vault // openGenerator has extended functionality if called in the vault
if (!this.router.url.includes("vault")) { if (!this.router.url.includes("vault")) {
await this.openPasswordGenerator(); await this.openGenerator();
} }
break; break;
case "convertAccountToKeyConnector": case "convertAccountToKeyConnector":
@ -402,14 +402,14 @@ export class AppComponent implements OnInit {
}); });
} }
async openPasswordGenerator() { async openGenerator() {
if (this.modal != null) { if (this.modal != null) {
this.modal.close(); this.modal.close();
} }
[this.modal] = await this.modalService.openViewRef( [this.modal] = await this.modalService.openViewRef(
PasswordGeneratorComponent, PasswordGeneratorComponent,
this.folderAddEditModalRef, this.passwordGeneratorModalRef,
(comp) => (comp.showSelect = false) (comp) => (comp.showSelect = false)
); );

View File

@ -27,15 +27,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"
appInputVerbatim [(ngModel)]="cipher.login.username"
/> appInputVerbatim
/>
</div>
<div class="action-buttons">
<a
class="row-btn"
href="#"
appStopClick
appBlurClick
role="button"
appA11yTitle="{{ 'generateUsername' | i18n }}"
(click)="generateUsername()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</a>
</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

@ -10,7 +10,7 @@
<div class="box-content-row box-content-row-flex" *ngFor="let h of history"> <div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main"> <div class="row-main">
<div <div
class="password-wrapper monospaced" class="generated-wrapper monospaced"
appSelectCopy appSelectCopy
[innerHTML]="h.password | colorPassword" [innerHTML]="h.password | colorPassword"
></div> ></div>

View File

@ -4,52 +4,84 @@
aria-modal="true" aria-modal="true"
attr.aria-label="{{ 'generatePassword' | i18n }}" attr.aria-label="{{ 'generatePassword' | i18n }}"
> >
<div class="modal-dialog modal-sm" role="document"> <div class="modal-dialog modal-md" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
<app-callout type="info" *ngIf="enforcedPasswordPolicyOptions?.inEffect()"> <div class="modal-title">
{{ "generator" | i18n }}
</div>
<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 class="action-buttons">
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'regeneratePassword' | i18n }}"
(click)="regenerate()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="generated-block" *ngIf="type === 'username'">
<div class="generated-wrapper" [innerHTML]="username | colorPassword" appSelectCopy></div>
<div class="action-buttons">
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy()"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="icon-btn primary"
appStopClick
appA11yTitle="{{ 'regenerateUsername' | i18n }}"
(click)="regenerate()"
>
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div> </div>
<div class="box"> <div class="box">
<div class="box-content condensed"> <div class="box-content condensed">
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="regenerate()">
<i class="bwi bwi-fw bwi-generate" aria-hidden="true"></i>
{{ "regeneratePassword" | i18n }}
</a>
<a class="box-content-row" href="#" appStopClick appBlurClick (click)="copy()">
<i class="bwi bwi-fw bwi-clone" aria-hidden="true"></i> {{ "copyPassword" | i18n }}
</a>
</div>
</div>
<div class="box">
<div class="box-header">
<button type="button" (click)="toggleOptions()" [attr.aria-expanded]="showOptions">
<i class="bwi bwi-plus-square" aria-hidden="true" [hidden]="showOptions"></i>
<i class="bwi bwi-minus-square" aria-hidden="true" [hidden]="!showOptions"></i>
{{ "options" | i18n }}
</button>
</div>
<div class="box-content condensed" [hidden]="!showOptions">
<div class="box-content-row box-content-row-radio"> <div class="box-content-row box-content-row-radio">
<label class="sr-only radio-header">{{ "type" | i18n }}</label> <label class="radio-header">{{ "whatWouldYouLikeToGenerate" | i18n }}</label>
<div <div
class="radio-group text-default" class="radio-group text-default"
appBoxRow appBoxRow
name="PassTypeOptions" name="TypeOptions"
*ngFor="let o of passTypeOptions" *ngFor="let o of typeOptions"
> >
<input <input
type="radio" type="radio"
class="radio" class="radio"
[(ngModel)]="passwordOptions.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)="savePasswordOptions()" (change)="typeChanged()"
[checked]="passwordOptions.type === o.value" [checked]="type === o.value"
[disabled]="showSelect"
/> />
<label class="unstyled" for="type_{{ o.value }}"> <label class="unstyled" for="type_{{ o.value }}">
{{ o.name }} {{ o.name }}
@ -58,148 +90,358 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box" [hidden]="!showOptions" *ngIf="passwordOptions.type === 'passphrase'"> <ng-container *ngIf="type === 'password'">
<div class="box-content condensed"> <div class="box">
<div class="box-content-row box-content-row-input" appBoxRow> <div class="box-header">
<label for="num-words">{{ "numWords" | i18n }}</label> <button
<input type="button"
id="num-words" (click)="toggleOptions()"
type="number" appA11yTitle="{{ 'toggleVisibility' | i18n }}"
min="3" [attr.aria-expanded]="showOptions"
max="20" >
(blur)="savePasswordOptions()" <i class="bwi bwi-plus-square" aria-hidden="true" [hidden]="showOptions"></i>
[(ngModel)]="passwordOptions.numWords" <i class="bwi bwi-minus-square" aria-hidden="true" [hidden]="!showOptions"></i>
/> {{ "options" | i18n }}
</button>
</div> </div>
<div class="box-content-row box-content-row-input" appBoxRow> <div class="box-content condensed" [hidden]="!showOptions">
<label for="word-separator">{{ "wordSeparator" | i18n }}</label> <div class="box-content-row box-content-row-radio">
<input <label class="radio-header">{{ "passwordType" | i18n }}</label>
id="word-separator" <div
type="text" class="radio-group text-default"
maxlength="1" appBoxRow
(input)="savePasswordOptions()" name="PassTypeOptions"
[(ngModel)]="passwordOptions.wordSeparator" *ngFor="let o of passTypeOptions"
/> >
</div> <input
<div class="box-content-row box-content-row-checkbox" appBoxRow> type="radio"
<label for="capitalize">{{ "capitalize" | i18n }}</label> class="radio"
<input [(ngModel)]="passwordOptions.type"
id="capitalize" name="PasswordType_{{ o.value }}"
type="checkbox" id="passwordType_{{ o.value }}"
(change)="savePasswordOptions()" [value]="o.value"
[(ngModel)]="passwordOptions.capitalize" (change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.capitalize" [checked]="passwordOptions.type === o.value"
/> />
</div> <label class="unstyled" for="passwordType_{{ o.value }}">
<div class="box-content-row box-content-row-checkbox" appBoxRow> {{ o.name }}
<label for="include-number">{{ "includeNumber" | i18n }}</label> </label>
<input </div>
id="include-number" </div>
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="passwordOptions.includeNumber"
[disabled]="enforcedPasswordPolicyOptions?.includeNumber"
/>
</div> </div>
</div> </div>
</div> <div class="box" [hidden]="!showOptions" *ngIf="passwordOptions.type === 'passphrase'">
<ng-container *ngIf="passwordOptions.type === 'password'">
<div class="box" [hidden]="!showOptions">
<div class="box-content condensed"> <div class="box-content condensed">
<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)]="passwordOptions.length"
(blur)="savePasswordOptions()" (blur)="savePasswordOptions()"
[(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)]="passwordOptions.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)="savePasswordOptions()" (change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useUppercase" [(ngModel)]="passwordOptions.capitalize"
[(ngModel)]="passwordOptions.uppercase" [disabled]="enforcedPasswordPolicyOptions?.capitalize"
/> />
</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)="savePasswordOptions()" (change)="savePasswordOptions()"
[disabled]="enforcedPasswordPolicyOptions?.useLowercase" [(ngModel)]="passwordOptions.includeNumber"
[(ngModel)]="passwordOptions.lowercase" [disabled]="enforcedPasswordPolicyOptions?.includeNumber"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="numbers">0-9</label>
<input
id="numbers"
type="checkbox"
(change)="savePasswordOptions()"
[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()"
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
[(ngModel)]="passwordOptions.special"
/> />
</div> </div>
</div> </div>
</div> </div>
<div class="box" [hidden]="!showOptions"> <ng-container *ngIf="passwordOptions.type === 'password'">
<div class="box" [hidden]="!showOptions">
<div class="box-content condensed">
<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"
(blur)="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()"
[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()"
[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()"
[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()"
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
[(ngModel)]="passwordOptions.special"
/>
</div>
</div>
</div>
<div class="box" [hidden]="!showOptions">
<div class="box-content condensed">
<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"
(blur)="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"
(blur)="savePasswordOptions()"
[(ngModel)]="passwordOptions.minSpecial"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="ambiguous">{{ "ambiguous" | i18n }}</label>
<input
id="ambiguous"
type="checkbox"
(change)="savePasswordOptions()"
[(ngModel)]="avoidAmbiguous"
/>
</div>
</div>
</div>
</ng-container>
</ng-container>
<ng-container *ngIf="type === 'username'">
<div class="box">
<div class="box-header">
<button
type="button"
(click)="toggleOptions()"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-expanded]="showOptions"
>
<i class="bwi bwi-plus-square" aria-hidden="true" [hidden]="showOptions"></i>
<i class="bwi bwi-minus-square" aria-hidden="true" [hidden]="!showOptions"></i>
{{ "options" | i18n }}
</button>
</div>
<div class="box-content condensed" [hidden]="!showOptions">
<div class="box-content-row box-content-row-radio">
<label class="radio-header">{{ "usernameType" | i18n }}</label>
<div
class="radio-group align-start text-default"
appBoxRow
name="UsernameTypeOptions"
*ngFor="let o of usernameTypeOptions"
>
<input
type="radio"
class="radio"
[(ngModel)]="usernameOptions.type"
name="UsernameType_{{ o.value }}"
id="usernameType_{{ o.value }}"
[value]="o.value"
(change)="saveUsernameOptions()"
[checked]="usernameOptions.type === o.value"
/>
<label class="unstyled" for="usernameType_{{ 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'" [hidden]="!showOptions">
<div class="box-content condensed"> <div class="box-content condensed">
<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">{{ "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'" [hidden]="!showOptions">
<div class="box-content condensed">
<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"
(blur)="savePasswordOptions()" (blur)="saveUsernameOptions()"
[(ngModel)]="passwordOptions.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"
(blur)="savePasswordOptions()" disabled
[(ngModel)]="passwordOptions.minSpecial" readonly
/>
</div>
</div>
</div>
<div class="box" *ngIf="usernameOptions.type === 'catchall'" [hidden]="!showOptions">
<div class="box-content condensed">
<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'" [hidden]="!showOptions">
<div class="box-content condensed">
<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">{{ "ambiguous" | i18n }}</label> <label for="include-number">{{ "includeNumber" | i18n }}</label>
<input <input
id="ambiguous" id="include-number"
type="checkbox" type="checkbox"
(change)="savePasswordOptions()" (change)="saveUsernameOptions()"
[(ngModel)]="avoidAmbiguous" [(ngModel)]="usernameOptions.wordIncludeNumber"
/> />
</div> </div>
</div> </div>

View File

@ -37,7 +37,8 @@
(onCancelled)="cancelledAddEdit($event)" (onCancelled)="cancelledAddEdit($event)"
(onShareCipher)="shareCipher($event)" (onShareCipher)="shareCipher($event)"
(onEditCollections)="cipherCollections($event)" (onEditCollections)="cipherCollections($event)"
(onGeneratePassword)="openPasswordGenerator(true)" (onGeneratePassword)="openGenerator(true, true)"
(onGenerateUsername)="openGenerator(true, false)"
> >
</app-vault-add-edit> </app-vault-add-edit>
<div <div

View File

@ -120,8 +120,8 @@ export class VaultComponent implements OnInit, OnDestroy {
(document.querySelector("#search") as HTMLInputElement).select(); (document.querySelector("#search") as HTMLInputElement).select();
detectChanges = false; detectChanges = false;
break; break;
case "openPasswordGenerator": case "openGenerator":
await this.openPasswordGenerator(false); await this.openGenerator(false);
break; break;
case "syncCompleted": case "syncCompleted":
await this.load(); await this.load();
@ -599,28 +599,39 @@ export class VaultComponent implements OnInit, OnDestroy {
this.go(); this.go();
} }
async openPasswordGenerator(showSelect: boolean) { async openGenerator(showSelect: boolean, passwordType = true) {
if (this.modal != null) { if (this.modal != null) {
this.modal.close(); this.modal.close();
} }
const cipher = this.addEditComponent?.cipher;
const loginType = cipher != null && cipher.type === CipherType.Login && cipher.login != null;
const [modal, childComponent] = await this.modalService.openViewRef( const [modal, childComponent] = await this.modalService.openViewRef(
PasswordGeneratorComponent, PasswordGeneratorComponent,
this.passwordGeneratorModalRef, this.passwordGeneratorModalRef,
(comp) => (comp.showSelect = showSelect) (comp) => {
comp.showSelect = showSelect;
if (showSelect) {
comp.type = passwordType ? "password" : "username";
if (loginType && cipher.login.hasUris && cipher.login.uris[0].hostname != null) {
comp.usernameWebsite = cipher.login.uris[0].hostname;
comp.showWebsiteOption = true;
}
}
}
); );
this.modal = modal; this.modal = modal;
childComponent.onSelected.subscribe((password: string) => { childComponent.onSelected.subscribe((value: string) => {
this.modal.close(); this.modal.close();
if ( if (loginType) {
this.addEditComponent != null &&
this.addEditComponent.cipher != null &&
this.addEditComponent.cipher.type === CipherType.Login &&
this.addEditComponent.cipher.login != null
) {
this.addEditComponent.markPasswordAsDirty(); this.addEditComponent.markPasswordAsDirty();
this.addEditComponent.cipher.login.password = password; if (passwordType) {
this.addEditComponent.cipher.login.password = value;
} else {
this.addEditComponent.cipher.login.username = value;
}
} }
}); });

View File

@ -47,7 +47,7 @@
</div> </div>
<div <div
*ngIf="showPassword" *ngIf="showPassword"
class="monospaced password-wrapper" class="monospaced generated-wrapper"
appSelectCopy appSelectCopy
[innerHTML]="cipher.login.password | colorPassword" [innerHTML]="cipher.login.password | colorPassword"
></div> ></div>

View File

@ -369,6 +369,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?"
},
"noneFolder": { "noneFolder": {
"message": "No Folder", "message": "No Folder",
"description": "This is the folder for uncategorized items" "description": "This is the folder for uncategorized items"
@ -1188,7 +1194,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",
@ -1828,5 +1839,47 @@
"example": "name@example.com" "example": "name@example.com"
} }
} }
},
"generator": {
"message": "Generator"
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
},
"passwordType": {
"message": "Password Type"
},
"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"
},
"service": {
"message": "Service"
} }
} }

View File

@ -16,7 +16,7 @@ export class ViewMenu implements IMenubarMenu {
return [ return [
this.searchVault, this.searchVault,
this.separator, this.separator,
this.passwordGenerator, this.generator,
this.passwordHistory, this.passwordHistory,
this.separator, this.separator,
this.zoomIn, this.zoomIn,
@ -54,11 +54,11 @@ export class ViewMenu implements IMenubarMenu {
return { type: "separator" }; return { type: "separator" };
} }
private get passwordGenerator(): MenuItemConstructorOptions { private get generator(): MenuItemConstructorOptions {
return { return {
id: "passwordGenerator", id: "generator",
label: this.localize("passwordGenerator"), label: this.localize("generator"),
click: () => this.sendMessage("openPasswordGenerator"), click: () => this.sendMessage("openGenerator"),
accelerator: "CmdOrCtrl+G", accelerator: "CmdOrCtrl+G",
enabled: !this._isLocked, enabled: !this._isLocked,
}; };

View File

@ -4,15 +4,6 @@
position: relative; position: relative;
width: 100%; width: 100%;
.settingsTitle {
margin: 0 10px 5px 10px;
display: flex;
@include themify($themes) {
color: themed("headingColor");
}
}
.box-header { .box-header {
margin: 0 10px 5px 10px; margin: 0 10px 5px 10px;
text-transform: uppercase; text-transform: uppercase;
@ -302,7 +293,7 @@
&.box-content-row-slider { &.box-content-row-slider {
input[type="range"] { input[type="range"] {
height: 10px; height: 10px;
width: 110px !important; width: 220px !important;
} }
input[type="number"] { input[type="number"] {
@ -364,33 +355,7 @@
margin-left: 5px; margin-left: 5px;
.row-btn { .row-btn {
cursor: pointer; @extend .icon-btn;
padding: 10px 8px;
background: none;
border: none;
@include themify($themes) {
color: themed("boxRowButtonColor");
}
&:hover,
&:focus {
@include themify($themes) {
color: themed("boxRowButtonHoverColor");
}
}
&.disabled {
@include themify($themes) {
color: themed("disabledIconColor");
}
&:hover {
@include themify($themes) {
color: themed("disabledIconColor");
}
}
}
} }
&.no-pad .row-btn { &.no-pad .row-btn {
@ -485,4 +450,36 @@
min-width: 25px; min-width: 25px;
} }
} }
.radio-group {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 5px;
input {
flex-grow: 0;
}
label {
margin: 0 0 0 5px;
flex-grow: 1;
font-size: $font-size-base;
display: block;
width: 100%;
@include themify($themes) {
color: themed("textColor");
}
}
&.align-start {
align-items: start;
margin-top: 10px;
label {
margin-top: -4px;
}
}
}
} }

View File

@ -115,3 +115,57 @@
} }
} }
} }
.icon-btn {
cursor: pointer;
padding: 10px 8px;
background: none;
border: none;
@include themify($themes) {
color: themed("boxRowButtonColor");
}
&.primary {
@include themify($themes) {
color: themed("buttonPrimaryColor");
}
}
&.danger {
@include themify($themes) {
color: themed("buttonDangerColor");
}
}
&:hover,
&:focus {
@include themify($themes) {
color: themed("boxRowButtonHoverColor");
}
&.primary {
@include themify($themes) {
color: darken(themed("buttonPrimaryColor"), 6%);
}
}
&.danger {
@include themify($themes) {
color: darken(themed("buttonDangerColor"), 6%);
}
}
}
&.disabled {
@include themify($themes) {
color: themed("disabledIconColor");
}
&:hover {
@include themify($themes) {
color: themed("disabledIconColor");
}
}
}
}

View File

@ -1,6 +1,7 @@
@import "variables.scss"; @import "variables.scss";
small { small,
.small {
font-size: $font-size-small; font-size: $font-size-small;
} }
@ -173,22 +174,44 @@ p.lead {
} }
} }
.password-block { .modal-title {
margin: 0 10px 5px 10px;
text-transform: uppercase;
display: flex;
@include themify($themes) {
color: themed("headingColor");
}
}
.generated-block {
font-size: $font-size-large; font-size: $font-size-large;
font-family: $font-family-monospace; font-family: $font-family-monospace;
min-height: 50px; min-height: 50px;
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center;
.modal-body & { .modal-body & {
margin-top: 10px; margin: 10px;
}
.generated-wrapper {
text-align: left;
width: 100%;
}
.action-buttons {
display: flex;
align-self: center;
button {
margin-left: 10px;
}
} }
} }
.password-wrapper { .generated-wrapper {
word-break: break-all; word-break: break-all;
white-space: pre-wrap; white-space: pre-wrap;
min-width: 0; min-width: 0;