mirror of
https://github.com/bitwarden/browser
synced 2026-02-16 00:24:52 +00:00
Merge branch 'main' into ps/extension-refresh
This commit is contained in:
@@ -62,6 +62,10 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||
(enforcedPasswordPolicyOptions) =>
|
||||
(this.enforcedPolicyOptions ??= enforcedPasswordPolicyOptions),
|
||||
);
|
||||
|
||||
if (this.enforcedPolicyOptions?.minLength) {
|
||||
this.minimumLength = this.enforcedPolicyOptions.minLength;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<p bitTypography="body1">
|
||||
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
|
||||
</p>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="text"
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
[(ngModel)]="tokenValue"
|
||||
(input)="token.emit(tokenValue)"
|
||||
/>
|
||||
<bit-hint>
|
||||
<a bitLink href="#" appStopClick (click)="sendEmail(true)">
|
||||
{{ "sendVerificationCodeEmailAgain" | i18n }}
|
||||
</a></bit-hint
|
||||
>
|
||||
</bit-form-field>
|
||||
@@ -0,0 +1,109 @@
|
||||
import { DialogModule } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-two-factor-auth-email",
|
||||
templateUrl: "two-factor-auth-email.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
DialogModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
FormsModule,
|
||||
],
|
||||
providers: [I18nPipe],
|
||||
})
|
||||
export class TwoFactorAuthEmailComponent {
|
||||
@Output() token = new EventEmitter<string>();
|
||||
|
||||
twoFactorEmail: string = null;
|
||||
emailPromise: Promise<any>;
|
||||
tokenValue: string = "";
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected logService: LogService,
|
||||
protected apiService: ApiService,
|
||||
protected appIdService: AppIdService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const providerData = await this.twoFactorService.getProviders().then((providers) => {
|
||||
return providers.get(TwoFactorProviderType.Email);
|
||||
});
|
||||
this.twoFactorEmail = providerData.Email;
|
||||
|
||||
if ((await this.twoFactorService.getProviders()).size > 1) {
|
||||
await this.sendEmail(false);
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmail(doToast: boolean) {
|
||||
if (this.emailPromise != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((await this.loginStrategyService.getEmail()) == null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("sessionTimeout"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const request = new TwoFactorEmailRequest();
|
||||
request.email = await this.loginStrategyService.getEmail();
|
||||
request.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||
request.ssoEmail2FaSessionToken =
|
||||
await this.loginStrategyService.getSsoEmail2FaSessionToken();
|
||||
request.deviceIdentifier = await this.appIdService.getAppId();
|
||||
request.authRequestAccessCode = await this.loginStrategyService.getAccessCode();
|
||||
request.authRequestId = await this.loginStrategyService.getAuthRequestId();
|
||||
this.emailPromise = this.apiService.postTwoFactorEmail(request);
|
||||
await this.emailPromise;
|
||||
if (doToast) {
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("verificationCodeEmailSent", this.twoFactorEmail),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
this.emailPromise = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div id="web-authn-frame" class="tw-mb-3" *ngIf="!webAuthnNewTab">
|
||||
<iframe id="webauthn_iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
</div>
|
||||
<ng-container *ngIf="webAuthnNewTab">
|
||||
<div class="content text-center" *ngIf="webAuthnNewTab">
|
||||
<p class="text-center">{{ "webAuthnNewTab" | i18n }}</p>
|
||||
<button type="button" class="btn primary block" (click)="authWebAuthn()" appStopClick>
|
||||
{{ "webAuthnNewTabOpen" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,131 @@
|
||||
import { DialogModule } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-two-factor-auth-webauthn",
|
||||
templateUrl: "two-factor-auth-webauthn.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
DialogModule,
|
||||
ButtonModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
ReactiveFormsModule,
|
||||
FormFieldModule,
|
||||
AsyncActionsModule,
|
||||
FormsModule,
|
||||
],
|
||||
providers: [I18nPipe],
|
||||
})
|
||||
export class TwoFactorAuthWebAuthnComponent {
|
||||
@Output() token = new EventEmitter<string>();
|
||||
|
||||
webAuthnReady = false;
|
||||
webAuthnNewTab = false;
|
||||
webAuthnSupported = false;
|
||||
webAuthn: WebAuthnIFrame = null;
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
@Inject(WINDOW) protected win: Window,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
|
||||
if (this.platformUtilsService.getClientType() == ClientType.Browser) {
|
||||
// FIXME: Chromium 110 has broken WebAuthn support in extensions via an iframe
|
||||
this.webAuthnNewTab = true;
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (this.route.snapshot.paramMap.has("webAuthnResponse")) {
|
||||
this.token.emit(this.route.snapshot.paramMap.get("webAuthnResponse"));
|
||||
}
|
||||
|
||||
this.cleanupWebAuthn();
|
||||
|
||||
if (this.win != null && this.webAuthnSupported) {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
this.webAuthn = new WebAuthnIFrame(
|
||||
this.win,
|
||||
webVaultUrl,
|
||||
this.webAuthnNewTab,
|
||||
this.platformUtilsService,
|
||||
this.i18nService,
|
||||
(token: string) => {
|
||||
this.token.emit(token);
|
||||
},
|
||||
(error: string) => {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("webauthnCancelOrTimeout"),
|
||||
);
|
||||
},
|
||||
(info: string) => {
|
||||
if (info === "ready") {
|
||||
this.webAuthnReady = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!this.webAuthnNewTab) {
|
||||
setTimeout(async () => {
|
||||
await this.authWebAuthn();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.cleanupWebAuthn();
|
||||
}
|
||||
|
||||
async authWebAuthn() {
|
||||
const providerData = (await this.twoFactorService.getProviders()).get(
|
||||
TwoFactorProviderType.WebAuthn,
|
||||
);
|
||||
|
||||
if (!this.webAuthnSupported || this.webAuthn == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.webAuthn.init(providerData);
|
||||
}
|
||||
|
||||
private cleanupWebAuthn() {
|
||||
if (this.webAuthn != null) {
|
||||
this.webAuthn.stop();
|
||||
this.webAuthn.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<form [bitSubmit]="submitForm" [formGroup]="formGroup" autocomplete="off">
|
||||
<div class="tw-min-w-96">
|
||||
<app-two-factor-auth-email
|
||||
(token)="token = $event"
|
||||
*ngIf="selectedProviderType === providerType.Email"
|
||||
/>
|
||||
<app-two-factor-auth-authenticator
|
||||
(token)="token = $event"
|
||||
*ngIf="selectedProviderType === providerType.Authenticator"
|
||||
@@ -8,6 +12,10 @@
|
||||
(token)="token = $event"
|
||||
*ngIf="selectedProviderType === providerType.Yubikey"
|
||||
/>
|
||||
<app-two-factor-auth-webauthn
|
||||
(token)="token = $event; submitForm()"
|
||||
*ngIf="selectedProviderType === providerType.WebAuthn"
|
||||
/>
|
||||
<bit-form-control *ngIf="selectedProviderType != null">
|
||||
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="remember" />
|
||||
@@ -27,7 +35,7 @@
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
*ngIf="selectedProviderType != null"
|
||||
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.WebAuthn"
|
||||
>
|
||||
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ actionButtonText }} </span>
|
||||
</button>
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
import { CaptchaProtectedComponent } from "../captcha-protected.component";
|
||||
|
||||
import { TwoFactorAuthAuthenticatorComponent } from "./two-factor-auth-authenticator.component";
|
||||
import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component";
|
||||
import { TwoFactorAuthWebAuthnComponent } from "./two-factor-auth-webauthn.component";
|
||||
import { TwoFactorAuthYubikeyComponent } from "./two-factor-auth-yubikey.component";
|
||||
import {
|
||||
TwoFactorOptionsDialogResult,
|
||||
@@ -60,7 +62,9 @@ import {
|
||||
ButtonModule,
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorAuthAuthenticatorComponent,
|
||||
TwoFactorAuthEmailComponent,
|
||||
TwoFactorAuthYubikeyComponent,
|
||||
TwoFactorAuthWebAuthnComponent,
|
||||
],
|
||||
providers: [I18nPipe],
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { TwoFactorComponent } from "./two-factor.component";
|
||||
|
||||
@@ -71,6 +72,7 @@ describe("TwoFactorComponent", () => {
|
||||
let mockConfigService: MockProxy<ConfigService>;
|
||||
let mockMasterPasswordService: FakeMasterPasswordService;
|
||||
let mockAccountService: FakeAccountService;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
|
||||
let mockUserDecryptionOpts: {
|
||||
noMasterPassword: UserDecryptionOptions;
|
||||
@@ -102,6 +104,7 @@ describe("TwoFactorComponent", () => {
|
||||
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
mockAccountService = mockAccountServiceWith(userId);
|
||||
mockToastService = mock<ToastService>();
|
||||
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||
|
||||
mockUserDecryptionOpts = {
|
||||
@@ -182,6 +185,7 @@ describe("TwoFactorComponent", () => {
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: ToastService, useValue: mockToastService },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
@@ -94,6 +95,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
protected configService: ConfigService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService);
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
@@ -474,6 +476,15 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
||||
}
|
||||
|
||||
async launchDuoFrameless() {
|
||||
if (this.duoFramelessUrl === null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.launchUri(this.duoFramelessUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import { StopClickDirective } from "./directives/stop-click.directive";
|
||||
import { StopPropDirective } from "./directives/stop-prop.directive";
|
||||
import { TrueFalseValueDirective } from "./directives/true-false-value.directive";
|
||||
import { CreditCardNumberPipe } from "./pipes/credit-card-number.pipe";
|
||||
import { PluralizePipe } from "./pipes/pluralize.pipe";
|
||||
import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe";
|
||||
import { SearchPipe } from "./pipes/search.pipe";
|
||||
import { UserNamePipe } from "./pipes/user-name.pipe";
|
||||
@@ -162,6 +163,7 @@ import { IconComponent } from "./vault/components/icon.component";
|
||||
UserNamePipe,
|
||||
UserTypePipe,
|
||||
FingerprintPipe,
|
||||
PluralizePipe,
|
||||
],
|
||||
})
|
||||
export class JslibModule {}
|
||||
|
||||
11
libs/angular/src/pipes/pluralize.pipe.ts
Normal file
11
libs/angular/src/pipes/pluralize.pipe.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
@Pipe({
|
||||
name: "pluralize",
|
||||
standalone: true,
|
||||
})
|
||||
export class PluralizePipe implements PipeTransform {
|
||||
transform(count: number, singular: string, plural: string): string {
|
||||
return `${count} ${count === 1 ? singular : plural}`;
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,7 @@ import { BraintreeService } from "@bitwarden/common/billing/services/payment-pro
|
||||
import { StripeService } from "@bitwarden/common/billing/services/payment-processors/stripe.service";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
|
||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
@@ -157,11 +158,16 @@ import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/inter
|
||||
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
|
||||
import { Account } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
||||
import {
|
||||
TaskSchedulerService,
|
||||
DefaultTaskSchedulerService,
|
||||
} from "@bitwarden/common/platform/scheduling";
|
||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
|
||||
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/services/crypto.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation";
|
||||
import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service";
|
||||
import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service";
|
||||
@@ -409,6 +415,7 @@ const safeProviders: SafeProvider[] = [
|
||||
BillingAccountProfileStateService,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
KdfConfigServiceAbstraction,
|
||||
TaskSchedulerService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -432,6 +439,7 @@ const safeProviders: SafeProvider[] = [
|
||||
stateService: StateServiceAbstraction,
|
||||
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
encryptService: EncryptService,
|
||||
bulkEncryptService: BulkEncryptService,
|
||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||
configService: ConfigService,
|
||||
stateProvider: StateProvider,
|
||||
@@ -445,6 +453,7 @@ const safeProviders: SafeProvider[] = [
|
||||
stateService,
|
||||
autofillSettingsService,
|
||||
encryptService,
|
||||
bulkEncryptService,
|
||||
fileUploadService,
|
||||
configService,
|
||||
stateProvider,
|
||||
@@ -458,6 +467,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateServiceAbstraction,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
EncryptService,
|
||||
BulkEncryptService,
|
||||
CipherFileUploadServiceAbstraction,
|
||||
ConfigService,
|
||||
StateProvider,
|
||||
@@ -714,6 +724,8 @@ const safeProviders: SafeProvider[] = [
|
||||
AuthServiceAbstraction,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
StateEventRunnerService,
|
||||
TaskSchedulerService,
|
||||
LogService,
|
||||
LOCKED_CALLBACK,
|
||||
LOGOUT_CALLBACK,
|
||||
],
|
||||
@@ -812,6 +824,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
TaskSchedulerService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -824,10 +837,21 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: MultithreadEncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService, LOG_MAC_FAILURES],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: BulkEncryptService,
|
||||
useClass: BulkEncryptServiceImplementation,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EventUploadServiceAbstraction,
|
||||
useClass: EventUploadService,
|
||||
deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction],
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
StateProvider,
|
||||
LogService,
|
||||
AuthServiceAbstraction,
|
||||
TaskSchedulerService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EventCollectionServiceAbstraction,
|
||||
@@ -954,7 +978,13 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: DefaultConfigService,
|
||||
useClass: DefaultConfigService,
|
||||
deps: [ConfigApiServiceAbstraction, EnvironmentService, LogService, StateProvider],
|
||||
deps: [
|
||||
ConfigApiServiceAbstraction,
|
||||
EnvironmentService,
|
||||
LogService,
|
||||
StateProvider,
|
||||
AuthServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ConfigService,
|
||||
@@ -1209,6 +1239,11 @@ const safeProviders: SafeProvider[] = [
|
||||
new SubjectMessageSender(subject),
|
||||
deps: [INTRAPROCESS_MESSAGING_SUBJECT],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaskSchedulerService,
|
||||
useClass: DefaultTaskSchedulerService,
|
||||
deps: [LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ProviderApiServiceAbstraction,
|
||||
useClass: ProviderApiService,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class UnassignedItemsBannerApiService {
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
async getShowUnassignedCiphersBanner(): Promise<boolean> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
"/ciphers/has-unassigned-ciphers",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service";
|
||||
import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service";
|
||||
|
||||
describe("UnassignedItemsBanner", () => {
|
||||
let stateProvider: FakeStateProvider;
|
||||
let apiService: MockProxy<UnassignedItemsBannerApiService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
let organizationService: MockProxy<OrganizationService>;
|
||||
|
||||
const sutFactory = () =>
|
||||
new UnassignedItemsBannerService(
|
||||
stateProvider,
|
||||
apiService,
|
||||
environmentService,
|
||||
organizationService,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
const fakeAccountService = mockAccountServiceWith("userId" as UserId);
|
||||
stateProvider = new FakeStateProvider(fakeAccountService);
|
||||
apiService = mock();
|
||||
environmentService = mock();
|
||||
environmentService.environment$ = of(null);
|
||||
organizationService = mock();
|
||||
organizationService.organizations$ = of([]);
|
||||
});
|
||||
|
||||
it("shows the banner if showBanner local state is true", async () => {
|
||||
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
|
||||
showBanner.nextState(true);
|
||||
|
||||
const sut = sutFactory();
|
||||
expect(await firstValueFrom(sut.showBanner$)).toBe(true);
|
||||
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not show the banner if showBanner local state is false", async () => {
|
||||
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
|
||||
showBanner.nextState(false);
|
||||
|
||||
const sut = sutFactory();
|
||||
expect(await firstValueFrom(sut.showBanner$)).toBe(false);
|
||||
expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fetches from server if local state has not been set yet", async () => {
|
||||
apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true);
|
||||
|
||||
const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY);
|
||||
showBanner.nextState(undefined);
|
||||
|
||||
const sut = sutFactory();
|
||||
|
||||
expect(await firstValueFrom(sut.showBanner$)).toBe(true);
|
||||
expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, concatMap, map, startWith } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationService,
|
||||
canAccessOrgAdmin,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import {
|
||||
StateProvider,
|
||||
UNASSIGNED_ITEMS_BANNER_DISK,
|
||||
UserKeyDefinition,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service";
|
||||
|
||||
export const SHOW_BANNER_KEY = new UserKeyDefinition<boolean>(
|
||||
UNASSIGNED_ITEMS_BANNER_DISK,
|
||||
"showBanner",
|
||||
{
|
||||
deserializer: (b) => b,
|
||||
clearOn: [],
|
||||
},
|
||||
);
|
||||
|
||||
/** Displays a banner that tells users how to move their unassigned items into a collection. */
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class UnassignedItemsBannerService {
|
||||
private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY);
|
||||
|
||||
showBanner$ = this._showBanner.state$.pipe(
|
||||
concatMap(async (showBannerState) => {
|
||||
// null indicates that the user has not seen or dismissed the banner yet - get the flag from server
|
||||
if (showBannerState == null) {
|
||||
const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner();
|
||||
await this._showBanner.update(() => showBannerResponse);
|
||||
return showBannerResponse;
|
||||
}
|
||||
|
||||
return showBannerState;
|
||||
}),
|
||||
);
|
||||
|
||||
private adminConsoleOrg$ = this.organizationService.organizations$.pipe(
|
||||
map((orgs) => orgs.find((o) => canAccessOrgAdmin(o))),
|
||||
);
|
||||
|
||||
adminConsoleUrl$ = combineLatest([
|
||||
this.adminConsoleOrg$,
|
||||
this.environmentService.environment$,
|
||||
]).pipe(
|
||||
map(([org, environment]) => {
|
||||
if (org == null || environment == null) {
|
||||
return "#";
|
||||
}
|
||||
|
||||
return environment.getWebVaultUrl() + "/#/organizations/" + org.id;
|
||||
}),
|
||||
);
|
||||
|
||||
bannerText$ = this.environmentService.environment$.pipe(
|
||||
map((e) =>
|
||||
e?.getRegion() == Region.SelfHosted
|
||||
? "unassignedItemsBannerSelfHostNotice"
|
||||
: "unassignedItemsBannerNotice",
|
||||
),
|
||||
);
|
||||
|
||||
loading$ = combineLatest([this.adminConsoleUrl$, this.bannerText$]).pipe(
|
||||
startWith(true),
|
||||
map(() => false),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private apiService: UnassignedItemsBannerApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
private organizationService: OrganizationService,
|
||||
) {}
|
||||
|
||||
async hideBanner() {
|
||||
await this._showBanner.update(() => false);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { DialogService } from "@bitwarden/components";
|
||||
@Directive()
|
||||
export class AttachmentsComponent implements OnInit {
|
||||
@Input() cipherId: string;
|
||||
@Input() viewOnly: boolean;
|
||||
@Output() onUploadedAttachment = new EventEmitter();
|
||||
@Output() onDeletedAttachment = new EventEmitter();
|
||||
@Output() onReuploadedAttachment = new EventEmitter();
|
||||
|
||||
Reference in New Issue
Block a user