1
0
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:
Victoria League
2024-07-18 12:35:00 -04:00
committed by GitHub
536 changed files with 32338 additions and 7878 deletions

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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],
})

View File

@@ -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 },
],
});

View File

@@ -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);
}
}

View File

@@ -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 {}

View 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}`;
}
}

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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);
});
});

View File

@@ -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);
}
}

View File

@@ -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();