mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[PM-15200] add "generated credential" screen reader notification (#12877)
replaces website$ dependency with `GenerateRequest`
This commit is contained in:
@@ -445,6 +445,18 @@
|
|||||||
"generatePassphrase": {
|
"generatePassphrase": {
|
||||||
"message": "Generate passphrase"
|
"message": "Generate passphrase"
|
||||||
},
|
},
|
||||||
|
"passwordGenerated": {
|
||||||
|
"message": "Password generated"
|
||||||
|
},
|
||||||
|
"passphraseGenerated": {
|
||||||
|
"message": "Passphrase generated"
|
||||||
|
},
|
||||||
|
"usernameGenerated": {
|
||||||
|
"message": "Username generated"
|
||||||
|
},
|
||||||
|
"emailGenerated": {
|
||||||
|
"message": "Email generated"
|
||||||
|
},
|
||||||
"regeneratePassword": {
|
"regeneratePassword": {
|
||||||
"message": "Regenerate password"
|
"message": "Regenerate password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { Policy } from "../admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { OrganizationId, UserId } from "../types/guid";
|
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction";
|
import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction";
|
||||||
import { UserEncryptor } from "./cryptography/user-encryptor.abstraction";
|
import { UserEncryptor } from "./cryptography/user-encryptor.abstraction";
|
||||||
@@ -152,7 +152,8 @@ export type SingleUserDependency = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** A pattern for types that emit values exclusively when the dependency
|
/** A pattern for types that emit values exclusively when the dependency
|
||||||
* emits a message.
|
* emits a message. Set a type parameter when your method requires contextual
|
||||||
|
* information when the request is issued.
|
||||||
*
|
*
|
||||||
* Consumers of this dependency should emit when `on$` emits. If `on$`
|
* Consumers of this dependency should emit when `on$` emits. If `on$`
|
||||||
* completes, the consumer should also complete. If `on$`
|
* completes, the consumer should also complete. If `on$`
|
||||||
@@ -161,10 +162,10 @@ export type SingleUserDependency = {
|
|||||||
* @remarks This dependency is useful when you have a nondeterministic
|
* @remarks This dependency is useful when you have a nondeterministic
|
||||||
* or stateful algorithm that you would like to run when an event occurs.
|
* or stateful algorithm that you would like to run when an event occurs.
|
||||||
*/
|
*/
|
||||||
export type OnDependency = {
|
export type OnDependency<T = any> = {
|
||||||
/** The stream that controls emissions
|
/** The stream that controls emissions
|
||||||
*/
|
*/
|
||||||
on$: Observable<any>;
|
on$: Observable<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A pattern for types that emit when a dependency is `true`.
|
/** A pattern for types that emit when a dependency is `true`.
|
||||||
|
|||||||
@@ -72,6 +72,6 @@ export class CredentialGeneratorHistoryComponent {
|
|||||||
|
|
||||||
protected getGeneratedValueText(credential: GeneratedCredential) {
|
protected getGeneratedValueText(credential: GeneratedCredential) {
|
||||||
const info = this.generatorService.algorithm(credential.category);
|
const info = this.generatorService.algorithm(credential.category);
|
||||||
return info.generatedValue;
|
return info.credentialType;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-generate"
|
bitIconButton="bwi-generate"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate('user request')"
|
(click)="generate(USER_REQUEST)"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
[disabled]="!(algorithm$ | async)"
|
[disabled]="!(algorithm$ | async)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||||
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
CredentialAlgorithm,
|
CredentialAlgorithm,
|
||||||
CredentialCategory,
|
CredentialCategory,
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
|
GenerateRequest,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
Generators,
|
Generators,
|
||||||
getForwarderConfiguration,
|
getForwarderConfiguration,
|
||||||
@@ -60,6 +62,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
|
private ariaLive: LiveAnnouncer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings. When this input is not provided,
|
/** Binds the component to a specific user's settings. When this input is not provided,
|
||||||
@@ -185,10 +188,10 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
// continue with origin stream
|
// continue with origin stream
|
||||||
return generator;
|
return generator;
|
||||||
}),
|
}),
|
||||||
withLatestFrom(this.userId$),
|
withLatestFrom(this.userId$, this.algorithm$),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([generated, userId]) => {
|
.subscribe(([generated, userId, algorithm]) => {
|
||||||
this.generatorHistoryService
|
this.generatorHistoryService
|
||||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||||
.catch((e: unknown) => {
|
.catch((e: unknown) => {
|
||||||
@@ -198,8 +201,12 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
|
if (generated.source === this.USER_REQUEST) {
|
||||||
|
this.announce(algorithm.onGeneratedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generatedCredential$.next(generated);
|
||||||
this.onGenerated.next(generated);
|
this.onGenerated.next(generated);
|
||||||
this.value$.next(generated.credential);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,7 +390,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
if (!a || a.onlyOnRequest) {
|
if (!a || a.onlyOnRequest) {
|
||||||
this.value$.next("-");
|
this.generatedCredential$.next(null);
|
||||||
} else {
|
} else {
|
||||||
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
this.generate("autogenerate").catch((e: unknown) => this.logService.error(e));
|
||||||
}
|
}
|
||||||
@@ -391,6 +398,10 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private announce(message: string) {
|
||||||
|
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||||
|
}
|
||||||
|
|
||||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
on$: this.generate$,
|
on$: this.generate$,
|
||||||
@@ -473,7 +484,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
filter((algorithm) => !!algorithm),
|
||||||
map(({ generatedValue }) => generatedValue),
|
map(({ credentialType }) => credentialType),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Emits hint key for the currently selected credential type */
|
/** Emits hint key for the currently selected credential type */
|
||||||
@@ -482,21 +493,28 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
/** tracks the currently selected credential category */
|
/** tracks the currently selected credential category */
|
||||||
protected category$ = new ReplaySubject<string>(1);
|
protected category$ = new ReplaySubject<string>(1);
|
||||||
|
|
||||||
|
private readonly generatedCredential$ = new BehaviorSubject<GeneratedCredential>(null);
|
||||||
|
|
||||||
/** Emits the last generated value. */
|
/** Emits the last generated value. */
|
||||||
protected readonly value$ = new BehaviorSubject<string>("");
|
protected readonly value$ = this.generatedCredential$.pipe(
|
||||||
|
map((generated) => generated?.credential ?? "-"),
|
||||||
|
);
|
||||||
|
|
||||||
/** Emits when the userId changes */
|
/** Emits when the userId changes */
|
||||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
|
/** Identifies generator requests that were requested by the user */
|
||||||
|
protected readonly USER_REQUEST = "user request";
|
||||||
|
|
||||||
/** Emits when a new credential is requested */
|
/** Emits when a new credential is requested */
|
||||||
private readonly generate$ = new Subject<string>();
|
private readonly generate$ = new Subject<GenerateRequest>();
|
||||||
|
|
||||||
/** Request a new value from the generator
|
/** Request a new value from the generator
|
||||||
* @param requestor a label used to trace generation request
|
* @param requestor a label used to trace generation request
|
||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected async generate(requestor: string) {
|
protected async generate(requestor: string) {
|
||||||
this.generate$.next(requestor);
|
this.generate$.next({ source: requestor });
|
||||||
}
|
}
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmInfo[]) {
|
||||||
@@ -515,7 +533,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// finalize subjects
|
// finalize subjects
|
||||||
this.generate$.complete();
|
this.generate$.complete();
|
||||||
this.value$.complete();
|
this.generatedCredential$.complete();
|
||||||
|
|
||||||
// finalize component bindings
|
// finalize component bindings
|
||||||
this.onGenerated.complete();
|
this.onGenerated.complete();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-generate"
|
bitIconButton="bwi-generate"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate('user request')"
|
(click)="generate(USER_REQUEST)"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
[disabled]="!(algorithm$ | async)"
|
[disabled]="!(algorithm$ | async)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +17,6 @@ import {
|
|||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { ToastService, Option } from "@bitwarden/components";
|
import { ToastService, Option } from "@bitwarden/components";
|
||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
isPasswordAlgorithm,
|
isPasswordAlgorithm,
|
||||||
AlgorithmInfo,
|
AlgorithmInfo,
|
||||||
isSameAlgorithm,
|
isSameAlgorithm,
|
||||||
|
GenerateRequest,
|
||||||
} from "@bitwarden/generator-core";
|
} from "@bitwarden/generator-core";
|
||||||
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
import { GeneratorHistoryService } from "@bitwarden/generator-history";
|
||||||
|
|
||||||
@@ -42,9 +43,9 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
private generatorHistoryService: GeneratorHistoryService,
|
private generatorHistoryService: GeneratorHistoryService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private i18nService: I18nService,
|
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
|
private ariaLive: LiveAnnouncer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings.
|
/** Binds the component to a specific user's settings.
|
||||||
@@ -67,14 +68,17 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
/** Emits when a new credential is requested */
|
/** Emits when a new credential is requested */
|
||||||
private readonly generate$ = new Subject<string>();
|
private readonly generate$ = new Subject<GenerateRequest>();
|
||||||
|
|
||||||
|
/** Identifies generator requests that were requested by the user */
|
||||||
|
protected readonly USER_REQUEST = "user request";
|
||||||
|
|
||||||
/** Request a new value from the generator
|
/** Request a new value from the generator
|
||||||
* @param requestor a label used to trace generation request
|
* @param requestor a label used to trace generation request
|
||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected async generate(requestor: string) {
|
protected async generate(requestor: string) {
|
||||||
this.generate$.next(requestor);
|
this.generate$.next({ source: requestor });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Tracks changes to the selected credential type
|
/** Tracks changes to the selected credential type
|
||||||
@@ -137,10 +141,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
// continue with origin stream
|
// continue with origin stream
|
||||||
return generator;
|
return generator;
|
||||||
}),
|
}),
|
||||||
withLatestFrom(this.userId$),
|
withLatestFrom(this.userId$, this.algorithm$),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([generated, userId]) => {
|
.subscribe(([generated, userId, algorithm]) => {
|
||||||
this.generatorHistoryService
|
this.generatorHistoryService
|
||||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||||
.catch((e: unknown) => {
|
.catch((e: unknown) => {
|
||||||
@@ -150,6 +154,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
|
if (generated.source === this.USER_REQUEST) {
|
||||||
|
this.announce(algorithm.onGeneratedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
this.onGenerated.next(generated);
|
this.onGenerated.next(generated);
|
||||||
this.value$.next(generated.credential);
|
this.value$.next(generated.credential);
|
||||||
});
|
});
|
||||||
@@ -205,6 +213,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private announce(message: string) {
|
||||||
|
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||||
|
}
|
||||||
|
|
||||||
private typeToGenerator$(type: CredentialAlgorithm) {
|
private typeToGenerator$(type: CredentialAlgorithm) {
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
on$: this.generate$,
|
on$: this.generate$,
|
||||||
@@ -249,7 +261,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
filter((algorithm) => !!algorithm),
|
||||||
map(({ generatedValue }) => generatedValue),
|
map(({ credentialType }) => credentialType),
|
||||||
);
|
);
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmInfo[]) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-generate"
|
bitIconButton="bwi-generate"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
(click)="generate('user request')"
|
(click)="generate(USER_REQUEST)"
|
||||||
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
[appA11yTitle]="credentialTypeGenerateLabel$ | async"
|
||||||
[disabled]="!(algorithm$ | async)"
|
[disabled]="!(algorithm$ | async)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
|
import { LiveAnnouncer } from "@angular/cdk/a11y";
|
||||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
AlgorithmInfo,
|
AlgorithmInfo,
|
||||||
CredentialAlgorithm,
|
CredentialAlgorithm,
|
||||||
CredentialGeneratorService,
|
CredentialGeneratorService,
|
||||||
|
GenerateRequest,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
Generators,
|
Generators,
|
||||||
getForwarderConfiguration,
|
getForwarderConfiguration,
|
||||||
@@ -66,6 +68,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
|
private ariaLive: LiveAnnouncer,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Binds the component to a specific user's settings. When this input is not provided,
|
/** Binds the component to a specific user's settings. When this input is not provided,
|
||||||
@@ -160,10 +163,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
// continue with origin stream
|
// continue with origin stream
|
||||||
return generator;
|
return generator;
|
||||||
}),
|
}),
|
||||||
withLatestFrom(this.userId$),
|
withLatestFrom(this.userId$, this.algorithm$),
|
||||||
takeUntil(this.destroyed),
|
takeUntil(this.destroyed),
|
||||||
)
|
)
|
||||||
.subscribe(([generated, userId]) => {
|
.subscribe(([generated, userId, algorithm]) => {
|
||||||
this.generatorHistoryService
|
this.generatorHistoryService
|
||||||
.track(userId, generated.credential, generated.category, generated.generationDate)
|
.track(userId, generated.credential, generated.category, generated.generationDate)
|
||||||
.catch((e: unknown) => {
|
.catch((e: unknown) => {
|
||||||
@@ -173,6 +176,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
// update subjects within the angular zone so that the
|
// update subjects within the angular zone so that the
|
||||||
// template bindings refresh immediately
|
// template bindings refresh immediately
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
|
if (generated.source === this.USER_REQUEST) {
|
||||||
|
this.announce(algorithm.onGeneratedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
this.onGenerated.next(generated);
|
this.onGenerated.next(generated);
|
||||||
this.value$.next(generated.credential);
|
this.value$.next(generated.credential);
|
||||||
});
|
});
|
||||||
@@ -360,6 +367,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
throw new Error(`Invalid generator type: "${type}"`);
|
throw new Error(`Invalid generator type: "${type}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private announce(message: string) {
|
||||||
|
this.ariaLive.announce(message).catch((e) => this.logService.error(e));
|
||||||
|
}
|
||||||
|
|
||||||
/** Lists the credential types supported by the component. */
|
/** Lists the credential types supported by the component. */
|
||||||
protected typeOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
protected typeOptions$ = new BehaviorSubject<Option<string>[]>([]);
|
||||||
|
|
||||||
@@ -375,6 +386,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
/** tracks the currently selected credential type */
|
/** tracks the currently selected credential type */
|
||||||
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
|
protected algorithm$ = new ReplaySubject<AlgorithmInfo>(1);
|
||||||
|
|
||||||
|
/** Emits hint key for the currently selected credential type */
|
||||||
|
protected credentialTypeHint$ = new ReplaySubject<string>(1);
|
||||||
|
|
||||||
|
/** Emits the last generated value. */
|
||||||
|
protected readonly value$ = new BehaviorSubject<string>("");
|
||||||
|
|
||||||
|
/** Emits when the userId changes */
|
||||||
|
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
||||||
|
|
||||||
|
/** Emits when a new credential is requested */
|
||||||
|
private readonly generate$ = new Subject<GenerateRequest>();
|
||||||
|
|
||||||
protected showAlgorithm$ = this.algorithm$.pipe(
|
protected showAlgorithm$ = this.algorithm$.pipe(
|
||||||
combineLatestWith(this.showForwarder$),
|
combineLatestWith(this.showForwarder$),
|
||||||
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
|
map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)),
|
||||||
@@ -401,27 +424,18 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
protected credentialTypeLabel$ = this.algorithm$.pipe(
|
||||||
filter((algorithm) => !!algorithm),
|
filter((algorithm) => !!algorithm),
|
||||||
map(({ generatedValue }) => generatedValue),
|
map(({ credentialType }) => credentialType),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Emits hint key for the currently selected credential type */
|
/** Identifies generator requests that were requested by the user */
|
||||||
protected credentialTypeHint$ = new ReplaySubject<string>(1);
|
protected readonly USER_REQUEST = "user request";
|
||||||
|
|
||||||
/** Emits the last generated value. */
|
|
||||||
protected readonly value$ = new BehaviorSubject<string>("");
|
|
||||||
|
|
||||||
/** Emits when the userId changes */
|
|
||||||
protected readonly userId$ = new BehaviorSubject<UserId>(null);
|
|
||||||
|
|
||||||
/** Emits when a new credential is requested */
|
|
||||||
private readonly generate$ = new Subject<string>();
|
|
||||||
|
|
||||||
/** Request a new value from the generator
|
/** Request a new value from the generator
|
||||||
* @param requestor a label used to trace generation request
|
* @param requestor a label used to trace generation request
|
||||||
* origin in the debugger.
|
* origin in the debugger.
|
||||||
*/
|
*/
|
||||||
protected async generate(requestor: string) {
|
protected async generate(requestor: string) {
|
||||||
this.generate$.next(requestor);
|
this.generate$.next({ source: requestor });
|
||||||
}
|
}
|
||||||
|
|
||||||
private toOptions(algorithms: AlgorithmInfo[]) {
|
private toOptions(algorithms: AlgorithmInfo[]) {
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ const PASSPHRASE: CredentialGeneratorConfiguration<
|
|||||||
category: "password",
|
category: "password",
|
||||||
nameKey: "passphrase",
|
nameKey: "passphrase",
|
||||||
generateKey: "generatePassphrase",
|
generateKey: "generatePassphrase",
|
||||||
generatedValueKey: "passphrase",
|
onGeneratedMessageKey: "passphraseGenerated",
|
||||||
|
credentialTypeKey: "passphrase",
|
||||||
copyKey: "copyPassphrase",
|
copyKey: "copyPassphrase",
|
||||||
useGeneratedValueKey: "useThisPassword",
|
useGeneratedValueKey: "useThisPassword",
|
||||||
onlyOnRequest: false,
|
onlyOnRequest: false,
|
||||||
@@ -118,7 +119,8 @@ const PASSWORD: CredentialGeneratorConfiguration<
|
|||||||
category: "password",
|
category: "password",
|
||||||
nameKey: "password",
|
nameKey: "password",
|
||||||
generateKey: "generatePassword",
|
generateKey: "generatePassword",
|
||||||
generatedValueKey: "password",
|
onGeneratedMessageKey: "passwordGenerated",
|
||||||
|
credentialTypeKey: "password",
|
||||||
copyKey: "copyPassword",
|
copyKey: "copyPassword",
|
||||||
useGeneratedValueKey: "useThisPassword",
|
useGeneratedValueKey: "useThisPassword",
|
||||||
onlyOnRequest: false,
|
onlyOnRequest: false,
|
||||||
@@ -195,7 +197,8 @@ const USERNAME: CredentialGeneratorConfiguration<EffUsernameGenerationOptions, N
|
|||||||
category: "username",
|
category: "username",
|
||||||
nameKey: "randomWord",
|
nameKey: "randomWord",
|
||||||
generateKey: "generateUsername",
|
generateKey: "generateUsername",
|
||||||
generatedValueKey: "username",
|
onGeneratedMessageKey: "usernameGenerated",
|
||||||
|
credentialTypeKey: "username",
|
||||||
copyKey: "copyUsername",
|
copyKey: "copyUsername",
|
||||||
useGeneratedValueKey: "useThisUsername",
|
useGeneratedValueKey: "useThisUsername",
|
||||||
onlyOnRequest: false,
|
onlyOnRequest: false,
|
||||||
@@ -248,7 +251,8 @@ const CATCHALL: CredentialGeneratorConfiguration<CatchallGenerationOptions, NoPo
|
|||||||
nameKey: "catchallEmail",
|
nameKey: "catchallEmail",
|
||||||
descriptionKey: "catchallEmailDesc",
|
descriptionKey: "catchallEmailDesc",
|
||||||
generateKey: "generateEmail",
|
generateKey: "generateEmail",
|
||||||
generatedValueKey: "email",
|
onGeneratedMessageKey: "emailGenerated",
|
||||||
|
credentialTypeKey: "email",
|
||||||
copyKey: "copyEmail",
|
copyKey: "copyEmail",
|
||||||
useGeneratedValueKey: "useThisEmail",
|
useGeneratedValueKey: "useThisEmail",
|
||||||
onlyOnRequest: false,
|
onlyOnRequest: false,
|
||||||
@@ -304,7 +308,8 @@ const SUBADDRESS: CredentialGeneratorConfiguration<SubaddressGenerationOptions,
|
|||||||
nameKey: "plusAddressedEmail",
|
nameKey: "plusAddressedEmail",
|
||||||
descriptionKey: "plusAddressedEmailDesc",
|
descriptionKey: "plusAddressedEmailDesc",
|
||||||
generateKey: "generateEmail",
|
generateKey: "generateEmail",
|
||||||
generatedValueKey: "email",
|
onGeneratedMessageKey: "emailGenerated",
|
||||||
|
credentialTypeKey: "email",
|
||||||
copyKey: "copyEmail",
|
copyKey: "copyEmail",
|
||||||
useGeneratedValueKey: "useThisEmail",
|
useGeneratedValueKey: "useThisEmail",
|
||||||
onlyOnRequest: false,
|
onlyOnRequest: false,
|
||||||
@@ -362,7 +367,8 @@ export function toCredentialGeneratorConfiguration<Settings extends ApiSettings
|
|||||||
nameKey: configuration.name,
|
nameKey: configuration.name,
|
||||||
descriptionKey: "forwardedEmailDesc",
|
descriptionKey: "forwardedEmailDesc",
|
||||||
generateKey: "generateEmail",
|
generateKey: "generateEmail",
|
||||||
generatedValueKey: "email",
|
onGeneratedMessageKey: "emailGenerated",
|
||||||
|
credentialTypeKey: "email",
|
||||||
copyKey: "copyEmail",
|
copyKey: "copyEmail",
|
||||||
useGeneratedValueKey: "useThisEmail",
|
useGeneratedValueKey: "useThisEmail",
|
||||||
onlyOnRequest: true,
|
onlyOnRequest: true,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CatchallGenerationOptions,
|
CatchallGenerationOptions,
|
||||||
CredentialGenerator,
|
CredentialGenerator,
|
||||||
|
GenerateRequest,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
SubaddressGenerationOptions,
|
SubaddressGenerationOptions,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -112,25 +112,37 @@ export class EmailRandomizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
generate(
|
generate(
|
||||||
request: GenerationRequest,
|
request: GenerateRequest,
|
||||||
settings: CatchallGenerationOptions,
|
settings: CatchallGenerationOptions,
|
||||||
): Promise<GeneratedCredential>;
|
): Promise<GeneratedCredential>;
|
||||||
generate(
|
generate(
|
||||||
request: GenerationRequest,
|
request: GenerateRequest,
|
||||||
settings: SubaddressGenerationOptions,
|
settings: SubaddressGenerationOptions,
|
||||||
): Promise<GeneratedCredential>;
|
): Promise<GeneratedCredential>;
|
||||||
async generate(
|
async generate(
|
||||||
_request: GenerationRequest,
|
request: GenerateRequest,
|
||||||
settings: CatchallGenerationOptions | SubaddressGenerationOptions,
|
settings: CatchallGenerationOptions | SubaddressGenerationOptions,
|
||||||
) {
|
) {
|
||||||
if (isCatchallGenerationOptions(settings)) {
|
if (isCatchallGenerationOptions(settings)) {
|
||||||
const email = await this.randomAsciiCatchall(settings.catchallDomain);
|
const email = await this.randomAsciiCatchall(settings.catchallDomain);
|
||||||
|
|
||||||
return new GeneratedCredential(email, "catchall", Date.now());
|
return new GeneratedCredential(
|
||||||
|
email,
|
||||||
|
"catchall",
|
||||||
|
Date.now(),
|
||||||
|
request.source,
|
||||||
|
request.website,
|
||||||
|
);
|
||||||
} else if (isSubaddressGenerationOptions(settings)) {
|
} else if (isSubaddressGenerationOptions(settings)) {
|
||||||
const email = await this.randomAsciiSubaddress(settings.subaddressEmail);
|
const email = await this.randomAsciiSubaddress(settings.subaddressEmail);
|
||||||
|
|
||||||
return new GeneratedCredential(email, "subaddress", Date.now());
|
return new GeneratedCredential(
|
||||||
|
email,
|
||||||
|
"subaddress",
|
||||||
|
Date.now(),
|
||||||
|
request.source,
|
||||||
|
request.website,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Invalid settings received by generator.");
|
throw new Error("Invalid settings received by generator.");
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CredentialGenerator,
|
CredentialGenerator,
|
||||||
|
GenerateRequest,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
PassphraseGenerationOptions,
|
PassphraseGenerationOptions,
|
||||||
PasswordGenerationOptions,
|
PasswordGenerationOptions,
|
||||||
@@ -69,27 +69,39 @@ export class PasswordRandomizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
generate(
|
generate(
|
||||||
request: GenerationRequest,
|
request: GenerateRequest,
|
||||||
settings: PasswordGenerationOptions,
|
settings: PasswordGenerationOptions,
|
||||||
): Promise<GeneratedCredential>;
|
): Promise<GeneratedCredential>;
|
||||||
generate(
|
generate(
|
||||||
request: GenerationRequest,
|
request: GenerateRequest,
|
||||||
settings: PassphraseGenerationOptions,
|
settings: PassphraseGenerationOptions,
|
||||||
): Promise<GeneratedCredential>;
|
): Promise<GeneratedCredential>;
|
||||||
async generate(
|
async generate(
|
||||||
_request: GenerationRequest,
|
request: GenerateRequest,
|
||||||
settings: PasswordGenerationOptions | PassphraseGenerationOptions,
|
settings: PasswordGenerationOptions | PassphraseGenerationOptions,
|
||||||
) {
|
) {
|
||||||
if (isPasswordGenerationOptions(settings)) {
|
if (isPasswordGenerationOptions(settings)) {
|
||||||
const request = optionsToRandomAsciiRequest(settings);
|
const req = optionsToRandomAsciiRequest(settings);
|
||||||
const password = await this.randomAscii(request);
|
const password = await this.randomAscii(req);
|
||||||
|
|
||||||
return new GeneratedCredential(password, "password", Date.now());
|
return new GeneratedCredential(
|
||||||
|
password,
|
||||||
|
"password",
|
||||||
|
Date.now(),
|
||||||
|
request.source,
|
||||||
|
request.website,
|
||||||
|
);
|
||||||
} else if (isPassphraseGenerationOptions(settings)) {
|
} else if (isPassphraseGenerationOptions(settings)) {
|
||||||
const request = optionsToEffWordListRequest(settings);
|
const req = optionsToEffWordListRequest(settings);
|
||||||
const passphrase = await this.randomEffLongWords(request);
|
const passphrase = await this.randomEffLongWords(req);
|
||||||
|
|
||||||
return new GeneratedCredential(passphrase, "passphrase", Date.now());
|
return new GeneratedCredential(
|
||||||
|
passphrase,
|
||||||
|
"passphrase",
|
||||||
|
Date.now(),
|
||||||
|
request.source,
|
||||||
|
request.website,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Invalid settings received by generator.");
|
throw new Error("Invalid settings received by generator.");
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist";
|
||||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
|
||||||
|
|
||||||
import { CredentialGenerator, EffUsernameGenerationOptions, GeneratedCredential } from "../types";
|
import {
|
||||||
|
CredentialGenerator,
|
||||||
|
EffUsernameGenerationOptions,
|
||||||
|
GenerateRequest,
|
||||||
|
GeneratedCredential,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import { Randomizer } from "./abstractions";
|
import { Randomizer } from "./abstractions";
|
||||||
import { WordsRequest } from "./types";
|
import { WordsRequest } from "./types";
|
||||||
@@ -51,14 +55,20 @@ export class UsernameRandomizer implements CredentialGenerator<EffUsernameGenera
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(_request: GenerationRequest, settings: EffUsernameGenerationOptions) {
|
async generate(request: GenerateRequest, settings: EffUsernameGenerationOptions) {
|
||||||
if (isEffUsernameGenerationOptions(settings)) {
|
if (isEffUsernameGenerationOptions(settings)) {
|
||||||
const username = await this.randomWords({
|
const username = await this.randomWords({
|
||||||
digits: settings.wordIncludeNumber ? NUMBER_OF_DIGITS : 0,
|
digits: settings.wordIncludeNumber ? NUMBER_OF_DIGITS : 0,
|
||||||
casing: settings.wordCapitalize ? "TitleCase" : "lowercase",
|
casing: settings.wordCapitalize ? "TitleCase" : "lowercase",
|
||||||
});
|
});
|
||||||
|
|
||||||
return new GeneratedCredential(username, "username", Date.now());
|
return new GeneratedCredential(
|
||||||
|
username,
|
||||||
|
"username",
|
||||||
|
Date.now(),
|
||||||
|
request.source,
|
||||||
|
request.website,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Invalid settings received by generator.");
|
throw new Error("Invalid settings received by generator.");
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
// FIXME: remove ts-strict-ignore once `FakeAccountService` implements ts strict support
|
||||||
|
// @ts-strict-ignore
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { BehaviorSubject, filter, firstValueFrom, Subject } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@@ -23,6 +25,7 @@ import { Generators } from "../data";
|
|||||||
import {
|
import {
|
||||||
CredentialGeneratorConfiguration,
|
CredentialGeneratorConfiguration,
|
||||||
GeneratedCredential,
|
GeneratedCredential,
|
||||||
|
GenerateRequest,
|
||||||
GeneratorConstraints,
|
GeneratorConstraints,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
@@ -72,7 +75,8 @@ const SomeAlgorithm = "passphrase";
|
|||||||
const SomeCategory = "password";
|
const SomeCategory = "password";
|
||||||
const SomeNameKey = "passphraseKey";
|
const SomeNameKey = "passphraseKey";
|
||||||
const SomeGenerateKey = "generateKey";
|
const SomeGenerateKey = "generateKey";
|
||||||
const SomeGeneratedValueKey = "generatedValueKey";
|
const SomeCredentialTypeKey = "credentialTypeKey";
|
||||||
|
const SomeOnGeneratedMessageKey = "onGeneratedMessageKey";
|
||||||
const SomeCopyKey = "copyKey";
|
const SomeCopyKey = "copyKey";
|
||||||
const SomeUseGeneratedValueKey = "useGeneratedValueKey";
|
const SomeUseGeneratedValueKey = "useGeneratedValueKey";
|
||||||
|
|
||||||
@@ -82,7 +86,8 @@ const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePoli
|
|||||||
category: SomeCategory,
|
category: SomeCategory,
|
||||||
nameKey: SomeNameKey,
|
nameKey: SomeNameKey,
|
||||||
generateKey: SomeGenerateKey,
|
generateKey: SomeGenerateKey,
|
||||||
generatedValueKey: SomeGeneratedValueKey,
|
onGeneratedMessageKey: SomeOnGeneratedMessageKey,
|
||||||
|
credentialTypeKey: SomeCredentialTypeKey,
|
||||||
copyKey: SomeCopyKey,
|
copyKey: SomeCopyKey,
|
||||||
useGeneratedValueKey: SomeUseGeneratedValueKey,
|
useGeneratedValueKey: SomeUseGeneratedValueKey,
|
||||||
onlyOnRequest: false,
|
onlyOnRequest: false,
|
||||||
@@ -91,8 +96,13 @@ const SomeConfiguration: CredentialGeneratorConfiguration<SomeSettings, SomePoli
|
|||||||
create: (_randomizer) => {
|
create: (_randomizer) => {
|
||||||
return {
|
return {
|
||||||
generate: (request, settings) => {
|
generate: (request, settings) => {
|
||||||
const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo;
|
const result = new GeneratedCredential(
|
||||||
const result = new GeneratedCredential(credential, SomeAlgorithm, SomeTime);
|
settings.foo,
|
||||||
|
SomeAlgorithm,
|
||||||
|
SomeTime,
|
||||||
|
request.source,
|
||||||
|
request.website,
|
||||||
|
);
|
||||||
return Promise.resolve(result);
|
return Promise.resolve(result);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -191,7 +201,33 @@ describe("CredentialGeneratorService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("generate$", () => {
|
describe("generate$", () => {
|
||||||
it("emits a generation for the active user when subscribed", async () => {
|
it("completes when `on$` completes", async () => {
|
||||||
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(
|
||||||
|
randomizer,
|
||||||
|
stateProvider,
|
||||||
|
policyService,
|
||||||
|
apiService,
|
||||||
|
i18nService,
|
||||||
|
encryptorProvider,
|
||||||
|
accountService,
|
||||||
|
);
|
||||||
|
const on$ = new Subject<GenerateRequest>();
|
||||||
|
let complete = false;
|
||||||
|
|
||||||
|
// confirm no emission during subscription
|
||||||
|
generator.generate$(SomeConfiguration, { on$ }).subscribe({
|
||||||
|
complete: () => {
|
||||||
|
complete = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
on$.complete();
|
||||||
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(complete).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes request.source in the generated credential", async () => {
|
||||||
const settings = { foo: "value" };
|
const settings = { foo: "value" };
|
||||||
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
const generator = new CredentialGeneratorService(
|
const generator = new CredentialGeneratorService(
|
||||||
@@ -203,14 +239,35 @@ describe("CredentialGeneratorService", () => {
|
|||||||
encryptorProvider,
|
encryptorProvider,
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const on$ = new BehaviorSubject<GenerateRequest>({ source: "some source" });
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ }));
|
||||||
|
|
||||||
const result = await generated.expectEmission();
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
expect(result).toEqual(new GeneratedCredential("value", SomeAlgorithm, SomeTime));
|
expect(result.source).toEqual("some source");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("follows the active user", async () => {
|
it("includes request.website in the generated credential", async () => {
|
||||||
|
const settings = { foo: "value" };
|
||||||
|
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
||||||
|
const generator = new CredentialGeneratorService(
|
||||||
|
randomizer,
|
||||||
|
stateProvider,
|
||||||
|
policyService,
|
||||||
|
apiService,
|
||||||
|
i18nService,
|
||||||
|
encryptorProvider,
|
||||||
|
accountService,
|
||||||
|
);
|
||||||
|
const on$ = new BehaviorSubject({ website: "some website" });
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ }));
|
||||||
|
|
||||||
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
|
expect(result.website).toEqual("some website");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the active user's settings", async () => {
|
||||||
const someSettings = { foo: "some value" };
|
const someSettings = { foo: "some value" };
|
||||||
const anotherSettings = { foo: "another value" };
|
const anotherSettings = { foo: "another value" };
|
||||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
||||||
@@ -224,34 +281,11 @@ describe("CredentialGeneratorService", () => {
|
|||||||
encryptorProvider,
|
encryptorProvider,
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
const on$ = new BehaviorSubject({});
|
||||||
|
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { on$ }));
|
||||||
|
|
||||||
await accountService.switchAccount(AnotherUser);
|
await accountService.switchAccount(AnotherUser);
|
||||||
await generated.pauseUntilReceived(2);
|
on$.next({});
|
||||||
generated.unsubscribe();
|
|
||||||
|
|
||||||
expect(generated.emissions).toEqual([
|
|
||||||
new GeneratedCredential("some value", SomeAlgorithm, SomeTime),
|
|
||||||
new GeneratedCredential("another value", SomeAlgorithm, SomeTime),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits a generation when the settings change", async () => {
|
|
||||||
const someSettings = { foo: "some value" };
|
|
||||||
const anotherSettings = { foo: "another value" };
|
|
||||||
await stateProvider.setUserState(SettingsKey, someSettings, SomeUser);
|
|
||||||
const generator = new CredentialGeneratorService(
|
|
||||||
randomizer,
|
|
||||||
stateProvider,
|
|
||||||
policyService,
|
|
||||||
apiService,
|
|
||||||
i18nService,
|
|
||||||
encryptorProvider,
|
|
||||||
accountService,
|
|
||||||
);
|
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration));
|
|
||||||
|
|
||||||
await stateProvider.setUserState(SettingsKey, anotherSettings, SomeUser);
|
|
||||||
await generated.pauseUntilReceived(2);
|
await generated.pauseUntilReceived(2);
|
||||||
generated.unsubscribe();
|
generated.unsubscribe();
|
||||||
|
|
||||||
@@ -265,78 +299,6 @@ describe("CredentialGeneratorService", () => {
|
|||||||
it.todo("errors when the settings error");
|
it.todo("errors when the settings error");
|
||||||
it.todo("completes when the settings complete");
|
it.todo("completes when the settings complete");
|
||||||
|
|
||||||
it("includes `website$`'s last emitted value", async () => {
|
|
||||||
const settings = { foo: "value" };
|
|
||||||
await stateProvider.setUserState(SettingsKey, settings, SomeUser);
|
|
||||||
const generator = new CredentialGeneratorService(
|
|
||||||
randomizer,
|
|
||||||
stateProvider,
|
|
||||||
policyService,
|
|
||||||
apiService,
|
|
||||||
i18nService,
|
|
||||||
encryptorProvider,
|
|
||||||
accountService,
|
|
||||||
);
|
|
||||||
const website$ = new BehaviorSubject("some website");
|
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ }));
|
|
||||||
|
|
||||||
const result = await generated.expectEmission();
|
|
||||||
|
|
||||||
expect(result).toEqual(
|
|
||||||
new GeneratedCredential("some website|value", SomeAlgorithm, SomeTime),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("errors when `website$` errors", async () => {
|
|
||||||
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
|
||||||
const generator = new CredentialGeneratorService(
|
|
||||||
randomizer,
|
|
||||||
stateProvider,
|
|
||||||
policyService,
|
|
||||||
apiService,
|
|
||||||
i18nService,
|
|
||||||
encryptorProvider,
|
|
||||||
accountService,
|
|
||||||
);
|
|
||||||
const website$ = new BehaviorSubject("some website");
|
|
||||||
let error = null;
|
|
||||||
|
|
||||||
generator.generate$(SomeConfiguration, { website$ }).subscribe({
|
|
||||||
error: (e: unknown) => {
|
|
||||||
error = e;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
website$.error({ some: "error" });
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(error).toEqual({ some: "error" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("completes when `website$` completes", async () => {
|
|
||||||
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
|
||||||
const generator = new CredentialGeneratorService(
|
|
||||||
randomizer,
|
|
||||||
stateProvider,
|
|
||||||
policyService,
|
|
||||||
apiService,
|
|
||||||
i18nService,
|
|
||||||
encryptorProvider,
|
|
||||||
accountService,
|
|
||||||
);
|
|
||||||
const website$ = new BehaviorSubject("some website");
|
|
||||||
let completed = false;
|
|
||||||
|
|
||||||
generator.generate$(SomeConfiguration, { website$ }).subscribe({
|
|
||||||
complete: () => {
|
|
||||||
completed = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
website$.complete();
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(completed).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits a generation for a specific user when `user$` supplied", async () => {
|
it("emits a generation for a specific user when `user$` supplied", async () => {
|
||||||
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
||||||
await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser);
|
await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser);
|
||||||
@@ -350,38 +312,17 @@ describe("CredentialGeneratorService", () => {
|
|||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
const userId$ = new BehaviorSubject(AnotherUser).asObservable();
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
|
const on$ = new Subject<GenerateRequest>();
|
||||||
|
const generated = new ObservableTracker(
|
||||||
|
generator.generate$(SomeConfiguration, { on$, userId$ }),
|
||||||
|
);
|
||||||
|
on$.next({});
|
||||||
|
|
||||||
const result = await generated.expectEmission();
|
const result = await generated.expectEmission();
|
||||||
|
|
||||||
expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime));
|
expect(result).toEqual(new GeneratedCredential("another", SomeAlgorithm, SomeTime));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits a generation for a specific user when `user$` emits", async () => {
|
|
||||||
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
|
||||||
await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser);
|
|
||||||
const generator = new CredentialGeneratorService(
|
|
||||||
randomizer,
|
|
||||||
stateProvider,
|
|
||||||
policyService,
|
|
||||||
apiService,
|
|
||||||
i18nService,
|
|
||||||
encryptorProvider,
|
|
||||||
accountService,
|
|
||||||
);
|
|
||||||
const userId = new BehaviorSubject(SomeUser);
|
|
||||||
const userId$ = userId.pipe(filter((u) => !!u));
|
|
||||||
const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ }));
|
|
||||||
|
|
||||||
userId.next(AnotherUser);
|
|
||||||
const result = await generated.pauseUntilReceived(2);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
new GeneratedCredential("value", SomeAlgorithm, SomeTime),
|
|
||||||
new GeneratedCredential("another", SomeAlgorithm, SomeTime),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("errors when `user$` errors", async () => {
|
it("errors when `user$` errors", async () => {
|
||||||
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
await stateProvider.setUserState(SettingsKey, null, SomeUser);
|
||||||
const generator = new CredentialGeneratorService(
|
const generator = new CredentialGeneratorService(
|
||||||
@@ -393,10 +334,11 @@ describe("CredentialGeneratorService", () => {
|
|||||||
encryptorProvider,
|
encryptorProvider,
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
const on$ = new Subject<GenerateRequest>();
|
||||||
const userId$ = new BehaviorSubject(SomeUser);
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
generator.generate$(SomeConfiguration, { userId$ }).subscribe({
|
generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({
|
||||||
error: (e: unknown) => {
|
error: (e: unknown) => {
|
||||||
error = e;
|
error = e;
|
||||||
},
|
},
|
||||||
@@ -418,10 +360,11 @@ describe("CredentialGeneratorService", () => {
|
|||||||
encryptorProvider,
|
encryptorProvider,
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
|
const on$ = new Subject<GenerateRequest>();
|
||||||
const userId$ = new BehaviorSubject(SomeUser);
|
const userId$ = new BehaviorSubject(SomeUser);
|
||||||
let completed = false;
|
let completed = false;
|
||||||
|
|
||||||
generator.generate$(SomeConfiguration, { userId$ }).subscribe({
|
generator.generate$(SomeConfiguration, { on$, userId$ }).subscribe({
|
||||||
complete: () => {
|
complete: () => {
|
||||||
completed = true;
|
completed = true;
|
||||||
},
|
},
|
||||||
@@ -444,7 +387,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
encryptorProvider,
|
encryptorProvider,
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<GenerateRequest>();
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
|
|
||||||
// confirm no emission during subscription
|
// confirm no emission during subscription
|
||||||
@@ -455,7 +398,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
expect(results.length).toEqual(0);
|
expect(results.length).toEqual(0);
|
||||||
|
|
||||||
// confirm forwarded emission
|
// confirm forwarded emission
|
||||||
on$.next();
|
on$.next({});
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]);
|
expect(results).toEqual([new GeneratedCredential("value", SomeAlgorithm, SomeTime)]);
|
||||||
|
|
||||||
@@ -465,7 +408,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
expect(results.length).toBe(1);
|
expect(results.length).toBe(1);
|
||||||
|
|
||||||
// confirm forwarded emission takes latest value
|
// confirm forwarded emission takes latest value
|
||||||
on$.next();
|
on$.next({});
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
|
|
||||||
@@ -486,7 +429,7 @@ describe("CredentialGeneratorService", () => {
|
|||||||
encryptorProvider,
|
encryptorProvider,
|
||||||
accountService,
|
accountService,
|
||||||
);
|
);
|
||||||
const on$ = new Subject<void>();
|
const on$ = new Subject<GenerateRequest>();
|
||||||
let error: any = null;
|
let error: any = null;
|
||||||
|
|
||||||
// confirm no emission during subscription
|
// confirm no emission during subscription
|
||||||
@@ -501,35 +444,8 @@ describe("CredentialGeneratorService", () => {
|
|||||||
expect(error).toEqual({ some: "error" });
|
expect(error).toEqual({ some: "error" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("completes when `on$` completes", async () => {
|
|
||||||
await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser);
|
|
||||||
const generator = new CredentialGeneratorService(
|
|
||||||
randomizer,
|
|
||||||
stateProvider,
|
|
||||||
policyService,
|
|
||||||
apiService,
|
|
||||||
i18nService,
|
|
||||||
encryptorProvider,
|
|
||||||
accountService,
|
|
||||||
);
|
|
||||||
const on$ = new Subject<void>();
|
|
||||||
let complete = false;
|
|
||||||
|
|
||||||
// confirm no emission during subscription
|
|
||||||
generator.generate$(SomeConfiguration, { on$ }).subscribe({
|
|
||||||
complete: () => {
|
|
||||||
complete = true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
on$.complete();
|
|
||||||
await awaitAsync();
|
|
||||||
|
|
||||||
expect(complete).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: test these when the fake state provider can delay its first emission
|
// FIXME: test these when the fake state provider can delay its first emission
|
||||||
it.todo("emits when settings$ become available if on$ is called before they're ready.");
|
it.todo("emits when settings$ become available if on$ is called before they're ready.");
|
||||||
it.todo("emits when website$ become available if on$ is called before they're ready.");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("algorithms", () => {
|
describe("algorithms", () => {
|
||||||
|
|||||||
@@ -2,20 +2,15 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
|
||||||
concat,
|
|
||||||
concatMap,
|
concatMap,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
endWith,
|
endWith,
|
||||||
filter,
|
filter,
|
||||||
first,
|
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
ignoreElements,
|
ignoreElements,
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
share,
|
|
||||||
skipUntil,
|
|
||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
@@ -34,9 +29,9 @@ import {
|
|||||||
SingleUserDependency,
|
SingleUserDependency,
|
||||||
UserDependency,
|
UserDependency,
|
||||||
} from "@bitwarden/common/tools/dependencies";
|
} from "@bitwarden/common/tools/dependencies";
|
||||||
import { IntegrationId, IntegrationMetadata } from "@bitwarden/common/tools/integration";
|
import { IntegrationMetadata } from "@bitwarden/common/tools/integration";
|
||||||
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
import { RestClient } from "@bitwarden/common/tools/integration/rpc";
|
||||||
import { anyComplete } from "@bitwarden/common/tools/rx";
|
import { anyComplete, withLatestReady } from "@bitwarden/common/tools/rx";
|
||||||
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
|
||||||
@@ -57,6 +52,7 @@ import {
|
|||||||
CredentialPreference,
|
CredentialPreference,
|
||||||
isForwarderIntegration,
|
isForwarderIntegration,
|
||||||
ForwarderIntegration,
|
ForwarderIntegration,
|
||||||
|
GenerateRequest,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
CredentialGeneratorConfiguration as Configuration,
|
CredentialGeneratorConfiguration as Configuration,
|
||||||
@@ -69,19 +65,7 @@ import { PREFERENCES } from "./credential-preferences";
|
|||||||
|
|
||||||
type Policy$Dependencies = UserDependency;
|
type Policy$Dependencies = UserDependency;
|
||||||
type Settings$Dependencies = Partial<UserDependency>;
|
type Settings$Dependencies = Partial<UserDependency>;
|
||||||
type Generate$Dependencies = Simplify<Partial<OnDependency> & Partial<UserDependency>> & {
|
type Generate$Dependencies = Simplify<OnDependency<GenerateRequest> & Partial<UserDependency>>;
|
||||||
/** Emits the active website when subscribed.
|
|
||||||
*
|
|
||||||
* The generator does not respond to emissions of this interface;
|
|
||||||
* If it is provided, the generator blocks until a value becomes available.
|
|
||||||
* When `website$` is omitted, the generator uses the empty string instead.
|
|
||||||
* When `website$` completes, the generator completes.
|
|
||||||
* When `website$` errors, the generator forwards the error.
|
|
||||||
*/
|
|
||||||
website$?: Observable<string>;
|
|
||||||
|
|
||||||
integration$?: Observable<IntegrationId>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Algorithms$Dependencies = Partial<UserDependency>;
|
type Algorithms$Dependencies = Partial<UserDependency>;
|
||||||
|
|
||||||
@@ -111,43 +95,20 @@ export class CredentialGeneratorService {
|
|||||||
|
|
||||||
/** Generates a stream of credentials
|
/** Generates a stream of credentials
|
||||||
* @param configuration determines which generator's settings are loaded
|
* @param configuration determines which generator's settings are loaded
|
||||||
* @param dependencies.on$ when specified, a new credential is emitted when
|
* @param dependencies.on$ Required. A new credential is emitted when this emits.
|
||||||
* this emits. Otherwise, a new credential is emitted when the settings
|
|
||||||
* update.
|
|
||||||
*/
|
*/
|
||||||
generate$<Settings extends object, Policy>(
|
generate$<Settings extends object, Policy>(
|
||||||
configuration: Readonly<Configuration<Settings, Policy>>,
|
configuration: Readonly<Configuration<Settings, Policy>>,
|
||||||
dependencies?: Generate$Dependencies,
|
dependencies: Generate$Dependencies,
|
||||||
) {
|
) {
|
||||||
// instantiate the engine
|
|
||||||
const engine = configuration.engine.create(this.getDependencyProvider());
|
const engine = configuration.engine.create(this.getDependencyProvider());
|
||||||
|
|
||||||
// stream blocks until all of these values are received
|
|
||||||
const website$ = dependencies?.website$ ?? new BehaviorSubject<string>(null);
|
|
||||||
const request$ = website$.pipe(map((website) => ({ website })));
|
|
||||||
const settings$ = this.settings$(configuration, dependencies);
|
const settings$ = this.settings$(configuration, dependencies);
|
||||||
|
|
||||||
// if on$ triggers before settings are loaded, trigger as soon
|
|
||||||
// as they become available.
|
|
||||||
let readyOn$: Observable<any> = null;
|
|
||||||
if (dependencies?.on$) {
|
|
||||||
const NO_EMISSIONS = {};
|
|
||||||
const ready$ = combineLatest([settings$, request$]).pipe(
|
|
||||||
first(null, NO_EMISSIONS),
|
|
||||||
filter((value) => value !== NO_EMISSIONS),
|
|
||||||
share(),
|
|
||||||
);
|
|
||||||
readyOn$ = concat(
|
|
||||||
dependencies.on$?.pipe(switchMap(() => ready$)),
|
|
||||||
dependencies.on$.pipe(skipUntil(ready$)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// generation proper
|
// generation proper
|
||||||
const generate$ = (readyOn$ ?? settings$).pipe(
|
const generate$ = dependencies.on$.pipe(
|
||||||
withLatestFrom(request$, settings$),
|
withLatestReady(settings$),
|
||||||
concatMap(([, request, settings]) => engine.generate(request, settings)),
|
concatMap(([request, settings]) => engine.generate(request, settings)),
|
||||||
takeUntil(anyComplete([request$, settings$])),
|
takeUntil(anyComplete([settings$])),
|
||||||
);
|
);
|
||||||
|
|
||||||
return generate$;
|
return generate$;
|
||||||
@@ -256,7 +217,8 @@ export class CredentialGeneratorService {
|
|||||||
category: generator.category,
|
category: generator.category,
|
||||||
name: integration ? integration.name : this.i18nService.t(generator.nameKey),
|
name: integration ? integration.name : this.i18nService.t(generator.nameKey),
|
||||||
generate: this.i18nService.t(generator.generateKey),
|
generate: this.i18nService.t(generator.generateKey),
|
||||||
generatedValue: this.i18nService.t(generator.generatedValueKey),
|
onGeneratedMessage: this.i18nService.t(generator.onGeneratedMessageKey),
|
||||||
|
credentialType: this.i18nService.t(generator.credentialTypeKey),
|
||||||
copy: this.i18nService.t(generator.copyKey),
|
copy: this.i18nService.t(generator.copyKey),
|
||||||
useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey),
|
useGeneratedValue: this.i18nService.t(generator.useGeneratedValueKey),
|
||||||
onlyOnRequest: generator.onlyOnRequest,
|
onlyOnRequest: generator.onlyOnRequest,
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export type AlgorithmInfo = {
|
|||||||
/* Localized generate button label */
|
/* Localized generate button label */
|
||||||
generate: string;
|
generate: string;
|
||||||
|
|
||||||
|
/** Localized "credential generated" informational message */
|
||||||
|
onGeneratedMessage: string;
|
||||||
|
|
||||||
/* Localized copy button label */
|
/* Localized copy button label */
|
||||||
copy: string;
|
copy: string;
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ export type AlgorithmInfo = {
|
|||||||
useGeneratedValue: string;
|
useGeneratedValue: string;
|
||||||
|
|
||||||
/* Localized generated value label */
|
/* Localized generated value label */
|
||||||
generatedValue: string;
|
credentialType: string;
|
||||||
|
|
||||||
/** Localized algorithm description */
|
/** Localized algorithm description */
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -79,17 +82,22 @@ export type CredentialGeneratorInfo = {
|
|||||||
/** Localization key for the credential description*/
|
/** Localization key for the credential description*/
|
||||||
descriptionKey?: string;
|
descriptionKey?: string;
|
||||||
|
|
||||||
/* Localization key for the generate command label */
|
/** Localization key for the generate command label */
|
||||||
generateKey: string;
|
generateKey: string;
|
||||||
|
|
||||||
/* Localization key for the copy button label */
|
/** Localization key for the copy button label */
|
||||||
copyKey: string;
|
copyKey: string;
|
||||||
|
|
||||||
/* Localized "use generated credential" button label */
|
/** Localization key for the "credential generated" informational message */
|
||||||
|
onGeneratedMessageKey: string;
|
||||||
|
|
||||||
|
/** Localized "use generated credential" button label */
|
||||||
useGeneratedValueKey: string;
|
useGeneratedValueKey: string;
|
||||||
|
|
||||||
/* Localization key for describing values generated by this generator */
|
/** Localization key for describing the kind of credential generated
|
||||||
generatedValueKey: string;
|
* by this generator.
|
||||||
|
*/
|
||||||
|
credentialTypeKey: string;
|
||||||
|
|
||||||
/** When true, credential generation must be explicitly requested.
|
/** When true, credential generation must be explicitly requested.
|
||||||
* @remarks this property is useful when credential generation
|
* @remarks this property is useful when credential generation
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { GenerationRequest } from "@bitwarden/common/tools/types";
|
import { GenerateRequest } from "./generate-request";
|
||||||
|
|
||||||
import { GeneratedCredential } from "./generated-credential";
|
import { GeneratedCredential } from "./generated-credential";
|
||||||
|
|
||||||
/** An algorithm that generates credentials. */
|
/** An algorithm that generates credentials. */
|
||||||
@@ -8,5 +7,5 @@ export type CredentialGenerator<Settings> = {
|
|||||||
* @param request runtime parameters
|
* @param request runtime parameters
|
||||||
* @param settings stored parameters
|
* @param settings stored parameters
|
||||||
*/
|
*/
|
||||||
generate: (request: GenerationRequest, settings: Settings) => Promise<GeneratedCredential>;
|
generate: (request: GenerateRequest, settings: Settings) => Promise<GeneratedCredential>;
|
||||||
};
|
};
|
||||||
|
|||||||
24
libs/tools/generator/core/src/types/generate-request.ts
Normal file
24
libs/tools/generator/core/src/types/generate-request.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/** Contextual information about the application state when a generator is invoked.
|
||||||
|
*/
|
||||||
|
export type GenerateRequest = {
|
||||||
|
/** Traces the origin of the generation request. This parameter is
|
||||||
|
* copied to the generated credential.
|
||||||
|
*
|
||||||
|
* @remarks This parameter it is provided solely so that generator
|
||||||
|
* consumers can differentiate request sources from one another.
|
||||||
|
* It never affects the random output of the generator algorithms,
|
||||||
|
* and it is never communicated to 3rd party systems. It MAY be
|
||||||
|
* tracked in the generator history.
|
||||||
|
*/
|
||||||
|
source?: string;
|
||||||
|
|
||||||
|
/** Traces the website associated with a generated credential.
|
||||||
|
*
|
||||||
|
* @remarks Random generators MUST NOT depend upon the website during credential
|
||||||
|
* generation. Non-random generators MAY include the website in the generated
|
||||||
|
* credential (e.g. a catchall email address). This parameter MAY be transmitted
|
||||||
|
* to 3rd party systems (e.g. as the description for a forwarding email).
|
||||||
|
* It MAY be tracked in the generator history.
|
||||||
|
*/
|
||||||
|
website?: string;
|
||||||
|
};
|
||||||
@@ -11,11 +11,15 @@ export class GeneratedCredential {
|
|||||||
* @param generationDate The date that the credential was generated.
|
* @param generationDate The date that the credential was generated.
|
||||||
* Numeric values should are interpreted using {@link Date.valueOf}
|
* Numeric values should are interpreted using {@link Date.valueOf}
|
||||||
* semantics.
|
* semantics.
|
||||||
|
* @param source traces the origin of the request that generated this credential.
|
||||||
|
* @param website traces the website associated with the generated credential.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
readonly credential: string,
|
readonly credential: string,
|
||||||
readonly category: CredentialAlgorithm,
|
readonly category: CredentialAlgorithm,
|
||||||
generationDate: Date | number,
|
generationDate: Date | number,
|
||||||
|
readonly source?: string,
|
||||||
|
readonly website?: string,
|
||||||
) {
|
) {
|
||||||
if (typeof generationDate === "number") {
|
if (typeof generationDate === "number") {
|
||||||
this.generationDate = new Date(generationDate);
|
this.generationDate = new Date(generationDate);
|
||||||
@@ -25,7 +29,7 @@ export class GeneratedCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The date that the credential was generated */
|
/** The date that the credential was generated */
|
||||||
generationDate: Date;
|
readonly generationDate: Date;
|
||||||
|
|
||||||
/** Constructs a credential from its `toJSON` representation */
|
/** Constructs a credential from its `toJSON` representation */
|
||||||
static fromJSON(jsonValue: Jsonify<GeneratedCredential>) {
|
static fromJSON(jsonValue: Jsonify<GeneratedCredential>) {
|
||||||
@@ -38,6 +42,9 @@ export class GeneratedCredential {
|
|||||||
|
|
||||||
/** Serializes a credential to a JSON-compatible object */
|
/** Serializes a credential to a JSON-compatible object */
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
// omits the source and website because they were introduced to solve
|
||||||
|
// UI bugs and it's not yet known whether there's a desire to support
|
||||||
|
// them in the generator history view.
|
||||||
return {
|
return {
|
||||||
credential: this.credential,
|
credential: this.credential,
|
||||||
category: this.category,
|
category: this.category,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export * from "./credential-generator";
|
|||||||
export * from "./credential-generator-configuration";
|
export * from "./credential-generator-configuration";
|
||||||
export * from "./eff-username-generator-options";
|
export * from "./eff-username-generator-options";
|
||||||
export * from "./forwarder-options";
|
export * from "./forwarder-options";
|
||||||
|
export * from "./generate-request";
|
||||||
export * from "./generator-constraints";
|
export * from "./generator-constraints";
|
||||||
export * from "./generated-credential";
|
export * from "./generated-credential";
|
||||||
export * from "./generator-options";
|
export * from "./generator-options";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { Component, Input, OnInit } from "@angular/core";
|
import { Component, Input, OnInit } from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
import { BehaviorSubject, firstValueFrom, map, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
ToastService,
|
ToastService,
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { CredentialGeneratorService, Generators } from "@bitwarden/generator-core";
|
import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core";
|
||||||
|
|
||||||
import { SendFormConfig } from "../../abstractions/send-form-config.service";
|
import { SendFormConfig } from "../../abstractions/send-form-config.service";
|
||||||
import { SendFormContainer } from "../../send-form-container";
|
import { SendFormContainer } from "../../send-form-container";
|
||||||
@@ -121,8 +121,9 @@ export class SendOptionsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generatePassword = async () => {
|
generatePassword = async () => {
|
||||||
|
const on$ = new BehaviorSubject<GenerateRequest>({ source: "send" });
|
||||||
const generatedCredential = await firstValueFrom(
|
const generatedCredential = await firstValueFrom(
|
||||||
this.generatorService.generate$(Generators.password),
|
this.generatorService.generate$(Generators.password, { on$ }),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.sendOptionsForm.patchValue({
|
this.sendOptionsForm.patchValue({
|
||||||
|
|||||||
Reference in New Issue
Block a user