1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

Merge branch 'main' into PM-18027

This commit is contained in:
Jordan Aasen
2025-02-19 11:08:07 -08:00
committed by GitHub
704 changed files with 23650 additions and 12697 deletions

View File

@@ -129,10 +129,7 @@ export class OrganizationLayoutComponent implements OnInit {
),
);
this.integrationPageEnabled$ = combineLatest(
this.organization$,
this.configService.getFeatureFlag$(FeatureFlag.PM14505AdminConsoleIntegrationPage),
).pipe(map(([org, featureFlagEnabled]) => featureFlagEnabled && org.canAccessIntegrations));
this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations));
this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag(
FeatureFlag.AccountDeprovisioning,

View File

@@ -1,8 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
@@ -12,7 +14,6 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EventView } from "@bitwarden/common/models/view/event.view";
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
@@ -34,7 +35,7 @@ export interface EntityEventsDialogParams {
templateUrl: "entity-events.component.html",
standalone: true,
})
export class EntityEventsComponent implements OnInit {
export class EntityEventsComponent implements OnInit, OnDestroy {
loading = true;
continuationToken: string;
protected dataSource = new TableDataSource<EventView>();
@@ -59,13 +60,14 @@ export class EntityEventsComponent implements OnInit {
private apiService: ApiService,
private i18nService: I18nService,
private eventService: EventService,
private platformUtilsService: PlatformUtilsService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private organizationUserApiService: OrganizationUserApiService,
private formBuilder: FormBuilder,
private validationService: ValidationService,
private toastService: ToastService,
private router: Router,
private activeRoute: ActivatedRoute,
) {}
async ngOnInit() {
@@ -77,6 +79,21 @@ export class EntityEventsComponent implements OnInit {
await this.load();
}
async ngOnDestroy() {
await firstValueFrom(
this.activeRoute.queryParams.pipe(
switchMap(async (params) => {
await this.router.navigate([], {
queryParams: {
...params,
viewEvents: null,
},
});
}),
),
);
}
async load() {
try {
if (this.showUser) {

View File

@@ -7,9 +7,9 @@
{{ error }}
</bit-callout>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error">
<p bitTypography="body1">{{ "deleteManyOrganizationUsersWarningDesc" | i18n }}</p>
</bit-callout>
<p bitTypography="body1" *ngIf="users.length > 0 && !error">
{{ "deleteManyOrganizationUsersWarningDesc" | i18n }}
</p>
<bit-table>
<ng-container header>
<tr>

View File

@@ -3,9 +3,9 @@
*ngIf="{ enabled: accountDeprovisioningEnabled$ | async } as accountDeprovisioning"
>
<ng-container bitDialogTitle>
<h1 *ngIf="accountDeprovisioning.enabled; else nonMemberTitle">{{ bulkMemberTitle }}</h1>
<span *ngIf="accountDeprovisioning.enabled; else nonMemberTitle">{{ bulkMemberTitle }}</span>
<ng-template #nonMemberTitle>
<h1>{{ bulkTitle }}</h1>
{{ bulkTitle }}
</ng-template>
</ng-container>
@@ -46,10 +46,7 @@
</ng-template>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeUsersWarning" | i18n }}</p>
</bit-callout>
<p *ngIf="users.length > 0 && !error && isRevoking">{{ "revokeUsersWarning" | i18n }}</p>
<bit-table>
<ng-container header>
<tr>
@@ -109,7 +106,13 @@
</ng-container>
</div>
<ng-container bitDialogFooter>
<button type="button" bitButton *ngIf="!done && users.length > 0" [bitAction]="submit">
<button
type="button"
bitButton
*ngIf="!done && users.length > 0"
[bitAction]="submit"
buttonType="primary"
>
{{ accountDeprovisioning.enabled ? bulkMemberTitle : bulkTitle }}
</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>

View File

@@ -4,7 +4,6 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { authGuard } from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import {
canAccessOrgAdmin,
canAccessGroupsTab,
@@ -14,7 +13,6 @@ import {
canAccessSettingsTab,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
@@ -45,7 +43,6 @@ const routes: Routes = [
{
path: "integrations",
canActivate: [
canAccessFeature(FeatureFlag.PM14505AdminConsoleIntegrationPage),
isEnterpriseOrgGuard(false),
organizationPermissionsGuard(canAccessIntegrations),
],

View File

@@ -18,6 +18,8 @@ import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
@@ -41,6 +43,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
private organizationService: OrganizationService,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected accountService: AccountService,
configService: ConfigService,
i18nService: I18nService,
) {
super(
dialogService,
@@ -49,6 +53,8 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
policyService,
billingAccountProfileStateService,
accountService,
configService,
i18nService,
);
}

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { firstValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -11,6 +11,7 @@ import {
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -59,15 +60,13 @@ export class ExposedPasswordsReportComponent
this.isAdminConsoleActive = true;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
const userId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.organization = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(params.organizationId)),
);
this.manageableCiphers = await this.cipherService.getAll();
this.manageableCiphers = await this.cipherService.getAll(userId);
});
}

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { firstValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import {
@@ -10,6 +10,7 @@ import {
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -56,15 +57,13 @@ export class ReusedPasswordsReportComponent
this.isAdminConsoleActive = true;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
const userId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.organization = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(params.organizationId)),
);
this.manageableCiphers = await this.cipherService.getAll();
this.manageableCiphers = await this.cipherService.getAll(userId);
await super.ngOnInit();
});
}

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, map } from "rxjs";
import { firstValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import {
@@ -10,6 +10,7 @@ import {
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -59,15 +60,14 @@ export class WeakPasswordsReportComponent
this.isAdminConsoleActive = true;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
const userId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.organization = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(params.organizationId)),
);
this.manageableCiphers = await this.cipherService.getAll();
this.manageableCiphers = await this.cipherService.getAll(userId);
await super.ngOnInit();
});
}

View File

@@ -2,5 +2,9 @@
<bit-container>
<p>{{ "newOrganizationDesc" | i18n }}</p>
<app-organization-plans></app-organization-plans>
<app-organization-plans
[enableSecretsManagerByDefault]="secretsManager"
[plan]="plan"
[productTier]="productTier"
></app-organization-plans>
</bit-container>

View File

@@ -1,10 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild } from "@angular/core";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
import { PlanType, ProductTierType, ProductType } from "@bitwarden/common/billing/enums";
import { OrganizationPlansComponent } from "../../billing";
import { HeaderModule } from "../../layouts/header/header.module";
@@ -15,29 +16,34 @@ import { SharedModule } from "../../shared";
standalone: true,
imports: [SharedModule, OrganizationPlansComponent, HeaderModule],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: true })
orgPlansComponent: OrganizationPlansComponent;
export class CreateOrganizationComponent {
protected secretsManager = false;
protected plan: PlanType = PlanType.Free;
protected productTier: ProductTierType = ProductTierType.Free;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.plan === "families") {
this.orgPlansComponent.plan = PlanType.FamiliesAnnually;
this.orgPlansComponent.productTier = ProductTierType.Families;
} else if (qParams.plan === "teams") {
this.orgPlansComponent.plan = PlanType.TeamsAnnually;
this.orgPlansComponent.productTier = ProductTierType.Teams;
} else if (qParams.plan === "teamsStarter") {
this.orgPlansComponent.plan = PlanType.TeamsStarter;
this.orgPlansComponent.productTier = ProductTierType.TeamsStarter;
} else if (qParams.plan === "enterprise") {
this.orgPlansComponent.plan = PlanType.EnterpriseAnnually;
this.orgPlansComponent.productTier = ProductTierType.Enterprise;
constructor(private route: ActivatedRoute) {
this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => {
if (qParams.plan === "families" || qParams.productTier == ProductTierType.Families) {
this.plan = PlanType.FamiliesAnnually;
this.productTier = ProductTierType.Families;
} else if (qParams.plan === "teams" || qParams.productTier == ProductTierType.Teams) {
this.plan = PlanType.TeamsAnnually;
this.productTier = ProductTierType.Teams;
} else if (
qParams.plan === "teamsStarter" ||
qParams.productTier == ProductTierType.TeamsStarter
) {
this.plan = PlanType.TeamsStarter;
this.productTier = ProductTierType.TeamsStarter;
} else if (
qParams.plan === "enterprise" ||
qParams.productTier == ProductTierType.Enterprise
) {
this.plan = PlanType.EnterpriseAnnually;
this.productTier = ProductTierType.Enterprise;
}
this.secretsManager = qParams.product == ProductType.SecretsManager;
});
}
}

View File

@@ -17,3 +17,5 @@
<ng-container *ngIf="!loading; else loadingState">
<router-outlet></router-outlet>
</ng-container>
<bit-toast-container></bit-toast-container>

View File

@@ -74,10 +74,10 @@ describe("WebLoginComponentService", () => {
expect(service).toBeTruthy();
});
describe("getOrgPolicies", () => {
describe("getOrgPoliciesFromOrgInvite", () => {
it("returns undefined if organization invite is null", async () => {
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null);
const result = await service.getOrgPolicies();
const result = await service.getOrgPoliciesFromOrgInvite();
expect(result).toBeUndefined();
});
@@ -94,7 +94,7 @@ describe("WebLoginComponentService", () => {
organizationName: "org-name",
});
policyApiService.getPoliciesByToken.mockRejectedValue(error);
await service.getOrgPolicies();
await service.getOrgPoliciesFromOrgInvite();
expect(logService.error).toHaveBeenCalledWith(error);
});
@@ -130,7 +130,7 @@ describe("WebLoginComponentService", () => {
of(masterPasswordPolicyOptions),
);
const result = await service.getOrgPolicies();
const result = await service.getOrgPoliciesFromOrgInvite();
expect(result).toEqual({
policies: policies,

View File

@@ -48,7 +48,7 @@ export class WebLoginComponentService
this.clientType = this.platformUtilsService.getClientType();
}
async getOrgPolicies(): Promise<PasswordPolicies | null> {
async getOrgPoliciesFromOrgInvite(): Promise<PasswordPolicies | null> {
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
if (orgInvite != null) {

View File

@@ -1,6 +1,6 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<p bitTypography="body1">
{{ "recoverAccountTwoStepDesc" | i18n }}
{{ recoveryCodeMessage }}
<a
bitLink
href="https://bitwarden.com/help/lost-two-step-device/"

View File

@@ -1,13 +1,20 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import {
LoginStrategyServiceAbstraction,
PasswordLoginCredentials,
LoginSuccessHandlerService,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/two-factor-recovery.request";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@@ -16,13 +23,23 @@ import { KeyService } from "@bitwarden/key-management";
selector: "app-recover-two-factor",
templateUrl: "recover-two-factor.component.html",
})
export class RecoverTwoFactorComponent {
export class RecoverTwoFactorComponent implements OnInit {
protected formGroup = new FormGroup({
email: new FormControl(null, [Validators.required]),
masterPassword: new FormControl(null, [Validators.required]),
recoveryCode: new FormControl(null, [Validators.required]),
email: new FormControl("", [Validators.required]),
masterPassword: new FormControl("", [Validators.required]),
recoveryCode: new FormControl("", [Validators.required]),
});
/**
* Message to display to the user about the recovery code
*/
recoveryCodeMessage = "";
/**
* Whether the recovery code login feature flag is enabled
*/
private recoveryCodeLoginFeatureFlagEnabled = false;
constructor(
private router: Router,
private apiService: ApiService,
@@ -31,20 +48,35 @@ export class RecoverTwoFactorComponent {
private keyService: KeyService,
private loginStrategyService: LoginStrategyServiceAbstraction,
private toastService: ToastService,
private configService: ConfigService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private logService: LogService,
) {}
async ngOnInit() {
this.recoveryCodeLoginFeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.RecoveryCodeLogin,
);
this.recoveryCodeMessage = this.recoveryCodeLoginFeatureFlagEnabled
? this.i18nService.t("logInBelowUsingYourSingleUseRecoveryCode")
: this.i18nService.t("recoverAccountTwoStepDesc");
}
get email(): string {
return this.formGroup.value.email;
return this.formGroup.get("email")?.value ?? "";
}
get masterPassword(): string {
return this.formGroup.value.masterPassword;
return this.formGroup.get("masterPassword")?.value ?? "";
}
get recoveryCode(): string {
return this.formGroup.value.recoveryCode;
return this.formGroup.get("recoveryCode")?.value ?? "";
}
/**
* Handles the submission of the recovery code form.
*/
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
@@ -56,12 +88,90 @@ export class RecoverTwoFactorComponent {
request.email = this.email.trim().toLowerCase();
const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email);
request.masterPasswordHash = await this.keyService.hashMasterKey(this.masterPassword, key);
await this.apiService.postTwoFactorRecover(request);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("twoStepRecoverDisabled"),
});
await this.router.navigate(["/"]);
try {
await this.apiService.postTwoFactorRecover(request);
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("twoStepRecoverDisabled"),
});
if (!this.recoveryCodeLoginFeatureFlagEnabled) {
await this.router.navigate(["/"]);
return;
}
// Handle login after recovery if the feature flag is enabled
await this.handleRecoveryLogin(request);
} catch (e) {
const errorMessage = this.extractErrorMessage(e);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: errorMessage,
});
}
};
/**
* Handles the login process after a successful account recovery.
*/
private async handleRecoveryLogin(request: TwoFactorRecoveryRequest) {
// Build two-factor request to pass into PasswordLoginCredentials request using the 2FA recovery code and RecoveryCode type
const twoFactorRequest: TokenTwoFactorRequest = {
provider: TwoFactorProviderType.RecoveryCode,
token: request.recoveryCode,
remember: false,
};
const credentials = new PasswordLoginCredentials(
request.email,
this.masterPassword,
"",
twoFactorRequest,
);
try {
const authResult = await this.loginStrategyService.logIn(credentials);
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("youHaveBeenLoggedIn"),
});
await this.loginSuccessHandlerService.run(authResult.userId);
await this.router.navigate(["/settings/security/two-factor"]);
} catch (error) {
// If login errors, redirect to login page per product. Don't show error
this.logService.error("Error logging in automatically: ", (error as Error).message);
await this.router.navigate(["/login"], { queryParams: { email: request.email } });
}
}
/**
* Extracts an error message from the error object.
*/
private extractErrorMessage(error: unknown): string {
let errorMessage: string = this.i18nService.t("unexpectedError");
if (error && typeof error === "object" && "validationErrors" in error) {
const validationErrors = error.validationErrors;
if (validationErrors && typeof validationErrors === "object") {
errorMessage = Object.keys(validationErrors)
.map((key) => {
const messages = (validationErrors as Record<string, string | string[]>)[key];
return Array.isArray(messages) ? messages.join(" ") : messages;
})
.join(" ");
}
} else if (
error &&
typeof error === "object" &&
"message" in error &&
typeof error.message === "string"
) {
errorMessage = error.message;
}
return errorMessage;
}
}

View File

@@ -88,7 +88,9 @@ export class ChangePasswordComponent
async rotateUserKeyClicked() {
if (this.rotateUserKey) {
const ciphers = await this.cipherService.getAllDecrypted();
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
let hasOldAttachments = false;
if (ciphers != null) {
for (let i = 0; i < ciphers.length; i++) {

View File

@@ -146,7 +146,7 @@
<p bitTypography="body1" class="tw-mt-2" *ngIf="loaded">{{ "noTrustedContacts" | i18n }}</p>
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
@@ -263,7 +263,7 @@
<p bitTypography="body1" *ngIf="loaded">{{ "noGrantedAccess" | i18n }}</p>
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin text-muted"
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>

View File

@@ -4,8 +4,6 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault";
@@ -13,7 +11,6 @@ import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwar
import { EmergencyAccessService } from "../../../emergency-access";
import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component";
import { EmergencyAddEditCipherComponent } from "./emergency-add-edit-cipher.component";
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
@Component({
@@ -23,8 +20,6 @@ import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class EmergencyAccessViewComponent implements OnInit {
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@@ -37,7 +32,6 @@ export class EmergencyAccessViewComponent implements OnInit {
private router: Router,
private route: ActivatedRoute,
private emergencyAccessService: EmergencyAccessService,
private configService: ConfigService,
private dialogService: DialogService,
) {}
@@ -57,30 +51,10 @@ export class EmergencyAccessViewComponent implements OnInit {
}
async selectCipher(cipher: CipherView) {
const browserRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
if (browserRefreshEnabled) {
EmergencyViewDialogComponent.open(this.dialogService, {
cipher,
});
return;
}
// FIXME PM-15385: Remove below dialog service logic once extension refresh is live.
// eslint-disable-next-line
const [_, childComponent] = await this.modalService.openViewRef(
EmergencyAddEditCipherComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = cipher == null ? null : cipher.id;
comp.cipher = cipher;
},
);
return childComponent;
EmergencyViewDialogComponent.open(this.dialogService, {
cipher,
});
return;
}
async load() {
@@ -88,6 +62,7 @@ export class EmergencyAccessViewComponent implements OnInit {
this.loaded = true;
}
// FIXME PM-17747: This will also need to be replaced with the new AttachmentViewDialog
async viewAttachments(cipher: CipherView) {
await this.modalService.openViewRef(
EmergencyAccessAttachmentsComponent,

View File

@@ -1,104 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DatePipe } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { CollectionService } from "@bitwarden/admin-console/common";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { PasswordRepromptService } from "@bitwarden/vault";
import { AddEditComponent as BaseAddEditComponent } from "../../../../vault/individual-vault/add-edit.component";
@Component({
selector: "app-org-vault-add-edit",
templateUrl: "../../../../vault/individual-vault/add-edit.component.html",
})
export class EmergencyAddEditCipherComponent extends BaseAddEditComponent implements OnInit {
originalCipher: Cipher = null;
viewOnly = true;
protected override componentName = "app-org-vault-add-edit";
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
accountService: AccountService,
collectionService: CollectionService,
totpService: TotpService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
messagingService: MessagingService,
eventCollectionService: EventCollectionService,
policyService: PolicyService,
passwordRepromptService: PasswordRepromptService,
organizationService: OrganizationService,
logService: LogService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
toastService: ToastService,
sdkService: SdkService,
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
accountService,
collectionService,
totpService,
passwordGenerationService,
messagingService,
eventCollectionService,
policyService,
organizationService,
logService,
passwordRepromptService,
dialogService,
datePipe,
configService,
billingAccountProfileStateService,
cipherAuthorizationService,
toastService,
sdkService,
);
}
async load() {
this.title = this.i18nService.t("viewItem");
}
async ngOnInit(): Promise<void> {
await super.ngOnInit();
// The base component `ngOnInit` calculates the `viewOnly` property based on cipher properties
// In the case of emergency access, `viewOnly` should always be true, set it manually here after
// the base `ngOnInit` is complete.
this.viewOnly = true;
}
protected async loadCipher() {
return Promise.resolve(this.originalCipher);
}
}

View File

@@ -1,7 +1,7 @@
<bit-container>
<div class="tabbed-header">
<div class="tw-mt-6 tw-mb-2 tw-pb-2.5">
<div class="tw-flex tw-items-center tw-gap-2">
<h1>{{ "devices" | i18n }}</h1>
<h1 class="tw-m-0">{{ "devices" | i18n }}</h1>
<button
[bitPopoverTriggerFor]="infoPopover"
type="button"

View File

@@ -26,7 +26,7 @@
</p>
</ng-container>
<bit-callout type="warning" *ngIf="!organizationId">
<p>{{ "twoStepLoginRecoveryWarning" | i18n }}</p>
<p>{{ recoveryCodeWarningMessage }}</p>
<button type="button" bitButton buttonType="secondary" (click)="recoveryCode()">
{{ "viewRecoveryCode" | i18n }}
</button>

View File

@@ -29,6 +29,9 @@ import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.s
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
@@ -52,6 +55,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
organization: Organization;
providers: any[] = [];
canAccessPremium$: Observable<boolean>;
recoveryCodeWarningMessage: string;
showPolicyWarning = false;
loading = true;
modal: ModalRef;
@@ -70,6 +74,8 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
protected policyService: PolicyService,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected accountService: AccountService,
protected configService: ConfigService,
protected i18nService: I18nService,
) {
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -79,6 +85,13 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
const recoveryCodeLoginFeatureFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.RecoveryCodeLogin,
);
this.recoveryCodeWarningMessage = recoveryCodeLoginFeatureFlagEnabled
? this.i18nService.t("yourSingleUseRecoveryCode")
: this.i18nService.t("twoStepLoginRecoveryWarning");
for (const key in TwoFactorProviders) {
// eslint-disable-next-line
if (!TwoFactorProviders.hasOwnProperty(key)) {

View File

@@ -1,13 +1,6 @@
<div class="mt-5 d-flex justify-content-center">
<div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</p>
<div class="tw-p-8 tw-flex">
<img class="new-logo-themed" alt="Bitwarden" />
<div class="spinner-container tw-justify-center">
<i class="bwi bwi-spinner bwi-spin bwi-3x tw-text-muted" title="Loading" aria-hidden="true"></i>
</div>
</div>

View File

@@ -50,7 +50,9 @@
<div class="tw-mb-4">
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "paymentType" | i18n }}</h2>
<app-payment [showAccountCredit]="false"></app-payment>
<app-tax-info [trialFlow]="true" (countryChanged)="changedCountry()"></app-tax-info>
<app-manage-tax-information
(taxInformationChanged)="changedCountry()"
></app-manage-tax-information>
</div>
<div class="tw-flex tw-space-x-2">
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">

View File

@@ -3,6 +3,7 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import {
BillingInformation,
@@ -17,7 +18,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ToastService } from "@bitwarden/components";
import { BillingSharedModule, TaxInfoComponent } from "../../shared";
import { BillingSharedModule } from "../../shared";
import { PaymentComponent } from "../../shared/payment/payment.component";
export type TrialOrganizationType = Exclude<ProductTierType, ProductTierType.Free>;
@@ -51,7 +52,7 @@ export enum SubscriptionProduct {
})
export class TrialBillingStepComponent implements OnInit {
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
@ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent;
@Input() organizationInfo: OrganizationInfo;
@Input() subscriptionProduct: SubscriptionProduct = SubscriptionProduct.PasswordManager;
@Output() steppedBack = new EventEmitter();
@@ -89,8 +90,7 @@ export class TrialBillingStepComponent implements OnInit {
}
async submit(): Promise<void> {
if (!this.taxInfoComponent.taxFormGroup.valid && this.taxInfoComponent?.taxFormGroup.touched) {
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
if (!this.taxInfoComponent.validate()) {
return;
}
@@ -115,7 +115,8 @@ export class TrialBillingStepComponent implements OnInit {
}
protected changedCountry() {
this.paymentComponent.showBankAccount = this.taxInfoComponent.country === "US";
this.paymentComponent.showBankAccount =
this.taxInfoComponent.getTaxInformation().country === "US";
if (
!this.paymentComponent.showBankAccount &&
this.paymentComponent.selected === PaymentMethodType.BankAccount
@@ -210,13 +211,13 @@ export class TrialBillingStepComponent implements OnInit {
private getBillingInformationFromTaxInfoComponent(): BillingInformation {
return {
postalCode: this.taxInfoComponent.taxFormGroup?.value.postalCode,
country: this.taxInfoComponent.taxFormGroup?.value.country,
taxId: this.taxInfoComponent.taxFormGroup?.value.taxId,
addressLine1: this.taxInfoComponent.taxFormGroup?.value.line1,
addressLine2: this.taxInfoComponent.taxFormGroup?.value.line2,
city: this.taxInfoComponent.taxFormGroup?.value.city,
state: this.taxInfoComponent.taxFormGroup?.value.state,
postalCode: this.taxInfoComponent.getTaxInformation()?.postalCode,
country: this.taxInfoComponent.getTaxInformation()?.country,
taxId: this.taxInfoComponent.getTaxInformation()?.taxId,
addressLine1: this.taxInfoComponent.getTaxInformation()?.line1,
addressLine2: this.taxInfoComponent.getTaxInformation()?.line2,
city: this.taxInfoComponent.getTaxInformation()?.city,
state: this.taxInfoComponent.getTaxInformation()?.state,
};
}

View File

@@ -616,7 +616,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return subTotal - this.discount;
}
get secretsManagerSubtotal() {
secretsManagerSubtotal() {
this.secretsManagerTotal = 0;
const plan = this.selectedSecretsManagerPlan;
@@ -643,7 +643,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
return (
this.passwordManagerSubtotal +
this.additionalStorageTotal(this.selectedPlan) +
this.secretsManagerTotal +
this.secretsManagerSubtotal() +
this.estimatedTax
);
}
@@ -1043,7 +1043,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
if (this.organization.useSecretsManager) {
request.secretsManager = {
seats: this.sub.smSeats,
additionalMachineAccounts: this.sub.smServiceAccounts,
additionalMachineAccounts:
this.sub.smServiceAccounts - this.sub.plan.SecretsManager.baseServiceAccount,
};
}

View File

@@ -110,6 +110,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this._plan = plan;
this.formGroup?.controls?.plan?.setValue(plan);
}
@Input() enableSecretsManagerByDefault: boolean;
private _plan = PlanType.Free;
@Input() providerId?: string;
@@ -269,6 +270,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
.subscribe(() => {
this.refreshSalesTax();
});
if (this.enableSecretsManagerByDefault && this.selectedSecretsManagerPlan) {
this.secretsManagerSubscription.patchValue({
enabled: true,
userSeats: 1,
additionalServiceAccounts: 0,
});
}
}
ngOnDestroy() {

View File

@@ -3,8 +3,6 @@
import { Injectable } from "@angular/core";
import { BankAccount } from "@bitwarden/common/billing/models/domain";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { BillingServicesModule } from "./billing-services.module";
@@ -19,10 +17,7 @@ export class StripeService {
cardCvc: string;
};
constructor(
private logService: LogService,
private configService: ConfigService,
) {}
constructor(private logService: LogService) {}
/**
* Loads [Stripe JS]{@link https://docs.stripe.com/js} in the <head> element of the current page and mounts
@@ -43,19 +38,10 @@ export class StripeService {
const window$ = window as any;
this.stripe = window$.Stripe(process.env.STRIPE_KEY);
this.elements = this.stripe.elements();
const isExtensionRefresh = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
setTimeout(() => {
this.elements.create(
"cardNumber",
this.getElementOptions("cardNumber", isExtensionRefresh),
);
this.elements.create(
"cardExpiry",
this.getElementOptions("cardExpiry", isExtensionRefresh),
);
this.elements.create("cardCvc", this.getElementOptions("cardCvc", isExtensionRefresh));
this.elements.create("cardNumber", this.getElementOptions("cardNumber"));
this.elements.create("cardExpiry", this.getElementOptions("cardExpiry"));
this.elements.create("cardCvc", this.getElementOptions("cardCvc"));
if (autoMount) {
this.mountElements();
}
@@ -69,15 +55,21 @@ export class StripeService {
* Re-mounts previously created Stripe credit card [elements]{@link https://docs.stripe.com/js/elements_object/create} into the HTML elements
* specified during the {@link loadStripe} call. This is useful for when those HTML elements are removed from the DOM by Angular.
*/
mountElements() {
mountElements(i: number = 0) {
setTimeout(() => {
if (!document.querySelector(this.elementIds.cardNumber) && i < 10) {
this.logService.warning("Stripe container missing, retrying...");
this.mountElements(i + 1);
return;
}
const cardNumber = this.elements.getElement("cardNumber");
const cardExpiry = this.elements.getElement("cardExpiry");
const cardCvc = this.elements.getElement("cardCvc");
cardNumber.mount(this.elementIds.cardNumber);
cardExpiry.mount(this.elementIds.cardExpiry);
cardCvc.mount(this.elementIds.cardCvc);
});
}, 50);
}
/**
@@ -150,10 +142,7 @@ export class StripeService {
}, 500);
}
private getElementOptions(
element: "cardNumber" | "cardExpiry" | "cardCvc",
isExtensionRefresh: boolean,
): any {
private getElementOptions(element: "cardNumber" | "cardExpiry" | "cardCvc"): any {
const options: any = {
style: {
base: {
@@ -178,15 +167,12 @@ export class StripeService {
},
};
// Unique settings that should only be applied when the extension refresh flag is active
if (isExtensionRefresh) {
options.style.base.fontWeight = "500";
options.classes.base = "v2";
options.style.base.fontWeight = "500";
options.classes.base = "v2";
// Remove the placeholder for number and CVC fields
if (["cardNumber", "cardCvc"].includes(element)) {
options.placeholder = "";
}
// Remove the placeholder for number and CVC fields
if (["cardNumber", "cardCvc"].includes(element)) {
options.placeholder = "";
}
const style = getComputedStyle(document.documentElement);

View File

@@ -2,21 +2,12 @@
<ng-content></ng-content>
</ng-template>
<ng-container *ngIf="extensionRefreshFlag; else defaultLabel">
<div class="tw-relative tw-mt-2">
<bit-label
[attr.for]="for"
class="tw-absolute tw-bg-background tw-px-1 tw-text-sm tw-text-muted -tw-top-2.5 tw-left-3 tw-mb-0 tw-max-w-full tw-pointer-events-auto"
>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
<span class="tw-text-xs tw-font-normal">({{ "required" | i18n }})</span>
</bit-label>
</div>
</ng-container>
<ng-template #defaultLabel>
<label [attr.for]="for">
<div class="tw-relative tw-mt-2">
<bit-label
[attr.for]="for"
class="tw-absolute tw-bg-background tw-px-1 tw-text-sm tw-text-muted -tw-top-2.5 tw-left-3 tw-mb-0 tw-max-w-full tw-pointer-events-auto"
>
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
<span class="tw-text-xs tw-font-normal">({{ "required" | i18n }})</span>
</label>
</ng-template>
</bit-label>
</div>

View File

@@ -1,9 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { booleanAttribute, Component, Input, OnInit } from "@angular/core";
import { booleanAttribute, Component, Input } from "@angular/core";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FormFieldModule } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
@@ -11,8 +9,7 @@ import { SharedModule } from "../../../shared";
/**
* Label that should be used for elements loaded via Stripe API.
*
* Applies the same label styles from CL form-field component when
* the `ExtensionRefresh` flag is set.
* Applies the same label styles from CL form-field component
*/
@Component({
selector: "app-payment-label",
@@ -20,19 +17,11 @@ import { SharedModule } from "../../../shared";
standalone: true,
imports: [FormFieldModule, SharedModule],
})
export class PaymentLabelComponent implements OnInit {
export class PaymentLabelComponent {
/** `id` of the associated input */
@Input({ required: true }) for: string;
/** Displays required text on the label */
@Input({ transform: booleanAttribute }) required = false;
protected extensionRefreshFlag = false;
constructor(private configService: ConfigService) {}
async ngOnInit(): Promise<void> {
this.extensionRefreshFlag = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
constructor() {}
}

View File

@@ -15,6 +15,9 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { SharedModule } from "../../shared";
/**
* @deprecated Use `ManageTaxInformationComponent` instead.
*/
@Component({
selector: "app-tax-info",
templateUrl: "tax-info.component.html",

View File

@@ -6,7 +6,6 @@ import { InputPasswordComponent } from "@bitwarden/auth/angular";
import { FormFieldModule } from "@bitwarden/components";
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
import { TaxInfoComponent } from "../../billing";
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
import { SecretsManagerTrialFreeStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
import { SecretsManagerTrialPaidStepperComponent } from "../../billing/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component";
@@ -48,7 +47,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
FormFieldModule,
OrganizationCreateModule,
EnvironmentSelectorModule,
TaxInfoComponent,
TrialBillingStepComponent,
InputPasswordComponent,
],

View File

@@ -15,11 +15,11 @@
{{ region.domain }}
</a>
</bit-menu>
<div>
<div bitTypography="body2">
{{ "accessing" | i18n }}:
<a [routerLink]="[]" [bitMenuTriggerFor]="environmentOptions">
<b>{{ currentRegion?.domain }}</b
><i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
<b class="tw-text-primary-600 tw-font-semibold">{{ currentRegion?.domain }}</b>
<i class="bwi bwi-fw bwi-sm bwi-angle-down" aria-hidden="true"></i>
</a>
</div>
</div>

View File

@@ -12,12 +12,14 @@
<h1
bitTypography="h1"
noMargin
class="tw-m-0 tw-mr-2 tw-truncate tw-leading-10"
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex"
[title]="title || (routeData.titleId | i18n)"
>
<i *ngIf="icon" class="bwi {{ icon }}" aria-hidden="true"></i>
{{ title || (routeData.titleId | i18n) }}
<ng-content select="[slot=title-suffix]"></ng-content>
<div class="tw-truncate">
<i *ngIf="icon" class="bwi {{ icon }}" aria-hidden="true"></i>
{{ title || (routeData.titleId | i18n) }}
</div>
<div><ng-content select="[slot=title-suffix]"></ng-content></div>
</h1>
</div>
<div class="tw-ml-auto tw-flex tw-flex-col tw-gap-4">

View File

@@ -1,232 +0,0 @@
// import { CommonModule } from "@angular/common";
// import { Component, importProvidersFrom, Injectable, Input } from "@angular/core";
// import { RouterModule } from "@angular/router";
// import {
// applicationConfig,
// componentWrapperDecorator,
// Meta,
// moduleMetadata,
// Story,
// } from "@storybook/angular";
// import { BehaviorSubject, combineLatest, map, of } from "rxjs";
// import { JslibModule } from "@bitwarden/angular/jslib.module";
// import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
// import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
// import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
// import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
// import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
// import {
// AvatarModule,
// BreadcrumbsModule,
// ButtonModule,
// IconButtonModule,
// IconModule,
// InputModule,
// MenuModule,
// NavigationModule,
// TabsModule,
// TypographyModule,
// } from "@bitwarden/components";
// import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
// import { PreloadedEnglishI18nModule } from "../../core/tests";
// import { WebHeaderComponent } from "../header/web-header.component";
// import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service";
// @Injectable({
// providedIn: "root",
// })
// class MockStateService {
// activeAccount$ = new BehaviorSubject("1").asObservable();
// accounts$ = new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable();
// }
// class MockMessagingService implements MessagingService {
// send(subscriber: string, arg?: any) {
// alert(subscriber);
// }
// }
// class MockVaultTimeoutService {
// availableVaultTimeoutActions$() {
// return new BehaviorSubject([VaultTimeoutAction.Lock]).asObservable();
// }
// }
// class MockPlatformUtilsService {
// isSelfHost() {
// return false;
// }
// }
// @Component({
// selector: "product-switcher",
// template: `<button bitIconButton="bwi-filter"></button>`,
// })
// class MockProductSwitcher {}
// @Component({
// selector: "dynamic-avatar",
// template: `<bit-avatar [text]="name$ | async"></bit-avatar>`,
// standalone: true,
// imports: [CommonModule, AvatarModule],
// })
// class MockDynamicAvatar implements Partial<DynamicAvatarComponent> {
// protected name$ = combineLatest([
// this.stateService.accounts$,
// this.stateService.activeAccount$,
// ]).pipe(
// map(
// ([accounts, activeAccount]) => accounts[activeAccount as keyof typeof accounts].profile.name,
// ),
// );
// @Input()
// text: string;
// constructor(private stateService: MockStateService) {}
// }
// export default {
// title: "Web/Header",
// component: WebHeaderComponent,
// decorators: [
// componentWrapperDecorator(
// (story) => `<div class="tw-min-h-screen tw-flex-1 tw-p-6 tw-text-main">${story}</div>`,
// ),
// moduleMetadata({
// imports: [
// JslibModule,
// AvatarModule,
// BreadcrumbsModule,
// ButtonModule,
// IconButtonModule,
// IconModule,
// InputModule,
// MenuModule,
// TabsModule,
// TypographyModule,
// NavigationModule,
// MockDynamicAvatar,
// ],
// declarations: [WebHeaderComponent, MockProductSwitcher],
// providers: [
// { provide: StateService, useClass: MockStateService },
// {
// provide: WebLayoutMigrationBannerService,
// useValue: {
// showBanner$: of(false),
// } as Partial<WebLayoutMigrationBannerService>,
// },
// { provide: PlatformUtilsService, useClass: MockPlatformUtilsService },
// { provide: VaultTimeoutSettingsService, useClass: MockVaultTimeoutService },
// {
// provide: MessagingService,
// useFactory: () => {
// return new MockMessagingService();
// },
// },
// ],
// }),
// applicationConfig({
// providers: [
// importProvidersFrom(RouterModule.forRoot([], { useHash: true })),
// importProvidersFrom(PreloadedEnglishI18nModule),
// ],
// }),
// ],
// } as Meta;
// export const KitchenSink: Story = (args) => ({
// props: args,
// template: `
// <app-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
// <bit-breadcrumbs slot="breadcrumbs">
// <bit-breadcrumb>Foo</bit-breadcrumb>
// <bit-breadcrumb>Bar</bit-breadcrumb>
// </bit-breadcrumbs>
// <input
// bitInput
// placeholder="Ask Jeeves"
// type="text"
// />
// <button bitButton buttonType="primary">New</button>
// <button bitButton slot="secondary">Click Me 🎉</button>
// <bit-tab-nav-bar slot="tabs">
// <bit-tab-link route="">Foo</bit-tab-link>
// <bit-tab-link route="#bar">Bar</bit-tab-link>
// </bit-tab-nav-bar>
// </app-header>
// `,
// });
// export const Basic: Story = (args) => ({
// props: args,
// template: `
// <app-header title="Foobar" icon="bwi-bug"></app-header>
// `,
// });
// export const WithLongTitle: Story = (args) => ({
// props: args,
// template: `
// <app-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug"></app-header>
// `,
// });
// export const WithBreadcrumbs: Story = (args) => ({
// props: args,
// template: `
// <app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
// <bit-breadcrumbs slot="breadcrumbs">
// <bit-breadcrumb>Foo</bit-breadcrumb>
// <bit-breadcrumb>Bar</bit-breadcrumb>
// </bit-breadcrumbs>
// </app-header>
// `,
// });
// export const WithSearch: Story = (args) => ({
// props: args,
// template: `
// <app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
// <input
// bitInput
// placeholder="Ask Jeeves"
// type="text"
// />
// </app-header>
// `,
// });
// export const WithSecondaryContent: Story = (args) => ({
// props: args,
// template: `
// <app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
// <button bitButton slot="secondary">Click Me 🎉</button>
// </app-header>
// `,
// });
// export const WithTabs: Story = (args) => ({
// props: args,
// template: `
// <app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
// <bit-tab-nav-bar slot="tabs">
// <bit-tab-link route="">Foo</bit-tab-link>
// <bit-tab-link route="#bar">Bar</bit-tab-link>
// </bit-tab-nav-bar>
// </app-header>
// `,
// });
// export const WithTitleSuffixComponent: Story = (args) => ({
// props: args,
// template: `
// <app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
// <ng-container slot="title-suffix"><i class="bwi bwi-spinner bwi-spin"></i></ng-container>
// </app-header>
// `,
// });

View File

@@ -0,0 +1,260 @@
import { CommonModule } from "@angular/common";
import { Component, importProvidersFrom, Injectable, Input } from "@angular/core";
import { RouterModule } from "@angular/router";
import {
applicationConfig,
componentWrapperDecorator,
Meta,
moduleMetadata,
StoryObj,
} from "@storybook/angular";
import { BehaviorSubject, combineLatest, map, of } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import {
AvatarModule,
BreadcrumbsModule,
ButtonModule,
IconButtonModule,
IconModule,
InputModule,
MenuModule,
NavigationModule,
TabsModule,
TypographyModule,
} from "@bitwarden/components";
import { DynamicAvatarComponent } from "../../components/dynamic-avatar.component";
import { PreloadedEnglishI18nModule } from "../../core/tests";
import { WebHeaderComponent } from "../header/web-header.component";
import { WebLayoutMigrationBannerService } from "./web-layout-migration-banner.service";
@Injectable({
providedIn: "root",
})
class MockStateService {
activeAccount$ = new BehaviorSubject("1").asObservable();
accounts$ = new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable();
}
@Component({
selector: "product-switcher",
template: `<button type="button" bitIconButton="bwi-filter"></button>`,
})
class MockProductSwitcher {}
@Component({
selector: "dynamic-avatar",
template: `<bit-avatar [text]="name$ | async"></bit-avatar>`,
standalone: true,
imports: [CommonModule, AvatarModule],
})
class MockDynamicAvatar implements Partial<DynamicAvatarComponent> {
protected name$ = combineLatest([
this.stateService.accounts$,
this.stateService.activeAccount$,
]).pipe(
map(
([accounts, activeAccount]) => accounts[activeAccount as keyof typeof accounts].profile.name,
),
);
@Input()
text?: string;
constructor(private stateService: MockStateService) {}
}
export default {
title: "Web/Header",
component: WebHeaderComponent,
decorators: [
componentWrapperDecorator(
(story) => `<div class="tw-min-h-screen tw-flex-1 tw-p-6 tw-text-main">${story}</div>`,
),
moduleMetadata({
imports: [
JslibModule,
AvatarModule,
BreadcrumbsModule,
ButtonModule,
IconButtonModule,
IconModule,
InputModule,
MenuModule,
TabsModule,
TypographyModule,
NavigationModule,
MockDynamicAvatar,
],
declarations: [WebHeaderComponent, MockProductSwitcher],
providers: [
{ provide: StateService, useClass: MockStateService },
{
provide: AccountService,
useValue: {
activeAccount$: of({
name: "Foobar Warden",
}),
} as Partial<AccountService>,
},
{
provide: WebLayoutMigrationBannerService,
useValue: {
showBanner$: of(false),
} as Partial<WebLayoutMigrationBannerService>,
},
{
provide: PlatformUtilsService,
useValue: {
isSelfHost() {
return false;
},
} as Partial<PlatformUtilsService>,
},
{
provide: VaultTimeoutSettingsService,
useValue: {
availableVaultTimeoutActions$() {
return new BehaviorSubject([VaultTimeoutAction.Lock]).asObservable();
},
} as Partial<VaultTimeoutSettingsService>,
},
{
provide: MessagingService,
useValue: {
send: (...args: any[]) => {
// eslint-disable-next-line no-console
console.log("MessagingService.send", args);
},
} as Partial<MessagingService>,
},
],
}),
applicationConfig({
providers: [
importProvidersFrom(RouterModule.forRoot([], { useHash: true })),
importProvidersFrom(PreloadedEnglishI18nModule),
],
}),
],
} as Meta;
type Story = StoryObj<WebHeaderComponent>;
export const KitchenSink: Story = {
render: (args) => ({
props: args,
template: `
<app-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
<bit-breadcrumbs slot="breadcrumbs">
<bit-breadcrumb>Foo</bit-breadcrumb>
<bit-breadcrumb>Bar</bit-breadcrumb>
</bit-breadcrumbs>
<input
bitInput
placeholder="Ask Jeeves"
type="text"
/>
<button bitButton buttonType="primary">New</button>
<button bitButton slot="secondary">Click Me 🎉</button>
<bit-tab-nav-bar slot="tabs">
<bit-tab-link route="">Foo</bit-tab-link>
<bit-tab-link route="#bar">Bar</bit-tab-link>
</bit-tab-nav-bar>
</app-header>
`,
}),
};
export const Basic: Story = {
render: (args: any) => ({
props: args,
template: `
<app-header title="Foobar" icon="bwi-bug"></app-header>
`,
}),
};
export const WithLongTitle: Story = {
render: (arg: any) => ({
props: arg,
template: `
<app-header title="LongTitleeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" icon="bwi-bug">
<ng-container slot="title-suffix"><i class="bwi bwi-key"></i></ng-container>
</app-header>
`,
}),
};
export const WithBreadcrumbs: Story = {
render: (args: any) => ({
props: args,
template: `
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
<bit-breadcrumbs slot="breadcrumbs">
<bit-breadcrumb>Foo</bit-breadcrumb>
<bit-breadcrumb>Bar</bit-breadcrumb>
</bit-breadcrumbs>
</app-header>
`,
}),
};
export const WithSearch: Story = {
render: (args: any) => ({
props: args,
template: `
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
<input
bitInput
placeholder="Ask Jeeves"
type="text"
/>
</app-header>
`,
}),
};
export const WithSecondaryContent: Story = {
render: (args) => ({
props: args,
template: `
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
<button bitButton slot="secondary">Click Me 🎉</button>
</app-header>
`,
}),
};
export const WithTabs: Story = {
render: (args) => ({
props: args,
template: `
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
<bit-tab-nav-bar slot="tabs">
<bit-tab-link route="">Foo</bit-tab-link>
<bit-tab-link route="#bar">Bar</bit-tab-link>
</bit-tab-nav-bar>
</app-header>
`,
}),
};
export const WithTitleSuffixComponent: Story = {
render: (args) => ({
props: args,
template: `
<app-header title="Foobar" icon="bwi-bug" class="tw-text-main">
<ng-container slot="title-suffix"><i class="bwi bwi-spinner bwi-spin"></i></ng-container>
</app-header>
`,
}),
};

View File

@@ -10,6 +10,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
@@ -68,6 +69,12 @@ class MockAccountService implements Partial<AccountService> {
});
}
class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
isSelfHost() {
return false;
}
}
@Component({
selector: "story-layout",
template: `<ng-content></ng-content>`,
@@ -105,6 +112,7 @@ export default {
{ provide: AccountService, useClass: MockAccountService },
{ provide: ProviderService, useClass: MockProviderService },
{ provide: SyncService, useClass: MockSyncService },
{ provide: PlatformUtilsService, useClass: MockPlatformUtilsService },
ProductSwitcherService,
{
provide: I18nPipe,

View File

@@ -10,6 +10,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
import { IconButtonModule, LinkModule, MenuModule } from "@bitwarden/components";
@@ -68,6 +69,12 @@ class MockAccountService implements Partial<AccountService> {
});
}
class MockPlatformUtilsService implements Partial<PlatformUtilsService> {
isSelfHost() {
return false;
}
}
@Component({
selector: "story-layout",
template: `<ng-content></ng-content>`,
@@ -101,6 +108,8 @@ export default {
{ provide: ProviderService, useClass: MockProviderService },
MockProviderService,
{ provide: SyncService, useClass: MockSyncService },
{ provide: PlatformUtilsService, useClass: MockPlatformUtilsService },
MockPlatformUtilsService,
ProductSwitcherService,
{
provide: I18nService,

View File

@@ -90,9 +90,10 @@ import { DomainRulesComponent } from "./settings/domain-rules.component";
import { PreferencesComponent } from "./settings/preferences.component";
import { CredentialGeneratorComponent } from "./tools/credential-generator/credential-generator.component";
import { ReportsModule } from "./tools/reports";
import { AccessComponent } from "./tools/send/access.component";
import { SendAccessExplainerComponent } from "./tools/send/send-access-explainer.component";
import { AccessComponent, SendAccessExplainerComponent } from "./tools/send/send-access";
import { SendComponent } from "./tools/send/send.component";
import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component";
import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component";
import { VaultModule } from "./vault/individual-vault/vault.module";
const routes: Routes = [
@@ -696,6 +697,23 @@ const routes: Routes = [
maxWidth: "3xl",
} satisfies AnonLayoutWrapperData,
},
{
path: "browser-extension-prompt",
data: {
pageIcon: VaultIcons.BrowserExtensionIcon,
} satisfies AnonLayoutWrapperData,
children: [
{
path: "",
component: BrowserExtensionPromptComponent,
},
{
path: "",
component: BrowserExtensionPromptInstallComponent,
outlet: "secondary",
},
],
},
],
},
{

View File

@@ -4,7 +4,7 @@ import { AuthModule } from "./auth";
import { LoginModule } from "./auth/login/login.module";
import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module";
import { LooseComponentsModule, SharedModule } from "./shared";
import { AccessComponent } from "./tools/send/access.component";
import { AccessComponent } from "./tools/send/send-access/access.component";
import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module";
import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-filter.module";

View File

@@ -35,7 +35,6 @@ import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-acce
import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component";
import { EmergencyAccessTakeoverComponent } from "../auth/settings/emergency-access/takeover/emergency-access-takeover.component";
import { EmergencyAccessViewComponent } from "../auth/settings/emergency-access/view/emergency-access-view.component";
import { EmergencyAddEditCipherComponent } from "../auth/settings/emergency-access/view/emergency-add-edit-cipher.component";
import { ApiKeyComponent } from "../auth/settings/security/api-key.component";
import { ChangeKdfModule } from "../auth/settings/security/change-kdf/change-kdf.module";
import { SecurityKeysComponent } from "../auth/settings/security/security-keys.component";
@@ -124,7 +123,6 @@ import { SharedModule } from "./shared.module";
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
EmergencyAddEditCipherComponent,
FolderAddEditComponent,
FrontendLayoutComponent,
HintComponent,
@@ -188,7 +186,6 @@ import { SharedModule } from "./shared.module";
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
EmergencyAccessViewComponent,
EmergencyAddEditCipherComponent,
FolderAddEditComponent,
FrontendLayoutComponent,
HintComponent,

View File

@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core";
import { BehaviorSubject, Observable, Subject, switchMap, takeUntil } from "rxjs";
import { BehaviorSubject, Observable, Subject, firstValueFrom, switchMap, takeUntil } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -51,8 +52,10 @@ export class CipherReportComponent implements OnDestroy {
private syncService: SyncService,
) {
this.organizations$ = this.accountService.activeAccount$.pipe(
switchMap((account) => this.organizationService.organizations$(account?.id)),
getUserId,
switchMap((userId) => this.organizationService.organizations$(userId)),
);
this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => {
this.organizations = orgs;
});
@@ -182,7 +185,8 @@ export class CipherReportComponent implements OnDestroy {
}
protected async getAllCiphers(): Promise<CipherView[]> {
return await this.cipherService.getAllDecrypted();
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return await this.cipherService.getAllDecrypted(activeUserId);
}
protected filterCiphersByOrg(ciphersList: CipherView[]) {

View File

@@ -25,15 +25,14 @@ describe("ExposedPasswordsReportComponent", () => {
let auditService: MockProxy<AuditService>;
let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => {
syncServiceMock = mock<SyncService>();
auditService = mock<AuditService>();
organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([]));
accountService = mockAccountServiceWith(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({

View File

@@ -24,14 +24,13 @@ describe("InactiveTwoFactorReportComponent", () => {
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => {
organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([]));
syncServiceMock = mock<SyncService>();
accountService = mockAccountServiceWith(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({

View File

@@ -23,15 +23,13 @@ describe("ReusedPasswordsReportComponent", () => {
let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => {
organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([]));
syncServiceMock = mock<SyncService>();
accountService = mockAccountServiceWith(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({

View File

@@ -25,15 +25,14 @@ describe("UnsecuredWebsitesReportComponent", () => {
let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>;
let collectionService: MockProxy<CollectionService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => {
organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([]));
syncServiceMock = mock<SyncService>();
collectionService = mock<CollectionService>();
accountService = mockAccountServiceWith(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({

View File

@@ -25,15 +25,14 @@ describe("WeakPasswordsReportComponent", () => {
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => {
syncServiceMock = mock<SyncService>();
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([]));
accountService = mockAccountServiceWith(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({

View File

@@ -21,7 +21,7 @@ import { NoItemsModule, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { ExpiredSendIcon } from "@bitwarden/send-ui";
import { SharedModule } from "../../shared";
import { SharedModule } from "../../../shared";
import { SendAccessFileComponent } from "./send-access-file.component";
import { SendAccessPasswordComponent } from "./send-access-password.component";

View File

@@ -0,0 +1,2 @@
export { AccessComponent } from "./access.component";
export { SendAccessExplainerComponent } from "./send-access-explainer.component";

View File

@@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { SharedModule } from "../../shared";
import { SharedModule } from "../../../shared";
@Component({
selector: "app-send-access-explainer",

View File

@@ -13,7 +13,7 @@ import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-ac
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { ToastService } from "@bitwarden/components";
import { SharedModule } from "../../shared";
import { SharedModule } from "../../../shared";
@Component({
selector: "app-send-access-file",

View File

@@ -4,7 +4,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angu
import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { SharedModule } from "../../shared";
import { SharedModule } from "../../../shared";
@Component({
selector: "app-send-access-password",

View File

@@ -8,7 +8,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
import { ToastService } from "@bitwarden/components";
import { SharedModule } from "../../shared";
import { SharedModule } from "../../../shared";
@Component({
selector: "app-send-access-text",

View File

@@ -0,0 +1,4 @@
<div class="tw-text-center" *ngIf="shouldShow$ | async">
<p class="tw-mb-0">{{ "doNotHaveExtension" | i18n }}</p>
<a bitLink [href]="webStoreUrl">{{ "installExtension" | i18n }}</a>
</div>

View File

@@ -0,0 +1,145 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { BehaviorSubject } from "rxjs";
import { DeviceType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "../../services/browser-extension-prompt.service";
import { BrowserExtensionPromptInstallComponent } from "./browser-extension-prompt-install.component";
describe("BrowserExtensionInstallComponent", () => {
let fixture: ComponentFixture<BrowserExtensionPromptInstallComponent>;
let component: BrowserExtensionPromptInstallComponent;
const pageState$ = new BehaviorSubject(BrowserPromptState.Loading);
const getDevice = jest.fn();
beforeEach(async () => {
getDevice.mockClear();
await TestBed.configureTestingModule({
providers: [
{
provide: BrowserExtensionPromptService,
useValue: { pageState$ },
},
{
provide: I18nService,
useValue: { t: (key: string) => key },
},
{
provide: PlatformUtilsService,
useValue: { getDevice },
},
],
}).compileComponents();
fixture = TestBed.createComponent(BrowserExtensionPromptInstallComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("only shows during error state", () => {
expect(fixture.nativeElement.textContent).toBe("");
pageState$.next(BrowserPromptState.Success);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe("");
pageState$.next(BrowserPromptState.Error);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).not.toBe("");
pageState$.next(BrowserPromptState.ManualOpen);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).not.toBe("");
});
describe("error state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Error);
fixture.detectChanges();
});
it("shows error text", () => {
const errorText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(errorText.textContent).toBe("doNotHaveExtension");
});
it("links to bitwarden installation page by default", () => {
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://bitwarden.com/download/#downloads-web-browser",
);
});
it("links to bitwarden installation page for Chrome", () => {
getDevice.mockReturnValue(DeviceType.ChromeBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://chrome.google.com/webstore/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb",
);
});
it("links to bitwarden installation page for Firefox", () => {
getDevice.mockReturnValue(DeviceType.FirefoxBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/",
);
});
it("links to bitwarden installation page for Safari", () => {
getDevice.mockReturnValue(DeviceType.SafariBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12",
);
});
it("links to bitwarden installation page for Opera", () => {
getDevice.mockReturnValue(DeviceType.OperaBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://addons.opera.com/extensions/details/bitwarden-free-password-manager/",
);
});
it("links to bitwarden installation page for Edge", () => {
getDevice.mockReturnValue(DeviceType.EdgeBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
);
});
});
});

View File

@@ -0,0 +1,66 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { map } from "rxjs";
import { DeviceType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { LinkModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "../../services/browser-extension-prompt.service";
/** Device specific Urls for the extension */
const WebStoreUrls: Partial<Record<DeviceType, string>> = {
[DeviceType.ChromeBrowser]:
"https://chrome.google.com/webstore/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb",
[DeviceType.FirefoxBrowser]:
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/",
[DeviceType.SafariBrowser]: "https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12",
[DeviceType.OperaBrowser]:
"https://addons.opera.com/extensions/details/bitwarden-free-password-manager/",
[DeviceType.EdgeBrowser]:
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
};
@Component({
selector: "vault-browser-extension-prompt-install",
templateUrl: "./browser-extension-prompt-install.component.html",
standalone: true,
imports: [CommonModule, I18nPipe, LinkModule],
})
export class BrowserExtensionPromptInstallComponent implements OnInit {
/** The install link should only show for the error states */
protected shouldShow$ = this.browserExtensionPromptService.pageState$.pipe(
map((state) => state === BrowserPromptState.Error || state === BrowserPromptState.ManualOpen),
);
/** All available page states */
protected BrowserPromptState = BrowserPromptState;
/**
* Installation link for the extension
*/
protected webStoreUrl: string = "https://bitwarden.com/download/#downloads-web-browser";
constructor(
private browserExtensionPromptService: BrowserExtensionPromptService,
private platformService: PlatformUtilsService,
) {}
ngOnInit(): void {
this.setBrowserStoreLink();
}
/** If available, set web store specific URL for the extension */
private setBrowserStoreLink(): void {
const deviceType = this.platformService.getDevice();
const platformSpecificUrl = WebStoreUrls[deviceType];
if (platformSpecificUrl) {
this.webStoreUrl = platformSpecificUrl;
}
}
}

View File

@@ -0,0 +1,44 @@
<div class="tw-text-center" *ngIf="pageState$ | async as pageState">
<ng-container *ngIf="pageState === BrowserPromptState.Loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x tw-text-primary-600" aria-hidden="true"></i>
<p bitTypography="body1" class="tw-mb-0 tw-mt-2">{{ "openingExtension" | i18n }}</p>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.Error">
<p bitTypography="body1" class="tw-mb-4 tw-text-xl">{{ "openingExtensionError" | i18n }}</p>
<button
bitButton
buttonType="primary"
type="button"
(click)="openExtension()"
id="bw-extension-prompt-button"
>
{{ "openExtension" | i18n }}
<i class="bwi bwi-external-link tw-ml-2" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.Success">
<i class="bwi tw-text-2xl bwi-check-circle tw-text-success-700" aria-hidden="true"></i>
<p bitTypography="body1" class="tw-mb-4 tw-text-xl">
{{ "openedExtensionViewAtRiskPasswords" | i18n }}
</p>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.ManualOpen">
<p bitTypography="body1" class="tw-mb-0 tw-text-xl">
{{ "openExtensionManuallyPart1" | i18n }}
<bit-icon
[icon]="BitwardenIcon"
class="[&>svg]:tw-align-baseline [&>svg]:-tw-mb-[2px]"
></bit-icon>
{{ "openExtensionManuallyPart2" | i18n }}
</p>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.MobileBrowser">
<p bitTypography="body1" class="tw-mb-0 tw-text-xl">
{{ "reopenLinkOnDesktop" | i18n }}
</p>
</ng-container>
</div>

View File

@@ -0,0 +1,104 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { BehaviorSubject } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "../../services/browser-extension-prompt.service";
import { BrowserExtensionPromptComponent } from "./browser-extension-prompt.component";
describe("BrowserExtensionPromptComponent", () => {
let fixture: ComponentFixture<BrowserExtensionPromptComponent>;
const start = jest.fn();
const pageState$ = new BehaviorSubject(BrowserPromptState.Loading);
beforeEach(async () => {
start.mockClear();
await TestBed.configureTestingModule({
providers: [
{
provide: BrowserExtensionPromptService,
useValue: { start, pageState$ },
},
{
provide: I18nService,
useValue: { t: (key: string) => key },
},
],
}).compileComponents();
fixture = TestBed.createComponent(BrowserExtensionPromptComponent);
fixture.detectChanges();
});
it("calls start on initialization", () => {
expect(start).toHaveBeenCalledTimes(1);
});
describe("loading state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Loading);
fixture.detectChanges();
});
it("shows loading text", () => {
const element = fixture.nativeElement;
expect(element.textContent.trim()).toBe("openingExtension");
});
});
describe("error state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Error);
fixture.detectChanges();
});
it("shows error text", () => {
const errorText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(errorText.textContent.trim()).toBe("openingExtensionError");
});
});
describe("success state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Success);
fixture.detectChanges();
});
it("shows success message", () => {
const successText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(successText.textContent.trim()).toBe("openedExtensionViewAtRiskPasswords");
});
});
describe("mobile state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.MobileBrowser);
fixture.detectChanges();
});
it("shows mobile message", () => {
const mobileText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(mobileText.textContent.trim()).toBe("reopenLinkOnDesktop");
});
});
describe("manual error state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.ManualOpen);
fixture.detectChanges();
});
it("shows manual open error message", () => {
const manualText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart1");
expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart2");
});
});
});

View File

@@ -0,0 +1,37 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ButtonComponent, IconModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { VaultIcons } from "@bitwarden/vault";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "../../services/browser-extension-prompt.service";
@Component({
selector: "vault-browser-extension-prompt",
templateUrl: "./browser-extension-prompt.component.html",
standalone: true,
imports: [CommonModule, I18nPipe, ButtonComponent, IconModule],
})
export class BrowserExtensionPromptComponent implements OnInit {
/** Current state of the prompt page */
protected pageState$ = this.browserExtensionPromptService.pageState$;
/** All available page states */
protected BrowserPromptState = BrowserPromptState;
protected BitwardenIcon = VaultIcons.BitwardenIcon;
constructor(private browserExtensionPromptService: BrowserExtensionPromptService) {}
ngOnInit(): void {
this.browserExtensionPromptService.start();
}
openExtension(): void {
this.browserExtensionPromptService.openExtension();
}
}

View File

@@ -12,6 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -355,8 +356,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
this.formConfig.mode = "edit";
this.formConfig.initialValues = null;
}
let cipher = await this.cipherService.get(cipherView.id);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
let cipher = await this.cipherService.get(cipherView.id, activeUserId);
// When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint (if not found in local state)
if (this.formConfig.isAdminConsole && (cipher == null || this.formConfig.admin)) {
@@ -448,10 +449,13 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
result.action === AttachmentDialogResult.Removed ||
result.action === AttachmentDialogResult.Uploaded
) {
const updatedCipher = await this.cipherService.get(this.formConfig.originalCipher?.id);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const updatedCipher = await this.cipherService.get(
this.formConfig.originalCipher?.id,
activeUserId,
);
const updatedCipherView = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
@@ -490,9 +494,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
if (config.originalCipher == null) {
return;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return await config.originalCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId),
);
@@ -574,10 +576,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
// - The cipher is unassigned
const asAdmin = this.organization?.canEditAllCiphers || cipherIsUnassigned;
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (this.cipher.isDeleted) {
await this.cipherService.deleteWithServer(this.cipher.id, asAdmin);
await this.cipherService.deleteWithServer(this.cipher.id, activeUserId, asAdmin);
} else {
await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
await this.cipherService.softDeleteWithServer(this.cipher.id, activeUserId, asAdmin);
}
}

View File

@@ -22,7 +22,7 @@
[routerLink]="[]"
[queryParams]="{ itemId: cipher.id, action: clickAction }"
queryParamsHandling="merge"
[replaceUrl]="extensionRefreshEnabled"
[replaceUrl]="true"
title="{{ 'editItemWithName' | i18n: cipher.name }}"
type="button"
appStopProp

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CollectionView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -25,11 +22,6 @@ import { RowHeightClass } from "./vault-items.component";
export class VaultCipherRowComponent implements OnInit {
protected RowHeightClass = RowHeightClass;
/**
* Flag to determine if the extension refresh feature flag is enabled.
*/
protected extensionRefreshEnabled = false;
@Input() disabled: boolean;
@Input() cipher: CipherView;
@Input() showOwner: boolean;
@@ -61,19 +53,12 @@ export class VaultCipherRowComponent implements OnInit {
];
protected organization?: Organization;
constructor(
private configService: ConfigService,
private i18nService: I18nService,
) {}
constructor(private i18nService: I18nService) {}
/**
* Lifecycle hook for component initialization.
* Checks if the extension refresh feature flag is enabled to provide to template.
*/
async ngOnInit(): Promise<void> {
this.extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
if (this.cipher.organizationId != null) {
this.organization = this.organizations.find((o) => o.id === this.cipher.organizationId);
}
@@ -83,7 +68,7 @@ export class VaultCipherRowComponent implements OnInit {
if (this.cipher.decryptionFailure) {
return "showFailedToDecrypt";
}
return this.extensionRefreshEnabled ? "view" : null;
return "view";
}
protected get showTotpCopyButton() {

View File

@@ -11,7 +11,7 @@
/>
</td>
<td bitCell [ngClass]="RowHeightClass" class="tw-min-w-fit">
<div class="icon" aria-hidden="true">
<div aria-hidden="true">
<i class="bwi bwi-fw bwi-lg bwi-collection"></i>
</div>
</td>

View File

@@ -66,7 +66,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
protected messagingService: MessagingService,
eventCollectionService: EventCollectionService,
protected policyService: PolicyService,
protected organizationService: OrganizationService,
organizationService: OrganizationService,
logService: LogService,
passwordRepromptService: PasswordRepromptService,
dialogService: DialogService,
@@ -143,11 +143,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
}, 1000);
}
const extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
this.cardIsExpired = extensionRefreshEnabled && isCardExpired(this.cipher.card);
this.cardIsExpired = isCardExpired(this.cipher.card);
}
ngOnDestroy() {
@@ -307,8 +303,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.cipher.type === CipherType.Login &&
this.cipher.login.totp &&
this.organization?.productTierType != ProductTierType.Free &&
((this.canAccessPremium && this.cipher.organizationId == null) ||
this.cipher.organizationUseTotp)
(this.cipher.organizationUseTotp || this.canAccessPremium)
);
}

View File

@@ -2,10 +2,13 @@
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -61,6 +64,7 @@ export class BulkDeleteDialogComponent {
private apiService: ApiService,
private collectionService: CollectionService,
private toastService: ToastService,
private accountService: AccountService,
) {
this.cipherIds = params.cipherIds ?? [];
this.permanent = params.permanent;
@@ -115,10 +119,12 @@ export class BulkDeleteDialogComponent {
private async deleteCiphers(): Promise<any> {
const asAdmin = this.organization?.canEditAllCiphers;
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (this.permanent) {
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
await this.cipherService.deleteManyWithServer(this.cipherIds, activeUserId, asAdmin);
} else {
await this.cipherService.softDeleteManyWithServer(this.cipherIds, asAdmin);
await this.cipherService.softDeleteManyWithServer(this.cipherIds, activeUserId, asAdmin);
}
}

View File

@@ -3,9 +3,10 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { firstValueFrom, map, Observable } from "rxjs";
import { firstValueFrom, Observable } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -48,8 +49,6 @@ export class BulkMoveDialogComponent implements OnInit {
});
folders$: Observable<FolderView[]>;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
@Inject(DIALOG_DATA) params: BulkMoveDialogParams,
private dialogRef: DialogRef<BulkMoveDialogResult>,
@@ -65,7 +64,7 @@ export class BulkMoveDialogComponent implements OnInit {
}
async ngOnInit() {
const activeUserId = await firstValueFrom(this.activeUserId$);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.folders$ = this.folderService.folderViews$(activeUserId);
this.formGroup.patchValue({
folderId: (await firstValueFrom(this.folders$))[0].id,
@@ -81,7 +80,12 @@ export class BulkMoveDialogComponent implements OnInit {
return;
}
await this.cipherService.moveManyWithServer(this.cipherIds, this.formGroup.value.folderId);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherService.moveManyWithServer(
this.cipherIds,
this.formGroup.value.folderId,
activeUserId,
);
this.toastService.showToast({
variant: "success",
title: null,

View File

@@ -71,7 +71,7 @@ describe("vault filter service", () => {
policyService.policyAppliesToActiveUser$
.calledWith(PolicyType.SingleOrg)
.mockReturnValue(singleOrgPolicy);
cipherService.cipherViews$ = cipherViews;
cipherService.cipherViews$.mockReturnValue(cipherViews);
vaultFilterService = new VaultFilterService(
organizationService,

View File

@@ -5,6 +5,7 @@ import {
BehaviorSubject,
combineLatest,
combineLatestWith,
filter,
firstValueFrom,
map,
Observable,
@@ -68,14 +69,16 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
switchMap((userId) =>
combineLatest([
this.folderService.folderViews$(userId),
this.cipherService.cipherViews$,
this.cipherService.cipherViews$(userId),
this._organizationFilter,
]),
),
filter(([folders, ciphers, org]) => !!ciphers), // ciphers may be null, meaning decryption is in progress. Ignore this emission
switchMap(([folders, ciphers, org]) => {
return this.filterFolders(folders, ciphers, org);
}),
);
folderTree$: Observable<TreeNode<FolderFilter>> = this.filteredFolders$.pipe(
map((folders) => this.buildFolderTree(folders)),
);

View File

@@ -121,7 +121,7 @@
></ng-container>
<li class="filter-option" *ngIf="showAddLink">
<span class="filter-buttons">
<a href="#" routerLink="{{ addInfo.route }}" class="filter-button">
<a href="#" routerLink="{{ addInfo.route }}" class="filter-button tw-truncate">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
&nbsp;{{ addInfo.text | i18n }}
</a>

View File

@@ -69,88 +69,48 @@
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
<div appListDropdown>
<ng-container [ngSwitch]="extensionRefreshEnabled">
<ng-container *ngSwitchCase="true">
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
</button>
<bit-menu-divider />
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
<ng-container *ngSwitchCase="false">
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
</ng-container>
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
<i class="bwi bwi-key" slot="start" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
</button>
<bit-menu-divider />
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button *ngIf="canCreateCollections" type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</div>
</div>
</app-header>

View File

@@ -9,13 +9,10 @@ import {
OnInit,
Output,
} from "@angular/core";
import { firstValueFrom } from "rxjs";
import { Unassigned, CollectionView } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@@ -50,7 +47,6 @@ export class VaultHeaderComponent implements OnInit {
protected All = All;
protected CollectionDialogTabType = CollectionDialogTabType;
protected CipherType = CipherType;
protected extensionRefreshEnabled: boolean;
/**
* Boolean to determine the loading state of the header.
@@ -85,16 +81,9 @@ export class VaultHeaderComponent implements OnInit {
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {}
constructor(private i18nService: I18nService) {}
async ngOnInit() {
this.extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
}
async ngOnInit() {}
/**
* The id of the organization that is currently being filtered on.

View File

@@ -1,10 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/common/types/guid";
import { VaultOnboardingTasks } from "../vault-onboarding.service";
export abstract class VaultOnboardingService {
vaultOnboardingState$: Observable<VaultOnboardingTasks>;
abstract setVaultOnboardingTasks(newState: VaultOnboardingTasks): Promise<void>;
abstract setVaultOnboardingTasks(userId: UserId, newState: VaultOnboardingTasks): Promise<void>;
abstract vaultOnboardingState$(userId: UserId): Observable<VaultOnboardingTasks | null>;
}

View File

@@ -1,14 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import {
ActiveUserState,
SingleUserState,
StateProvider,
UserKeyDefinition,
VAULT_ONBOARDING,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./abstraction/vault-onboarding.service";
@@ -26,20 +25,20 @@ const VAULT_ONBOARDING_KEY = new UserKeyDefinition<VaultOnboardingTasks>(
clearOn: [], // do not clear tutorials
},
);
@Injectable()
export class VaultOnboardingService implements VaultOnboardingServiceAbstraction {
private vaultOnboardingState: ActiveUserState<VaultOnboardingTasks>;
vaultOnboardingState$: Observable<VaultOnboardingTasks>;
constructor(private stateProvider: StateProvider) {}
constructor(private stateProvider: StateProvider) {
this.vaultOnboardingState = this.stateProvider.getActive(VAULT_ONBOARDING_KEY);
this.vaultOnboardingState$ = this.vaultOnboardingState.state$;
private vaultOnboardingState(userId: UserId): SingleUserState<VaultOnboardingTasks> {
return this.stateProvider.getUser(userId, VAULT_ONBOARDING_KEY);
}
async setVaultOnboardingTasks(newState: VaultOnboardingTasks): Promise<void> {
await this.vaultOnboardingState.update(() => {
return { ...newState };
});
vaultOnboardingState$(userId: UserId): Observable<VaultOnboardingTasks | null> {
return this.vaultOnboardingState(userId).state$;
}
async setVaultOnboardingTasks(userId: UserId, newState: VaultOnboardingTasks): Promise<void> {
const state = this.vaultOnboardingState(userId);
await state.update(() => ({ ...newState }));
}
}

View File

@@ -22,12 +22,7 @@
<p class="tw-pl-1">
{{ "onboardingImportDataDetailsPartOne" | i18n }}
<button type="button" bitLink (click)="emitToAddCipher()">
{{
(extensionRefreshEnabled
? "onboardingImportDataDetailsLoginLink"
: "onboardingImportDataDetailsLink"
) | i18n
}}
{{ "onboardingImportDataDetailsLoginLink" | i18n }}
</button>
<span>
{{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }}

View File

@@ -7,12 +7,16 @@ import { Subject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
import { VaultOnboardingComponent } from "./vault-onboarding.component";
@@ -25,10 +29,11 @@ describe("VaultOnboardingComponent", () => {
let mockPolicyService: MockProxy<PolicyService>;
let mockI18nService: MockProxy<I18nService>;
let mockVaultOnboardingService: MockProxy<VaultOnboardingServiceAbstraction>;
let mockStateProvider: Partial<StateProvider>;
let setInstallExtLinkSpy: any;
let individualVaultPolicyCheckSpy: any;
let mockConfigService: MockProxy<ConfigService>;
const mockAccountService: FakeAccountService = mockAccountServiceWith(Utils.newGuid() as UserId);
let mockStateProvider: Partial<StateProvider>;
beforeEach(() => {
mockPolicyService = mock<PolicyService>();
@@ -38,6 +43,7 @@ describe("VaultOnboardingComponent", () => {
getProfile: jest.fn(),
};
mockVaultOnboardingService = mock<VaultOnboardingServiceAbstraction>();
mockConfigService = mock<ConfigService>();
mockStateProvider = {
getActive: jest.fn().mockReturnValue(
of({
@@ -47,7 +53,6 @@ describe("VaultOnboardingComponent", () => {
}),
),
};
mockConfigService = mock<ConfigService>();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({
@@ -59,8 +64,9 @@ describe("VaultOnboardingComponent", () => {
{ provide: VaultOnboardingServiceAbstraction, useValue: mockVaultOnboardingService },
{ provide: I18nService, useValue: mockI18nService },
{ provide: ApiService, useValue: mockApiService },
{ provide: StateProvider, useValue: mockStateProvider },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: AccountService, useValue: mockAccountService },
{ provide: StateProvider, useValue: mockStateProvider },
],
}).compileComponents();
fixture = TestBed.createComponent(VaultOnboardingComponent);
@@ -71,11 +77,15 @@ describe("VaultOnboardingComponent", () => {
.mockReturnValue(undefined);
jest.spyOn(component, "checkCreationDate").mockReturnValue(null);
jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
(component as any).vaultOnboardingService.vaultOnboardingState$ = of({
createAccount: true,
importData: false,
installExtension: false,
});
(component as any).vaultOnboardingService.vaultOnboardingState$ = jest
.fn()
.mockImplementation(() => {
return of({
createAccount: true,
importData: false,
installExtension: false,
});
});
});
it("should create", () => {
@@ -148,7 +158,7 @@ describe("VaultOnboardingComponent", () => {
it("should call getMessages when showOnboarding is true", () => {
const messageEventSubject = new Subject<MessageEvent>();
const messageEvent = new MessageEvent("message", {
data: VaultOnboardingMessages.HasBwInstalled,
data: VaultMessages.HasBwInstalled,
});
const getMessagesSpy = jest.spyOn(component, "getMessages");
@@ -158,7 +168,7 @@ describe("VaultOnboardingComponent", () => {
void fixture.whenStable().then(() => {
expect(window.postMessage).toHaveBeenCalledWith({
command: VaultOnboardingMessages.checkBwInstalled,
command: VaultMessages.checkBwInstalled,
});
expect(getMessagesSpy).toHaveBeenCalled();
});
@@ -169,13 +179,16 @@ describe("VaultOnboardingComponent", () => {
.spyOn((component as any).vaultOnboardingService, "setVaultOnboardingTasks")
.mockReturnValue(Promise.resolve());
(component as any).vaultOnboardingService.vaultOnboardingState$ = of({
createAccount: true,
importData: false,
installExtension: false,
});
const eventData = { data: { command: VaultOnboardingMessages.HasBwInstalled } };
(component as any).vaultOnboardingService.vaultOnboardingState$ = jest
.fn()
.mockImplementation(() => {
return of({
createAccount: true,
importData: false,
installExtension: false,
});
});
const eventData = { data: { command: VaultMessages.HasBwInstalled } };
(component as any).showOnboarding = true;

View File

@@ -18,11 +18,13 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LinkModule } from "@bitwarden/components";
@@ -58,25 +60,25 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
protected onboardingTasks$: Observable<VaultOnboardingTasks>;
protected showOnboarding = false;
protected extensionRefreshEnabled = false;
private activeId: UserId;
constructor(
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
private apiService: ApiService,
private vaultOnboardingService: VaultOnboardingServiceAbstraction,
private configService: ConfigService,
private accountService: AccountService,
) {}
async ngOnInit() {
this.onboardingTasks$ = this.vaultOnboardingService.vaultOnboardingState$;
this.activeId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.onboardingTasks$ = this.vaultOnboardingService.vaultOnboardingState$(this.activeId);
await this.setOnboardingTasks();
this.setInstallExtLink();
this.individualVaultPolicyCheck();
this.checkForBrowserExtension();
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
async ngOnChanges(changes: SimpleChanges) {
@@ -87,7 +89,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
importData: this.ciphers.length > 0,
installExtension: currentTasks.installExtension,
};
await this.vaultOnboardingService.setVaultOnboardingTasks(updatedTasks);
await this.vaultOnboardingService.setVaultOnboardingTasks(this.activeId, updatedTasks);
}
}
@@ -104,19 +106,19 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
void this.getMessages(event);
});
window.postMessage({ command: VaultOnboardingMessages.checkBwInstalled });
window.postMessage({ command: VaultMessages.checkBwInstalled });
}
}
async getMessages(event: any) {
if (event.data.command === VaultOnboardingMessages.HasBwInstalled && this.showOnboarding) {
if (event.data.command === VaultMessages.HasBwInstalled && this.showOnboarding) {
const currentTasks = await firstValueFrom(this.onboardingTasks$);
const updatedTasks = {
createAccount: currentTasks.createAccount,
importData: currentTasks.importData,
installExtension: true,
};
await this.vaultOnboardingService.setVaultOnboardingTasks(updatedTasks);
await this.vaultOnboardingService.setVaultOnboardingTasks(this.activeId, updatedTasks);
}
}
@@ -159,7 +161,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
private async saveCompletedTasks(vaultTasks: VaultOnboardingTasks) {
this.showOnboarding = Object.values(vaultTasks).includes(false);
await this.vaultOnboardingService.setVaultOnboardingTasks(vaultTasks);
await this.vaultOnboardingService.setVaultOnboardingTasks(this.activeId, vaultTasks);
}
individualVaultPolicyCheck() {

View File

@@ -1,15 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef } from "@angular/cdk/dialog";
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import {
BehaviorSubject,
@@ -42,7 +34,6 @@ import {
Unassigned,
} from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
@@ -53,13 +44,12 @@ import {
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -71,7 +61,6 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
@@ -105,13 +94,11 @@ import { VaultItemEvent } from "../components/vault-items/vault-item-event";
import { VaultItemsModule } from "../components/vault-items/vault-items.module";
import { getNestedCollectionTree } from "../utils/collection-utils";
import { AddEditComponent } from "./add-edit.component";
import {
AttachmentDialogCloseResult,
AttachmentDialogResult,
AttachmentsV2Component,
} from "./attachments-v2.component";
import { AttachmentsComponent } from "./attachments.component";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,
@@ -160,15 +147,6 @@ const SearchTextDebounceInterval = 200;
})
export class VaultComponent implements OnInit, OnDestroy {
@ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("share", { read: ViewContainerRef, static: true }) shareModalRef: ViewContainerRef;
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
trashCleanupWarning: string = null;
kdfIterations: number;
@@ -189,11 +167,9 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string>;
private activeUserId: UserId;
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
private extensionRefreshEnabled: boolean;
private hasSubscription$ = new BehaviorSubject<boolean>(false);
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
@@ -260,7 +236,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private router: Router,
private changeDetectorRef: ChangeDetectorRef,
private i18nService: I18nService,
private modalService: ModalService,
private dialogService: DialogService,
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
@@ -278,7 +253,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private eventCollectionService: EventCollectionService,
private searchService: SearchService,
private searchPipe: SearchPipe,
private configService: ConfigService,
private apiService: ApiService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
private toastService: ToastService,
@@ -297,9 +271,7 @@ export class VaultComponent implements OnInit, OnDestroy {
: "trashCleanupWarning",
);
this.activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const firstSetup$ = this.route.queryParams.pipe(
first(),
@@ -363,13 +335,15 @@ export class VaultComponent implements OnInit, OnDestroy {
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
const ciphers$ = combineLatest([
this.cipherService.cipherViews$.pipe(filter((c) => c !== null)),
this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)),
filter$,
this.currentSearchText$,
]).pipe(
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
concatMap(async ([ciphers, filter, searchText]) => {
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$);
const failedCiphers = await firstValueFrom(
this.cipherService.failedToDecryptCiphers$(activeUserId),
);
const filterFunction = createFilterFunction(filter);
// Append any failed to decrypt ciphers to the top of the cipher list
const allCiphers = [...failedCiphers, ...ciphers];
@@ -437,15 +411,15 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$
.pipe(
switchMap(() => this.route.queryParams),
// Only process the queryParams if the dialog is not open (only when extension refresh is enabled)
filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled),
// Only process the queryParams if the dialog is not open
filter(() => this.vaultItemDialogRef == undefined),
switchMap(async (params) => {
const cipherId = getCipherIdFromParams(params);
if (cipherId) {
if (await this.cipherService.get(cipherId)) {
if (await this.cipherService.get(cipherId, activeUserId)) {
let action = params.action;
// Default to "view" if extension refresh is enabled
if (action == null && this.extensionRefreshEnabled) {
// Default to "view"
if (action == null) {
action = "view";
}
@@ -485,7 +459,7 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$
.pipe(
switchMap(() => this.cipherService.failedToDecryptCiphers$),
switchMap(() => this.cipherService.failedToDecryptCiphers$(activeUserId)),
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
filter((ciphers) => ciphers.length > 0),
take(1),
@@ -506,7 +480,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(() =>
combineLatest([
filter$,
this.billingAccountProfileStateService.hasPremiumFromAnySource$(this.activeUserId),
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
allCollections$,
this.organizations$,
ciphers$,
@@ -544,11 +518,6 @@ export class VaultComponent implements OnInit, OnDestroy {
this.refreshing = false;
},
);
// Check if the extension refresh feature flag is enabled
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
ngOnDestroy() {
@@ -642,8 +611,7 @@ export class VaultComponent implements OnInit, OnDestroy {
* Handles opening the attachments dialog for a cipher.
* Runs several checks to ensure that the user has the correct permissions
* and then opens the attachments dialog.
* Uses the new AttachmentsV2Component if the extensionRefresh feature flag is enabled.
*
* Uses the new AttachmentsV2Component
* @param cipher
* @returns
*/
@@ -668,51 +636,20 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
const canEditAttachments = await this.canEditAttachments(cipher);
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
cipherId: cipher.id as CipherId,
});
let madeAttachmentChanges = false;
const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed);
if (this.extensionRefreshEnabled) {
const dialogRef = AttachmentsV2Component.open(this.dialogService, {
cipherId: cipher.id as CipherId,
});
const result: AttachmentDialogCloseResult = await lastValueFrom(dialogRef.closed);
if (
result.action === AttachmentDialogResult.Uploaded ||
result.action === AttachmentDialogResult.Removed
) {
this.refresh();
}
return;
if (
result.action === AttachmentDialogResult.Uploaded ||
result.action === AttachmentDialogResult.Removed
) {
this.refresh();
}
const [modal] = await this.modalService.openViewRef(
AttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.viewOnly = !canEditAttachments;
comp.onUploadedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
comp.onDeletedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
comp.onReuploadedAttachment
.pipe(takeUntil(this.destroy$))
.subscribe(() => (madeAttachmentChanges = true));
},
);
modal.onClosed.pipe(takeUntil(this.destroy$)).subscribe(() => {
if (madeAttachmentChanges) {
this.refresh();
}
madeAttachmentChanges = false;
});
return;
}
/**
@@ -751,48 +688,13 @@ export class VaultComponent implements OnInit, OnDestroy {
await this.go({ cipherId: null, itemId: null, action: null });
}
async addCipher(cipherType?: CipherType) {
const type = cipherType ?? this.activeFilter.cipherType;
if (this.extensionRefreshEnabled) {
return this.addCipherV2(type);
}
const component = (await this.editCipher(null)) as AddEditComponent;
component.type = type;
if (
this.activeFilter.organizationId !== "MyVault" &&
this.activeFilter.organizationId != null
) {
component.organizationId = this.activeFilter.organizationId;
component.collections = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).filter((c) => !c.readOnly && c.id != null);
}
const selectedColId = this.activeFilter.collectionId;
if (selectedColId !== "AllCollections" && selectedColId != null) {
const selectedCollection = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).find((c) => c.id === selectedColId);
component.organizationId = selectedCollection?.organizationId;
if (!selectedCollection.readOnly) {
component.collectionIds = [selectedColId];
}
}
component.folderId = this.activeFilter.folderId;
}
/**
* Opens the add cipher dialog.
* @param cipherType The type of cipher to add.
* @returns The dialog reference.
*/
async addCipherV2(cipherType?: CipherType) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
"add",
null,
cipherType,
);
async addCipher(cipherType?: CipherType) {
const type = cipherType ?? this.activeFilter.cipherType;
const cipherFormConfig = await this.cipherFormConfigService.buildConfig("add", null, type);
const collectionId =
this.activeFilter.collectionId !== "AllCollections" && this.activeFilter.collectionId != null
? this.activeFilter.collectionId
@@ -823,8 +725,15 @@ export class VaultComponent implements OnInit, OnDestroy {
return this.editCipherId(cipher?.id, cloneMode);
}
/**
* Edit a cipher using the new VaultItemDialog.
* @param id
* @param cloneMode
* @returns
*/
async editCipherId(id: string, cloneMode?: boolean) {
const cipher = await this.cipherService.get(id);
const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const cipher = await this.cipherService.get(id, activeUserId);
if (
cipher &&
@@ -836,49 +745,6 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if (this.extensionRefreshEnabled) {
await this.editCipherIdV2(cipher, cloneMode);
return;
}
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
(comp) => {
comp.cipherId = id;
comp.collectionId = this.selectedCollection?.node.id;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
},
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
modal.onClosedPromise().then(() => {
void this.go({ cipherId: null, itemId: null, action: null });
});
return childComponent;
}
/**
* Edit a cipher using the new VaultItemDialog.
*
* @param cipher
* @param cloneMode
*/
private async editCipherIdV2(cipher: Cipher, cloneMode?: boolean) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
cloneMode ? "clone" : "edit",
cipher.id as CipherId,
@@ -903,7 +769,8 @@ export class VaultComponent implements OnInit, OnDestroy {
* @returns Promise<void>
*/
async viewCipherById(id: string) {
const cipher = await this.cipherService.get(id);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.get(id, activeUserId);
// If cipher exists (cipher is null when new) and MP reprompt
// is on for this cipher, then show password reprompt.
if (
@@ -1076,11 +943,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
const component = await this.editCipher(cipher, true);
if (component != null) {
component.cloneMode = true;
}
await this.editCipher(cipher, true);
}
restore = async (c: CipherView): Promise<boolean> => {
@@ -1098,7 +961,8 @@ export class VaultComponent implements OnInit, OnDestroy {
}
try {
await this.cipherService.restoreWithServer(c.id);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherService.restoreWithServer(c.id, activeUserId);
this.toastService.showToast({
variant: "success",
title: null,
@@ -1180,7 +1044,8 @@ export class VaultComponent implements OnInit, OnDestroy {
}
try {
await this.deleteCipherWithServer(c.id, permanent);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.deleteCipherWithServer(c.id, activeUserId, permanent);
this.toastService.showToast({
variant: "success",
@@ -1315,10 +1180,10 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
protected deleteCipherWithServer(id: string, permanent: boolean) {
protected deleteCipherWithServer(id: string, userId: UserId, permanent: boolean) {
return permanent
? this.cipherService.deleteWithServer(id)
: this.cipherService.softDeleteWithServer(id);
? this.cipherService.deleteWithServer(id, userId)
: this.cipherService.softDeleteWithServer(id, userId);
}
protected async repromptCipher(ciphers: CipherView[]) {
@@ -1331,15 +1196,6 @@ export class VaultComponent implements OnInit, OnDestroy {
this.refresh$.next();
}
private async canEditAttachments(cipher: CipherView) {
if (cipher.organizationId == null || cipher.edit) {
return true;
}
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
return organization.canEditAllCiphers;
}
private async go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {

View File

@@ -165,10 +165,11 @@ export class ViewComponent implements OnInit {
*/
protected async deleteCipher(): Promise<void> {
const asAdmin = this.organization?.canEditAllCiphers;
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (this.cipher.isDeleted) {
await this.cipherService.deleteWithServer(this.cipher.id, asAdmin);
await this.cipherService.deleteWithServer(this.cipher.id, userId, asAdmin);
} else {
await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin);
await this.cipherService.softDeleteWithServer(this.cipher.id, userId, asAdmin);
}
}

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore
import { DatePipe } from "@angular/common";
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -10,6 +11,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -98,8 +100,9 @@ export class AddEditComponent extends BaseAddEditComponent {
protected async loadCipher() {
this.isAdminConsoleAction = true;
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
// Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin
const firstCipherCheck = await super.loadCipher();
const firstCipherCheck = await super.loadCipher(activeUserId);
if (!this.organization.canEditAllCiphers && firstCipherCheck != null) {
return firstCipherCheck;
@@ -123,7 +126,8 @@ export class AddEditComponent extends BaseAddEditComponent {
protected async deleteCipher() {
if (!this.organization.canEditAllCiphers) {
return super.deleteCipher();
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return super.deleteCipher(activeUserId);
}
return this.cipher.isDeleted
? this.apiService.deleteCipherAdmin(this.cipherId)

View File

@@ -1,10 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
@@ -74,7 +76,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
protected async loadCipher() {
if (!this.organization.canEditAllCiphers) {
return await super.loadCipher();
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return await super.loadCipher(activeUserId);
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
return new Cipher(new CipherData(response));
@@ -89,9 +92,9 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
);
}
protected deleteCipherAttachment(attachmentId: string) {
protected deleteCipherAttachment(attachmentId: string, userId: UserId) {
if (!this.organization.canEditAllCiphers) {
return super.deleteCipherAttachment(attachmentId);
return super.deleteCipherAttachment(attachmentId, userId);
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
}

View File

@@ -7,7 +7,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -26,6 +27,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
isMember: true,
enabled: true,
status: OrganizationUserStatusType.Confirmed,
userId: "UserId",
};
const testOrg2 = {
id: "333-999-888",
@@ -34,6 +36,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
isMember: true,
enabled: true,
status: OrganizationUserStatusType.Confirmed,
userId: "UserId",
};
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
const collection = {
@@ -80,17 +83,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
},
{ provide: ApiService, useValue: { getCipherAdmin } },
{ provide: CipherService, useValue: { get: getCipher } },
{
provide: AccountService,
useValue: {
activeAccount$: new BehaviorSubject<Account>({
id: "123-456-789" as UserId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
}),
},
},
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
],
});
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
@@ -207,7 +200,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(getCipherAdmin).not.toHaveBeenCalled();
expect(getCipher).toHaveBeenCalledWith(cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId, "UserId");
});
});
});

View File

@@ -10,7 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
@@ -100,7 +100,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
return null;
}
const localCipher = await this.cipherService.get(id);
const localCipher = await this.cipherService.get(id, organization.userId as UserId);
// Fetch from the API because we don't need the permissions in local state OR the cipher was not found (e.g. unassigned)
if (organization.canEditAllCiphers || localCipher == null) {

View File

@@ -104,8 +104,8 @@
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned && organization"
class="tw-shrink-0"
>
<!-- "New" menu is always shown for Extension Refresh unless the user cannot create a cipher -->
<ng-container *ngIf="extensionRefreshEnabled && canCreateCipher; else nonRefresh">
<!-- "New" menu is always shown unless the user cannot create a cipher and cannot create a collection-->
<ng-container *ngIf="canCreateCipher || canCreateCollection">
<div appListDropdown>
<button
bitButton
@@ -119,24 +119,26 @@
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<ng-container *ngIf="canCreateCipher">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="canCreateCollection">
<bit-menu-divider></bit-menu-divider>
<bit-menu-divider *ngIf="canCreateCipher"></bit-menu-divider>
<button type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
@@ -145,56 +147,5 @@
</bit-menu>
</div>
</ng-container>
<ng-template #nonRefresh>
<!-- Show a menu when the user can create a cipher and collection -->
<div *ngIf="canCreateCipher && canCreateCollection" appListDropdown>
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</div>
<!-- Show a single button when the user can only create a cipher -->
<button
*ngIf="canCreateCipher && !canCreateCollection"
type="button"
bitButton
buttonType="primary"
(click)="addCipher()"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
<!-- Show a single button when the user can only create a collection -->
<button
*ngIf="canCreateCollection && !canCreateCipher"
type="button"
bitButton
buttonType="primary"
(click)="addCollection()"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newCollection" | i18n }}
</button>
</ng-template>
</div>
</app-header>

View File

@@ -13,7 +13,6 @@ import {
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -90,11 +89,6 @@ export class VaultHeaderComponent implements OnInit {
protected CollectionDialogTabType = CollectionDialogTabType;
/**
* Whether the extension refresh feature flag is enabled.
*/
protected extensionRefreshEnabled = false;
/** The cipher type enum. */
protected CipherType = CipherType;
@@ -106,11 +100,7 @@ export class VaultHeaderComponent implements OnInit {
private configService: ConfigService,
) {}
async ngOnInit() {
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
async ngOnInit() {}
get title() {
const headerType = this.i18nService.t("collections").toLowerCase();

View File

@@ -1,15 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DialogRef } from "@angular/cdk/dialog";
import {
ChangeDetectorRef,
Component,
NgZone,
OnDestroy,
OnInit,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import {
BehaviorSubject,
@@ -37,12 +29,10 @@ import {
import {
CollectionAdminService,
CollectionAdminView,
CollectionService,
CollectionView,
Unassigned,
} from "@bitwarden/admin-console/common";
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
@@ -50,6 +40,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
@@ -62,7 +53,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -127,7 +118,6 @@ import {
import { VaultHeaderComponent } from "../org-vault/vault-header/vault-header.component";
import { getNestedCollectionTree } from "../utils/collection-utils";
import { AddEditComponent } from "./add-edit.component";
import {
BulkCollectionsDialogComponent,
BulkCollectionsDialogResult,
@@ -166,13 +156,6 @@ enum AddAccessStatusType {
export class VaultComponent implements OnInit, OnDestroy {
protected Unassigned = Unassigned;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true })
cipherAddEditModalRef: ViewContainerRef;
@ViewChild("collectionsModal", { read: ViewContainerRef, static: true })
collectionsModalRef: ViewContainerRef;
trashCleanupWarning: string = null;
activeFilter: VaultFilter = new VaultFilter();
@@ -210,7 +193,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
private extensionRefreshEnabled: boolean;
private resellerManagedOrgAlert: boolean;
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
@@ -249,7 +231,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService,
private i18nService: I18nService,
private modalService: ModalService,
private dialogService: DialogService,
private messagingService: MessagingService,
private broadcasterService: BroadcasterService,
@@ -265,7 +246,6 @@ export class VaultComponent implements OnInit, OnDestroy {
private eventCollectionService: EventCollectionService,
private totpService: TotpService,
private apiService: ApiService,
private collectionService: CollectionService,
private toastService: ToastService,
private configService: ConfigService,
private cipherFormConfigService: CipherFormConfigService,
@@ -278,10 +258,6 @@ export class VaultComponent implements OnInit, OnDestroy {
) {}
async ngOnInit() {
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
FeatureFlag.ResellerManagedOrgAlert,
);
@@ -555,7 +531,7 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$
.pipe(
switchMap(() => combineLatest([this.route.queryParams, allCipherMap$])),
filter(() => this.vaultItemDialogRef == undefined || !this.extensionRefreshEnabled),
filter(() => this.vaultItemDialogRef == undefined),
switchMap(async ([qParams, allCiphersMap]) => {
const cipherId = getCipherIdFromParams(qParams);
@@ -586,15 +562,15 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
// Default to "view" if extension refresh is enabled
if (action == null && this.extensionRefreshEnabled) {
// Default to "view"
if (action == null) {
action = "view";
}
if (action === "view") {
await this.viewCipherById(cipher);
} else {
await this.editCipherId(cipher, false);
await this.editCipher(cipher, false);
}
} else {
this.toastService.showToast({
@@ -836,27 +812,8 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
/** Opens the Add/Edit Dialog */
async addCipher(cipherType?: CipherType) {
if (this.extensionRefreshEnabled) {
return this.addCipherV2(cipherType);
}
let collections: CollectionView[] = [];
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(null, false, (comp) => {
comp.type = cipherType || this.activeFilter.cipherType;
comp.collections = collections;
if (this.activeFilter.collectionId) {
comp.collectionIds = [this.activeFilter.collectionId];
}
});
}
/** Opens the Add/Edit Dialog. Only to be used when the BrowserExtension feature flag is active */
async addCipherV2(cipherType?: CipherType) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
"add",
null,
@@ -877,24 +834,8 @@ export class VaultComponent implements OnInit, OnDestroy {
* Edit the given cipher or add a new cipher
* @param cipherView - When set, the cipher to be edited
* @param cloneCipher - `true` when the cipher should be cloned.
* Used in place of the `additionalComponentParameters`, as
* the `editCipherIdV2` method has a differing implementation.
* @param defaultComponentParameters - A method that takes in an instance of
* the `AddEditComponent` to edit methods directly.
*/
async editCipher(
cipher: CipherView | null,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
return this.editCipherId(cipher, cloneCipher, additionalComponentParameters);
}
async editCipherId(
cipher: CipherView | null,
cloneCipher: boolean,
additionalComponentParameters?: (comp: AddEditComponent) => void,
) {
async editCipher(cipher: CipherView | null, cloneCipher: boolean) {
if (
cipher &&
cipher.reprompt !== 0 &&
@@ -905,55 +846,6 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if (this.extensionRefreshEnabled) {
await this.editCipherIdV2(cipher, cloneCipher);
return;
}
const defaultComponentParameters = (comp: AddEditComponent) => {
comp.organization = this.organization;
comp.organizationId = this.organization.id;
comp.cipherId = cipher?.id;
comp.collectionId = this.activeFilter.collectionId;
comp.onSavedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onDeletedCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
comp.onRestoredCipher.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
};
const [modal, childComponent] = await this.modalService.openViewRef(
AddEditComponent,
this.cipherAddEditModalRef,
additionalComponentParameters == null
? defaultComponentParameters
: (comp) => {
defaultComponentParameters(comp);
additionalComponentParameters(comp);
},
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
modal.onClosedPromise().then(() => {
this.go({ cipherId: null, itemId: null, action: null });
});
return childComponent;
}
/**
* Edit a cipher using the new AddEditCipherDialogV2 component.
* Only to be used behind the ExtensionRefresh feature flag.
*/
private async editCipherIdV2(cipher: CipherView | null, cloneCipher: boolean) {
const cipherFormConfig = await this.cipherFormConfigService.buildConfig(
cloneCipher ? "clone" : "edit",
cipher?.id as CipherId | null,
@@ -1038,16 +930,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
let collections: CollectionView[] = [];
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(cipher, true, (comp) => {
comp.cloneMode = true;
comp.collections = collections;
comp.collectionIds = cipher.collectionIds;
});
await this.editCipher(cipher, true);
}
restore = async (c: CipherView): Promise<boolean> => {
@@ -1070,8 +953,9 @@ export class VaultComponent implements OnInit, OnDestroy {
// Allow restore of an Unassigned Item
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const asAdmin = this.organization?.canEditAnyCollection || c.isUnassigned;
await this.cipherService.restoreWithServer(c.id, asAdmin);
await this.cipherService.restoreWithServer(c.id, activeUserId, asAdmin);
this.toastService.showToast({
variant: "success",
title: null,
@@ -1162,7 +1046,8 @@ export class VaultComponent implements OnInit, OnDestroy {
}
try {
await this.deleteCipherWithServer(c.id, permanent, c.isUnassigned);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.deleteCipherWithServer(c.id, activeUserId, permanent, c.isUnassigned);
this.toastService.showToast({
variant: "success",
title: null,
@@ -1450,11 +1335,16 @@ export class VaultComponent implements OnInit, OnDestroy {
});
}
protected deleteCipherWithServer(id: string, permanent: boolean, isUnassigned: boolean) {
protected deleteCipherWithServer(
id: string,
userId: UserId,
permanent: boolean,
isUnassigned: boolean,
) {
const asAdmin = this.organization?.canEditAllCiphers || isUnassigned;
return permanent
? this.cipherService.deleteWithServer(id, asAdmin)
: this.cipherService.softDeleteWithServer(id, asAdmin);
? this.cipherService.deleteWithServer(id, userId, asAdmin)
: this.cipherService.softDeleteWithServer(id, userId, asAdmin);
}
protected async repromptCipher(ciphers: CipherView[]) {

View File

@@ -0,0 +1,173 @@
import { TestBed } from "@angular/core/testing";
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "./browser-extension-prompt.service";
describe("BrowserExtensionPromptService", () => {
let service: BrowserExtensionPromptService;
const setAnonLayoutWrapperData = jest.fn();
const isFirefox = jest.fn().mockReturnValue(false);
const postMessage = jest.fn();
window.postMessage = postMessage;
beforeEach(() => {
setAnonLayoutWrapperData.mockClear();
postMessage.mockClear();
isFirefox.mockClear();
TestBed.configureTestingModule({
providers: [
BrowserExtensionPromptService,
{ provide: AnonLayoutWrapperDataService, useValue: { setAnonLayoutWrapperData } },
{ provide: PlatformUtilsService, useValue: { isFirefox } },
],
});
jest.useFakeTimers();
service = TestBed.inject(BrowserExtensionPromptService);
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
it("defaults page state to loading", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.Loading);
done();
});
});
describe("start", () => {
it("posts message to check for extension", () => {
service.start();
expect(window.postMessage).toHaveBeenCalledWith({
command: VaultMessages.checkBwInstalled,
});
});
it("sets timeout for error state", () => {
service.start();
expect(service["extensionCheckTimeout"]).not.toBeNull();
});
it("attempts to open the extension when installed", () => {
service.start();
window.dispatchEvent(
new MessageEvent("message", { data: { command: VaultMessages.HasBwInstalled } }),
);
expect(window.postMessage).toHaveBeenCalledTimes(2);
expect(window.postMessage).toHaveBeenCalledWith({ command: VaultMessages.OpenPopup });
});
});
describe("success state", () => {
beforeEach(() => {
service.start();
window.dispatchEvent(
new MessageEvent("message", { data: { command: VaultMessages.PopupOpened } }),
);
});
it("sets layout title", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "openedExtension" },
});
});
it("sets success page state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.Success);
done();
});
});
it("clears the error timeout", () => {
expect(service["extensionCheckTimeout"]).toBeUndefined();
});
});
describe("firefox", () => {
beforeEach(() => {
isFirefox.mockReturnValue(true);
service.start();
});
afterEach(() => {
isFirefox.mockReturnValue(false);
});
it("sets manual open state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.ManualOpen);
done();
});
});
it("sets error state after timeout", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "somethingWentWrong" },
});
});
});
describe("mobile state", () => {
beforeEach(() => {
Utils.isMobileBrowser = true;
service.start();
});
afterEach(() => {
Utils.isMobileBrowser = false;
});
it("sets mobile state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.MobileBrowser);
done();
});
});
it("sets desktop required title", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "desktopRequired" },
});
});
it("clears the error timeout", () => {
expect(service["extensionCheckTimeout"]).toBeUndefined();
});
});
describe("error state", () => {
beforeEach(() => {
service.start();
jest.advanceTimersByTime(1000);
});
it("sets error state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.Error);
done();
});
});
it("sets error state after timeout", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "somethingWentWrong" },
});
});
});
});

View File

@@ -0,0 +1,125 @@
import { DestroyRef, Injectable } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, fromEvent } from "rxjs";
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
export enum BrowserPromptState {
Loading = "loading",
Error = "error",
Success = "success",
ManualOpen = "manualOpen",
MobileBrowser = "mobileBrowser",
}
type PromptErrorStates = BrowserPromptState.Error | BrowserPromptState.ManualOpen;
@Injectable({
providedIn: "root",
})
export class BrowserExtensionPromptService {
private _pageState$ = new BehaviorSubject<BrowserPromptState>(BrowserPromptState.Loading);
/** Current state of the prompt page */
pageState$ = this._pageState$.asObservable();
/** Timeout identifier for extension check */
private extensionCheckTimeout: number | undefined;
constructor(
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private destroyRef: DestroyRef,
private platformUtilsService: PlatformUtilsService,
) {}
start(): void {
if (Utils.isMobileBrowser) {
this.setMobileState();
return;
}
// Firefox does not support automatically opening the extension,
// it currently requires a user gesture within the context of the extension to open.
// Show message to direct the user to manually open the extension.
// Mozilla Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1799344
if (this.platformUtilsService.isFirefox()) {
this.setErrorState(BrowserPromptState.ManualOpen);
return;
}
this.checkForBrowserExtension();
}
/** Post a message to the extension to open */
openExtension() {
window.postMessage({ command: VaultMessages.OpenPopup });
}
/** Send message checking for the browser extension */
private checkForBrowserExtension() {
fromEvent<MessageEvent>(window, "message")
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
void this.getMessages(event);
});
window.postMessage({ command: VaultMessages.checkBwInstalled });
// Wait a second for the extension to respond and open, else show the error state
this.extensionCheckTimeout = window.setTimeout(() => {
this.setErrorState();
}, 1000);
}
/** Handle window message events */
private getMessages(event: any) {
if (event.data.command === VaultMessages.HasBwInstalled) {
this.openExtension();
}
if (event.data.command === VaultMessages.PopupOpened) {
this.setSuccessState();
}
}
/** Show message that this page should be opened on a desktop browser */
private setMobileState() {
this.clearExtensionCheckTimeout();
this._pageState$.next(BrowserPromptState.MobileBrowser);
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "desktopRequired",
},
});
}
/** Show the open extension success state */
private setSuccessState() {
this.clearExtensionCheckTimeout();
this._pageState$.next(BrowserPromptState.Success);
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "openedExtension",
},
});
}
/** Show open extension error state */
private setErrorState(errorState?: PromptErrorStates) {
this.clearExtensionCheckTimeout();
this._pageState$.next(errorState ?? BrowserPromptState.Error);
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "somethingWentWrong",
},
});
}
private clearExtensionCheckTimeout() {
window.clearTimeout(this.extensionCheckTimeout);
this.extensionCheckTimeout = undefined;
}
}

View File

@@ -464,6 +464,18 @@
"editFolder": {
"message": "Wysig vouer"
},
"newFolder": {
"message": "New folder"
},
"folderName": {
"message": "Folder name"
},
"folderHintText": {
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
},
"deleteFolderPermanently": {
"message": "Are you sure you want to permanently delete this folder?"
},
"baseDomain": {
"message": "Basisdomein",
"description": "Domain name. Example: website.com"
@@ -728,15 +740,6 @@
"itemName": {
"message": "Item name"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"ex": {
"message": "bv.",
"description": "Short abbreviation for 'example'."
@@ -1182,6 +1185,9 @@
"logInInitiated": {
"message": "Log in initiated"
},
"logInRequestSent": {
"message": "Request sent"
},
"submit": {
"message": "Dien in"
},
@@ -1371,12 +1377,39 @@
"notificationSentDevice": {
"message": "n Kennisgewing is na u toestel gestuur."
},
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the "
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Confirm access"
},
"denyAccess": {
"message": "Deny access"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"notificationSentDeviceComplete": {
"message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"versionNumber": {
"message": "Weergawe $VERSION_NUMBER$",
"placeholders": {
@@ -1664,9 +1697,6 @@
"message": "Avoid ambiguous characters",
"description": "Label for the avoid ambiguous characters checkbox."
},
"regeneratePassword": {
"message": "Hergenereer wagwoord"
},
"length": {
"message": "Lengte"
},
@@ -2170,8 +2200,20 @@
"manage": {
"message": "Bestuur"
},
"canManage": {
"message": "Can manage"
"manageCollection": {
"message": "Manage collection"
},
"viewItems": {
"message": "View items"
},
"viewItemsHidePass": {
"message": "View items, hidden passwords"
},
"editItems": {
"message": "Edit items"
},
"editItemsHidePass": {
"message": "Edit items, hidden passwords"
},
"disable": {
"message": "Deaktiveer"
@@ -2368,11 +2410,8 @@
"twoFactorU2fProblemReadingTryAgain": {
"message": "There was a problem reading the security key. Try again."
},
"twoFactorWebAuthnWarning": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:"
},
"twoFactorWebAuthnSupportWeb": {
"message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)."
"twoFactorWebAuthnWarning1": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used."
},
"twoFactorRecoveryYourCode": {
"message": "U herstelkode vir Bitwarden-tweestapaantekening"
@@ -4710,9 +4749,6 @@
"passwordGeneratorPolicyDesc": {
"message": "Stel minimum vereistes vir opstelling van wagwoordgenereerder."
},
"passwordGeneratorPolicyInEffect": {
"message": "Een of meer organisasiebeleide beïnvloed u genereerderinstellings."
},
"masterPasswordPolicyInEffect": {
"message": "Een of meer organisasiebeleide stel die volgende eise aan u hoofwagwoord:"
},
@@ -6681,15 +6717,6 @@
"message": "Genereerder",
"description": "Short for 'credential generator'."
},
"whatWouldYouLikeToGenerate": {
"message": "Wat wil u genereer?"
},
"passwordType": {
"message": "Wagwoordtipe"
},
"regenerateUsername": {
"message": "Hergenereer gebruikersnaam"
},
"generateUsername": {
"message": "Genereer gebruikersnaam"
},
@@ -6730,9 +6757,6 @@
}
}
},
"usernameType": {
"message": "Gebruikersnaamtipe"
},
"plusAddressedEmail": {
"message": "E-posadres met plus",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -6952,9 +6976,6 @@
"message": "Gasheernaam",
"description": "Part of a URL."
},
"apiAccessToken": {
"message": "API-toegangsteken"
},
"deviceVerification": {
"message": "Toestelbevestiging"
},
@@ -7666,18 +7687,6 @@
"noCollection": {
"message": "Geen versameling"
},
"canView": {
"message": "Kan bekyk"
},
"canViewExceptPass": {
"message": "Kan bekyk, behalwe wagwoorde"
},
"canEdit": {
"message": "Kan wysig"
},
"canEditExceptPass": {
"message": "Kan wysig behalwe wagwoorde"
},
"noCollectionsAdded": {
"message": "Geen versamelings toegevoeg"
},
@@ -8677,9 +8686,6 @@
"message": "Self-host server URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"aliasDomain": {
"message": "Alias domain"
},
"alreadyHaveAccount": {
"message": "Already have an account?"
},
@@ -8741,11 +8747,11 @@
"readOnlyCollectionAccess": {
"message": "You do not have access to manage this collection."
},
"grantAddAccessCollectionWarningTitle": {
"message": "Missing Can Manage Permissions"
"grantManageCollectionWarningTitle": {
"message": "Missing Manage Collection Permissions"
},
"grantAddAccessCollectionWarning": {
"message": "Grant Can manage permissions to allow full collection management including deletion of collection."
"grantManageCollectionWarning": {
"message": "Grant Manage collection permissions to allow full collection management including deletion of collection."
},
"grantCollectionAccess": {
"message": "Grant groups or members access to this collection."
@@ -10091,6 +10097,15 @@
"descriptorCode": {
"message": "Descriptor code"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"importantNotice": {
"message": "Important notice"
},
@@ -10281,5 +10296,54 @@
"example": "Acme c"
}
}
},
"accountDeprovisioningNotification": {
"message": "Administrators now have the ability to delete member accounts that belong to a claimed domain."
},
"deleteManagedUserWarningDesc": {
"message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action."
},
"deleteManagedUserWarning": {
"message": "Delete is a new action!"
},
"seatsRemaining": {
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
"placeholders": {
"remaining": {
"content": "$1",
"example": "5"
},
"total": {
"content": "$2",
"example": "10"
}
}
},
"existingOrganization": {
"message": "Existing organization"
},
"selectOrganizationProviderPortal": {
"message": "Select an organization to add to your Provider Portal."
},
"noOrganizations": {
"message": "There are no organizations to list"
},
"yourProviderSubscriptionCredit": {
"message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription."
},
"doYouWantToAddThisOrg": {
"message": "Do you want to add this organization to $PROVIDER$?",
"placeholders": {
"provider": {
"content": "$1",
"example": "Cool MSP"
}
}
},
"addedExistingOrganization": {
"message": "Added existing organization"
},
"assignedExceedsAvailable": {
"message": "Assigned seats exceed available seats."
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -464,6 +464,18 @@
"editFolder": {
"message": "Qovluğa düzəliş et"
},
"newFolder": {
"message": "Yeni qovluq"
},
"folderName": {
"message": "Qovluq adı"
},
"folderHintText": {
"message": "Ana qovluğun adından sonra \"/\" əlavə edərək qovluğu ardıcıl yerləşdirin. Nümunə: Social/Forums"
},
"deleteFolderPermanently": {
"message": "Bu qovluğu həmişəlik silmək istədiyinizə əminsiniz?"
},
"baseDomain": {
"message": "Baza domeni",
"description": "Domain name. Example: website.com"
@@ -728,15 +740,6 @@
"itemName": {
"message": "Element adı"
},
"cannotRemoveViewOnlyCollections": {
"message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"ex": {
"message": "məs.",
"description": "Short abbreviation for 'example'."
@@ -1182,6 +1185,9 @@
"logInInitiated": {
"message": "Giriş etmə başladıldı"
},
"logInRequestSent": {
"message": "Tələb göndərildi"
},
"submit": {
"message": "Göndər"
},
@@ -1371,12 +1377,39 @@
"notificationSentDevice": {
"message": "Cihazınıza bir bildiriş göndərildi."
},
"notificationSentDevicePart1": {
"message": "Cihazınızda Bitwarden kilidini açın, ya da "
},
"areYouTryingToAccessYourAccount": {
"message": "Hesabınıza müraciət etməyə çalışırsınız?"
},
"accessAttemptBy": {
"message": "$EMAIL$ ilə müraciət cəhdi",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Müraciəti təsdiqlə"
},
"denyAccess": {
"message": "Müraciətə rədd cavabı ver"
},
"notificationSentDeviceAnchor": {
"message": "veb tətbiqinizdə"
},
"notificationSentDevicePart2": {
"message": "Təsdiqləməzdən əvvəl Barmaq izi ifadəsinin aşağıdakı ifadə ilə uyuşduğuna əmin olun."
},
"notificationSentDeviceComplete": {
"message": "Cihazınızda Bitwarden-in kilidini açın. Təsdiqləməzdən əvvəl Barmaq izi ifadəsinin aşağıdakı ifadə ilə uyuşduğuna əmin olun."
},
"aNotificationWasSentToYourDevice": {
"message": "Cihazınıza bir bildiriş göndərildi"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Hesabınızın kilidinin açıq olduğuna və barmaq izi ifadəsinin digər cihazda uyuşduğuna əmin olun"
},
"versionNumber": {
"message": "Versiya $VERSION_NUMBER$",
"placeholders": {
@@ -1664,9 +1697,6 @@
"message": "Anlaşılmaz xarakterlərdən çəkin",
"description": "Label for the avoid ambiguous characters checkbox."
},
"regeneratePassword": {
"message": "Parolu yenidən yarat"
},
"length": {
"message": "Uzunluq"
},
@@ -2170,8 +2200,20 @@
"manage": {
"message": "İdarə et"
},
"canManage": {
"message": "İdarə edə bilər"
"manageCollection": {
"message": "Kolleksiyanı idarə et"
},
"viewItems": {
"message": "Elementlərə bax"
},
"viewItemsHidePass": {
"message": "Elementlərə, gizli parollara bax"
},
"editItems": {
"message": "Elementlərə düzəliş et"
},
"editItemsHidePass": {
"message": "Elementlərə, gizli parollara düzəliş et"
},
"disable": {
"message": "Sıradan çıxart"
@@ -2368,11 +2410,8 @@
"twoFactorU2fProblemReadingTryAgain": {
"message": "Güvənlik açarı oxunarkən problem yarandı. Yenidən sınayın."
},
"twoFactorWebAuthnWarning": {
"message": "Platforma məhdudiyyətlərinə görə, WebAuthn bütün Bitwarden tətbiqlərində istifadə edilə bilmir. WebAuthn istifadə edilə bilməyəndə, hesabınıza müraciət edə bilməyiniz üçün başqa bir iki addımlı giriş provayderini fəallaşdırmalısınız. Dəstəklənən platformalar:"
},
"twoFactorWebAuthnSupportWeb": {
"message": "WebAuthn dəstəkli brauzerə sahib masaüstü/dizüstü kompüterdə veb seyf və brauzer uzantıları (FIDO U2F açıq olan Chrome, Opera, Vivaldi və ya Firefox)."
"twoFactorWebAuthnWarning1": {
"message": "Platforma məhdudiyyətlərinə görə, WebAuthn bütün Bitwarden tətbiqlərində istifadə edilə bilmir. WebAuthn istifadə edilə bilməyəndə, hesabınıza müraciət edə bilməyiniz üçün başqa bir iki addımlı giriş provayderini fəallaşdırmalısınız."
},
"twoFactorRecoveryYourCode": {
"message": "Bitwarden iki addımlı giriş üçün geri qaytarma kodunuz"
@@ -3013,7 +3052,7 @@
"message": "Əlavə istifadəçi yerləri"
},
"userSeatsDesc": {
"message": "İstifadəçi sayı"
"message": "# / istifadəçi yeri"
},
"userSeatsAdditionalDesc": {
"message": "Planınızda $BASE_SEATS$ istifadəçi yeri var. İstifadəçiləri, istifadəçi/ay başına $SEAT_PRICE$ qarşılığında əlavə edə bilərsiniz.",
@@ -4284,7 +4323,7 @@
"message": "Sirr Menecerinə abunəliyiniz üçün bir limit müəyyən edin. Bu limitə çatanda, yeni istifadəçiləri dəvət edə bilməyəcəksiniz."
},
"maxSeatLimit": {
"message": "Maksimum yer limiti (ixtiyari)",
"message": "Yer limiti (ixtiyari)",
"description": "Upper limit of seats to allow through autoscaling"
},
"maxSeatCost": {
@@ -4710,9 +4749,6 @@
"passwordGeneratorPolicyDesc": {
"message": "Parol yaradıcı üçün tələbləri ayarla."
},
"passwordGeneratorPolicyInEffect": {
"message": "Bir və ya daha çox təşkilat siyasətləri yaradıcı seçimlərinizə təsir edir."
},
"masterPasswordPolicyInEffect": {
"message": "Bir və ya daha çox təşkilat siyasəti, aşağıdakı tələbləri qarşılamaq üçün ana parolunuzu tələb edir:"
},
@@ -6681,15 +6717,6 @@
"message": "Yaradıcı",
"description": "Short for 'credential generator'."
},
"whatWouldYouLikeToGenerate": {
"message": "Nə yaratmaq istəyirsiniz?"
},
"passwordType": {
"message": "Parol növü"
},
"regenerateUsername": {
"message": "İstifadəçi adını yenidən yarat"
},
"generateUsername": {
"message": "İstifadəçi adı yarat"
},
@@ -6730,9 +6757,6 @@
}
}
},
"usernameType": {
"message": "İstifadəçi adı növü"
},
"plusAddressedEmail": {
"message": "Plyus ünvanlı e-poçt",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -6952,9 +6976,6 @@
"message": "Host adı",
"description": "Part of a URL."
},
"apiAccessToken": {
"message": "API müraciət tokeni"
},
"deviceVerification": {
"message": "Cihaz doğrulaması"
},
@@ -7666,18 +7687,6 @@
"noCollection": {
"message": "Kolleksiya yoxdur"
},
"canView": {
"message": "Baxa bilər"
},
"canViewExceptPass": {
"message": "Parollar istisna olmaqla baxa bilər"
},
"canEdit": {
"message": "Düzəliş edə bilər"
},
"canEditExceptPass": {
"message": "Parollar istisna olmaqla düzəliş edə bilər"
},
"noCollectionsAdded": {
"message": "Heç bir kolleksiya əlavə edilmədi"
},
@@ -8626,7 +8635,7 @@
"message": "Kolleksiya silinməsini sahibləri və adminləri ilə məhdudlaşdır"
},
"limitItemDeletionDesc": {
"message": "Limit item deletion to members with the Can manage permission"
"message": "Elementin silinməsini \"İdarə edə bilər\" icazəsinə sahib üzvlərlə məhdudlaşdır"
},
"allowAdminAccessToAllCollectionItemsDesc": {
"message": "Sahiblər və adminlər bütün kolleksiyaları və elementləri idarə edə bilər"
@@ -8677,9 +8686,6 @@
"message": "Self-host server URL-si",
"description": "Label for field requesting a self-hosted integration service URL"
},
"aliasDomain": {
"message": "Domen ləqəbi"
},
"alreadyHaveAccount": {
"message": "Artıq bir hesabınız var?"
},
@@ -8741,11 +8747,11 @@
"readOnlyCollectionAccess": {
"message": "Bu kolleksiyanı idarə etmək üçün müraciətiniz yoxdur."
},
"grantAddAccessCollectionWarningTitle": {
"message": "İdarə edə bilər icazələri əskikdir"
"grantManageCollectionWarningTitle": {
"message": "\"Kolleksiyanı idarə etmə\" icazələri əskikdir"
},
"grantAddAccessCollectionWarning": {
"message": "Kolleksiyanın silinməsi daxil olmaqla tam kolleksiya idarəetməsinə icazə vermək üçün \"İdarə edə bilər\" icazələrini verin."
"grantManageCollectionWarning": {
"message": "Kolleksiyanın silinməsi daxil olmaqla tam kolleksiya idarəetməsinə icazə vermək üçün \"Kolleksiyanı idarə etmə\" icazələrini verin."
},
"grantCollectionAccess": {
"message": "Qrup və ya üzvlərin bu kolleksiyaya müraciətinə icazə verin."
@@ -10091,6 +10097,15 @@
"descriptorCode": {
"message": "Açıqlayıcı kod"
},
"cannotRemoveViewOnlyCollections": {
"message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"importantNotice": {
"message": "Vacib bildiriş"
},
@@ -10281,5 +10296,54 @@
"example": "Acme c"
}
}
},
"accountDeprovisioningNotification": {
"message": "İnzibatçılar, artıq götürülmüş domenlərə aid üzv hesablarını silmə imkanına sahibdir."
},
"deleteManagedUserWarningDesc": {
"message": "Bu əməliyyat, seyfdəki bütün elementlər daxil olmaqla üzv hesabını siləcək. Bu, əvvəlki Sil əməliyyatını əvəz edir."
},
"deleteManagedUserWarning": {
"message": "Silmək, yeni bir əməliyyatdır!"
},
"seatsRemaining": {
"message": "Bu təşkilata təyin edilmiş $TOTAL$ yerdən $REMAINING$ yeriniz qalıb. Abunəliyinizi idarə etmək üçün provayderinizlə əlaqə saxlayın.",
"placeholders": {
"remaining": {
"content": "$1",
"example": "5"
},
"total": {
"content": "$2",
"example": "10"
}
}
},
"existingOrganization": {
"message": "Mövcud təşkilat"
},
"selectOrganizationProviderPortal": {
"message": "Provayder Portalınıza əlavə ediləcək təşkilatı seçin."
},
"noOrganizations": {
"message": "Sadalanacaq heç bir təşkilat yoxdur."
},
"yourProviderSubscriptionCredit": {
"message": "Provayder abunəliyiniz, təşkilatınızın abunəliyində qalan vaxt üçün bir kredit alacaq."
},
"doYouWantToAddThisOrg": {
"message": "Bu təşkilatı bura əlavə etmək istəyirsiniz: $PROVIDER$?",
"placeholders": {
"provider": {
"content": "$1",
"example": "Cool MSP"
}
}
},
"addedExistingOrganization": {
"message": "Mövcud təşkilat əlavə edildi"
},
"assignedExceedsAvailable": {
"message": "Təyin edilmiş yer sayı, boş yer sayından çoxdur."
}
}

View File

@@ -1,24 +1,24 @@
{
"allApplications": {
"message": "All applications"
"message": "Усе праграмы"
},
"criticalApplications": {
"message": "Critical applications"
"message": "Крытычныя праграмы"
},
"accessIntelligence": {
"message": "Access Intelligence"
"message": "Кіраванне доступам"
},
"riskInsights": {
"message": "Risk Insights"
"message": "Разуменне рызык"
},
"passwordRisk": {
"message": "Password Risk"
"message": "Рызыка пароля"
},
"reviewAtRiskPasswords": {
"message": "Review at-risk passwords (weak, exposed, or reused) across applications. Select your most critical applications to prioritize security actions for your users to address at-risk passwords."
"message": "Праглядайце паролі, якія знаходзяцца ў зоне рызыкі (ненадзейныя, скампраметаваныя або паўторна выкарыстаныя) ва ўсіх вашых праграмах. Выберыце найбольш крытычныя праграмы для вызначэння прыярытэту бяспекі дзеянняў для вашых карыстальнікаў, якія выкарыстоўваюць рызыкоўныя паролі."
},
"dataLastUpdated": {
"message": "Data last updated: $DATE$",
"message": "Апошняе абнаўленне даных: $DATE$",
"placeholders": {
"date": {
"content": "$1",
@@ -27,19 +27,19 @@
}
},
"notifiedMembers": {
"message": "Notified members"
"message": "Апавешчаныя ўдзельнікі"
},
"revokeMembers": {
"message": "Revoke members"
"message": "Адклікаць удзельнікаў"
},
"restoreMembers": {
"message": "Restore members"
"message": "Аднавіць удзельнікаў"
},
"cannotRestoreAccessError": {
"message": "Cannot restore organization access"
"message": "Немагчыма аднавіць доступ да арганізацыі"
},
"allApplicationsWithCount": {
"message": "All applications ($COUNT$)",
"message": "Усе праграмы ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
@@ -48,10 +48,10 @@
}
},
"createNewLoginItem": {
"message": "Create new login item"
"message": "Стварыць новы элемент запісу ўваходу"
},
"criticalApplicationsWithCount": {
"message": "Critical applications ($COUNT$)",
"message": "Крытычныя праграмы ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
@@ -60,7 +60,7 @@
}
},
"notifiedMembersWithCount": {
"message": "Notified members ($COUNT$)",
"message": "Апавешчаныя ўдзельнікі ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
@@ -69,7 +69,7 @@
}
},
"noAppsInOrgTitle": {
"message": "No applications found in $ORG NAME$",
"message": "Праграмы ў $ORG NAME$ не знойдзены",
"placeholders": {
"org name": {
"content": "$1",
@@ -78,43 +78,43 @@
}
},
"noAppsInOrgDescription": {
"message": "As users save logins, applications appear here, showing any at-risk passwords. Mark critical apps and notify users to update passwords."
"message": "Тут будуць адлюстроўвацца праграмы па меры таго, як карыстальнікі будуць захоўваць запісы ўваходу, якія знаходзяцца ў зоне рызыкі. Пазначце крытычныя праграмы і апавяшчайце карыстальнікаў аб неабходнасці абнавіць паролі."
},
"noCriticalAppsTitle": {
"message": "You haven't marked any applications as a Critical"
"message": "Вы не пазначылі ніводную праграму ў якасці кратычнай"
},
"noCriticalAppsDescription": {
"message": "Select your most critical applications to discover at-risk passwords, and notify users to change those passwords."
"message": "Выберыце найбольш крытычныя праграмы для выяўлення пароляў, якія знаходзяцца ў зоне рызыкі. Апавяшчайце карыстальнікаў аб неабходнасці змяніць іх."
},
"markCriticalApps": {
"message": "Mark critical apps"
"message": "Пазначыць крытычныя праграмы"
},
"markAppAsCritical": {
"message": "Mark app as critical"
"message": "Пазначыць праграму як крытычную"
},
"appsMarkedAsCritical": {
"message": "Apps marked as critical"
"message": "Праграмы пазначаныя як крытычныя"
},
"application": {
"message": "Application"
"message": "Праграма"
},
"atRiskPasswords": {
"message": "At-risk passwords"
"message": "Паролі ў зоне рызыкі"
},
"requestPasswordChange": {
"message": "Request password change"
"message": "Запытаць змену пароля"
},
"totalPasswords": {
"message": "Total passwords"
"message": "Усяго пароляў"
},
"searchApps": {
"message": "Search applications"
"message": "Пошук праграм"
},
"atRiskMembers": {
"message": "At-risk members"
"message": "Удзельнікі ў зоне рызыкі"
},
"atRiskMembersWithCount": {
"message": "At-risk members ($COUNT$)",
"message": "Удзельнікі ў зоне рызыкі ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
@@ -123,7 +123,7 @@
}
},
"atRiskApplicationsWithCount": {
"message": "At-risk applications ($COUNT$)",
"message": "Праграмы ў зоне рызыкі ($COUNT$)",
"placeholders": {
"count": {
"content": "$1",
@@ -132,13 +132,13 @@
}
},
"atRiskMembersDescription": {
"message": "These members are logging into applications with weak, exposed, or reused passwords."
"message": "Гэтыя ўдзельнікі ўваходзяць у праграму з ненадзейнымі, скампраметаванымі або паўторна выкарыстанымі паролямі."
},
"atRiskApplicationsDescription": {
"message": "These applications have weak, exposed, or reused passwords."
"message": "Гэтыя праграмы маюць ненадзейныя, скампраметаваныя або паўторна выкарыстаныя паролі."
},
"atRiskMembersDescriptionWithApp": {
"message": "These members are logging into $APPNAME$ with weak, exposed, or reused passwords.",
"message": "Гэтыя ўдзельнікі ўваходзяць у праграму $APPNAME$ з ненадзейнымі, скампраметаванымі або паўторна выкарыстанымі паролямі.",
"placeholders": {
"appname": {
"content": "$1",
@@ -147,19 +147,19 @@
}
},
"totalMembers": {
"message": "Total members"
"message": "Усяго ўдзельнікаў"
},
"atRiskApplications": {
"message": "At-risk applications"
"message": "Праграмы ў зоне рызыкі"
},
"totalApplications": {
"message": "Total applications"
"message": "Усяго праграм"
},
"unmarkAsCriticalApp": {
"message": "Unmark as critical app"
"message": "Зняць пазнаку крытычнай праграмы"
},
"criticalApplicationSuccessfullyUnmarked": {
"message": "Critical application successfully unmarked"
"message": "Пазнака з крытычнай праграмы паспяхова знята"
},
"whatTypeOfItem": {
"message": "Які гэта элемент запісу?"
@@ -260,10 +260,10 @@
"message": "Дадаць вэб-сайт"
},
"deleteWebsite": {
"message": "Delete website"
"message": "Выдаліць вэб-сайт"
},
"defaultLabel": {
"message": "Default ($VALUE$)",
"message": "Прадвызначана ($VALUE$)",
"description": "A label that indicates the default value for a field with the current default value in parentheses.",
"placeholders": {
"value": {
@@ -273,7 +273,7 @@
}
},
"showMatchDetection": {
"message": "Show match detection $WEBSITE$",
"message": "Паказаць выяўленне супадзенняў $WEBSITE$",
"placeholders": {
"website": {
"content": "$1",
@@ -282,7 +282,7 @@
}
},
"hideMatchDetection": {
"message": "Hide match detection $WEBSITE$",
"message": "Схаваць выяўленне супадзенняў $WEBSITE$",
"placeholders": {
"website": {
"content": "$1",
@@ -291,7 +291,7 @@
}
},
"autoFillOnPageLoad": {
"message": "Autofill on page load?"
"message": "Аўтазапаўняць пры загрузцы старонкі?"
},
"number": {
"message": "Нумар"
@@ -306,7 +306,7 @@
"message": "Код бяспекі (CVV)"
},
"securityCodeSlashCVV": {
"message": "Security code / CVV"
"message": "Код бяспекі (CVV)"
},
"identityName": {
"message": "Імя пасведчання"
@@ -384,10 +384,10 @@
"message": "Доктар"
},
"cardExpiredTitle": {
"message": "Expired card"
"message": "Картка пратэрмінавана"
},
"cardExpiredMessage": {
"message": "If you've renewed it, update the card's information"
"message": "Калі вы падаўжалі картку, то абнавіце яе звесткі"
},
"expirationMonth": {
"message": "Месяц завяршэння"
@@ -399,7 +399,7 @@
"message": "Ключ аўтэнтыфікацыі (TOTP)"
},
"totpHelperTitle": {
"message": "Make 2-step verification seamless"
"message": "Спрасціце двухэтапную праверку"
},
"totpHelper": {
"message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field."
@@ -464,6 +464,18 @@
"editFolder": {
"message": "Рэдагаваць папку"
},
"newFolder": {
"message": "New folder"
},
"folderName": {
"message": "Folder name"
},
"folderHintText": {
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
},
"deleteFolderPermanently": {
"message": "Are you sure you want to permanently delete this folder?"
},
"baseDomain": {
"message": "Асноўны дамен",
"description": "Domain name. Example: website.com"
@@ -728,15 +740,6 @@
"itemName": {
"message": "Item name"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"ex": {
"message": "напр.",
"description": "Short abbreviation for 'example'."
@@ -1182,6 +1185,9 @@
"logInInitiated": {
"message": "Ініцыяваны ўваход"
},
"logInRequestSent": {
"message": "Request sent"
},
"submit": {
"message": "Адправіць"
},
@@ -1371,12 +1377,39 @@
"notificationSentDevice": {
"message": "Апавяшчэнне было адпраўлена на вашу прыладу."
},
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the "
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Confirm access"
},
"denyAccess": {
"message": "Deny access"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"notificationSentDeviceComplete": {
"message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"versionNumber": {
"message": "Версія $VERSION_NUMBER$",
"placeholders": {
@@ -1664,9 +1697,6 @@
"message": "Avoid ambiguous characters",
"description": "Label for the avoid ambiguous characters checkbox."
},
"regeneratePassword": {
"message": "Паўторна генерыраваць пароль"
},
"length": {
"message": "Даўжыня"
},
@@ -2170,8 +2200,20 @@
"manage": {
"message": "Кіраванне"
},
"canManage": {
"message": "Can manage"
"manageCollection": {
"message": "Manage collection"
},
"viewItems": {
"message": "View items"
},
"viewItemsHidePass": {
"message": "View items, hidden passwords"
},
"editItems": {
"message": "Edit items"
},
"editItemsHidePass": {
"message": "Edit items, hidden passwords"
},
"disable": {
"message": "Адключыць"
@@ -2368,11 +2410,8 @@
"twoFactorU2fProblemReadingTryAgain": {
"message": "Праблема чытання ключа бяспекі. Паспрабуйце яшчэ раз."
},
"twoFactorWebAuthnWarning": {
"message": "У сувязі з абмежаваннямі платформы, WebAuthn немагчыма выкарыстоўваць ва ўсіх праграмах Bitwarden. Вам неабходна актываваць іншага пастаўшчыка двухэтапнага ўваходу, каб вы маглі атрымаць доступ да свайго ўліковага запісу, калі немагчыма скарыстацца WebAuthn. Платформы, які падтрымліваюцца:"
},
"twoFactorWebAuthnSupportWeb": {
"message": "Вэб-сховішча і пашырэнні браўзера на камп'ютары/ноўтбуку з браўзерам, які падтрымлівае WebAuthn (Chrome, Opera, Vivaldi або Firefox з уключаным FIDO U2F)."
"twoFactorWebAuthnWarning1": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used."
},
"twoFactorRecoveryYourCode": {
"message": "Ваш код аднаўлення двухэтапнага ўваходу Bitwarden"
@@ -4710,9 +4749,6 @@
"passwordGeneratorPolicyDesc": {
"message": "Прызначыць патрабаванні для генератара пароляў."
},
"passwordGeneratorPolicyInEffect": {
"message": "Адна або больш палітык арганізацыі ўплывае на налады генератара."
},
"masterPasswordPolicyInEffect": {
"message": "Адна або больш палітык арганізацыі патрабуе, каб ваш асноўны пароль адпавядаў наступным патрабаванням:"
},
@@ -6681,15 +6717,6 @@
"message": "Генератар",
"description": "Short for 'credential generator'."
},
"whatWouldYouLikeToGenerate": {
"message": "Што вы хочаце генерыраваць?"
},
"passwordType": {
"message": "Тып пароля"
},
"regenerateUsername": {
"message": "Паўторна генерыраваць імя карыстальніка"
},
"generateUsername": {
"message": "Генерыраваць імя карыстальніка"
},
@@ -6730,9 +6757,6 @@
}
}
},
"usernameType": {
"message": "Тып імя карыстальніка"
},
"plusAddressedEmail": {
"message": "Адрасы электроннай пошты з плюсам",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -6952,9 +6976,6 @@
"message": "Назва вузла",
"description": "Part of a URL."
},
"apiAccessToken": {
"message": "Токен доступу да API"
},
"deviceVerification": {
"message": "Праверка прылады"
},
@@ -7666,18 +7687,6 @@
"noCollection": {
"message": "Няма калекцый"
},
"canView": {
"message": "Можа праглядаць"
},
"canViewExceptPass": {
"message": "Можа праглядаць (без пароляў)"
},
"canEdit": {
"message": "Можа рэдагаваць"
},
"canEditExceptPass": {
"message": "Можа рэдагаваць (без пароляў)"
},
"noCollectionsAdded": {
"message": "Няма дадзеных калекцый"
},
@@ -8677,9 +8686,6 @@
"message": "Self-host server URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"aliasDomain": {
"message": "Мянушка дамена"
},
"alreadyHaveAccount": {
"message": "Ужо маеце ўліковы запіс?"
},
@@ -8741,11 +8747,11 @@
"readOnlyCollectionAccess": {
"message": "You do not have access to manage this collection."
},
"grantAddAccessCollectionWarningTitle": {
"message": "Missing Can Manage Permissions"
"grantManageCollectionWarningTitle": {
"message": "Missing Manage Collection Permissions"
},
"grantAddAccessCollectionWarning": {
"message": "Grant Can manage permissions to allow full collection management including deletion of collection."
"grantManageCollectionWarning": {
"message": "Grant Manage collection permissions to allow full collection management including deletion of collection."
},
"grantCollectionAccess": {
"message": "Grant groups or members access to this collection."
@@ -10091,6 +10097,15 @@
"descriptorCode": {
"message": "Descriptor code"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"importantNotice": {
"message": "Important notice"
},
@@ -10281,5 +10296,54 @@
"example": "Acme c"
}
}
},
"accountDeprovisioningNotification": {
"message": "Administrators now have the ability to delete member accounts that belong to a claimed domain."
},
"deleteManagedUserWarningDesc": {
"message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action."
},
"deleteManagedUserWarning": {
"message": "Delete is a new action!"
},
"seatsRemaining": {
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
"placeholders": {
"remaining": {
"content": "$1",
"example": "5"
},
"total": {
"content": "$2",
"example": "10"
}
}
},
"existingOrganization": {
"message": "Existing organization"
},
"selectOrganizationProviderPortal": {
"message": "Select an organization to add to your Provider Portal."
},
"noOrganizations": {
"message": "There are no organizations to list"
},
"yourProviderSubscriptionCredit": {
"message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription."
},
"doYouWantToAddThisOrg": {
"message": "Do you want to add this organization to $PROVIDER$?",
"placeholders": {
"provider": {
"content": "$1",
"example": "Cool MSP"
}
}
},
"addedExistingOrganization": {
"message": "Added existing organization"
},
"assignedExceedsAvailable": {
"message": "Assigned seats exceed available seats."
}
}

View File

@@ -464,6 +464,18 @@
"editFolder": {
"message": "Редактиране на папка"
},
"newFolder": {
"message": "Нова папка"
},
"folderName": {
"message": "Име на папката"
},
"folderHintText": {
"message": "Можете да вложите една папка в друга като въведете името на горната папка, а след това „/“. Пример: Социални/Форуми"
},
"deleteFolderPermanently": {
"message": "Наистина ли искате да изтриете тази папка окончателно?"
},
"baseDomain": {
"message": "Основен домейн",
"description": "Domain name. Example: website.com"
@@ -728,15 +740,6 @@
"itemName": {
"message": "Име на елемента"
},
"cannotRemoveViewOnlyCollections": {
"message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"ex": {
"message": "напр.",
"description": "Short abbreviation for 'example'."
@@ -1182,6 +1185,9 @@
"logInInitiated": {
"message": "Вписването е стартирано"
},
"logInRequestSent": {
"message": "Заявката е изпратена"
},
"submit": {
"message": "Подаване"
},
@@ -1371,12 +1377,39 @@
"notificationSentDevice": {
"message": "Към устройството Ви е изпратено известие."
},
"notificationSentDevicePart1": {
"message": "Отключете Битоурден на устройството си или в "
},
"areYouTryingToAccessYourAccount": {
"message": "Опитвате ли се да получите достъп до акаунта си?"
},
"accessAttemptBy": {
"message": "Опит за достъп от $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Разрешаване на достъпа"
},
"denyAccess": {
"message": "Отказване на достъпа"
},
"notificationSentDeviceAnchor": {
"message": "приложението по уеб"
},
"notificationSentDevicePart2": {
"message": "Уверете се, че уникалната фраза съвпада с тази по-долу, преди да одобрите."
},
"notificationSentDeviceComplete": {
"message": "Отключете Битоурден на устройството си. Уверете се, че уникалната фраза съвпада с тази по-долу, преди да одобрите."
},
"aNotificationWasSentToYourDevice": {
"message": "Към устройството Ви е изпратено известие"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Уверете се, че регистрацията Ви е отключена и че уникалната фраза съвпада с другото устройство"
},
"versionNumber": {
"message": "Версия $VERSION_NUMBER$",
"placeholders": {
@@ -1664,9 +1697,6 @@
"message": "Без нееднозначни знаци",
"description": "Label for the avoid ambiguous characters checkbox."
},
"regeneratePassword": {
"message": "Друга парола"
},
"length": {
"message": "Дължина"
},
@@ -2170,8 +2200,20 @@
"manage": {
"message": "Управление"
},
"canManage": {
"message": "Може да управлява"
"manageCollection": {
"message": "Управление на колекцията"
},
"viewItems": {
"message": "Преглед на елементите"
},
"viewItemsHidePass": {
"message": "Преглед на елементите, със скрити пароли"
},
"editItems": {
"message": "Редактиране на елементите"
},
"editItemsHidePass": {
"message": "Редактиране на елементите, със скрити пароли"
},
"disable": {
"message": "Изключване"
@@ -2368,11 +2410,8 @@
"twoFactorU2fProblemReadingTryAgain": {
"message": "Проблем при изчитането на ключа за сигурност. Пробвайте отново."
},
"twoFactorWebAuthnWarning": {
"message": "Поради платформени ограничения устройствата на WebAuthn не могат да се използват с всички приложения на Битуорден. В такъв случай ще трябва да добавите друг доставчик на двустепенно удостоверяване, за да имате достъп до абонамента си, дори когато WebAuthn не работи. Поддържани платформи:"
},
"twoFactorWebAuthnSupportWeb": {
"message": "Трезорът по уеб както и разширенията за браузърите с поддръжка на WebAuthn (Chrome, Opera, Vivaldi и Firefox с поддръжка на FIDO U2F)."
"twoFactorWebAuthnWarning1": {
"message": "Поради платформени ограничения устройствата на WebAuthn не могат да се използват с всички приложения на Битуорден. Ще трябва да настроите друг доставчик на двустепенно удостоверяване, за да имате достъп до акаунта си, когато WebAuthn не може да се ползва."
},
"twoFactorRecoveryYourCode": {
"message": "Код за възстановяване на достъпа до Битуорден при двустепенна идентификация"
@@ -4710,9 +4749,6 @@
"passwordGeneratorPolicyDesc": {
"message": "Задаване на минимална сила на генератора на пароли."
},
"passwordGeneratorPolicyInEffect": {
"message": "Поне една политика на организация влияе на настройките на генерирането на паролите."
},
"masterPasswordPolicyInEffect": {
"message": "Поне една политика на организация има следните изисквания към главната ви парола:"
},
@@ -6681,15 +6717,6 @@
"message": "Генератор",
"description": "Short for 'credential generator'."
},
"whatWouldYouLikeToGenerate": {
"message": "Какво бихте искали да генерирате?"
},
"passwordType": {
"message": "Тип парола"
},
"regenerateUsername": {
"message": "Повторно генериране на потр. име"
},
"generateUsername": {
"message": "Генериране на потр. име"
},
@@ -6730,9 +6757,6 @@
}
}
},
"usernameType": {
"message": "Тип потребителско име"
},
"plusAddressedEmail": {
"message": "Адрес на е-поща с плюс",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -6952,9 +6976,6 @@
"message": "Име на сървъра",
"description": "Part of a URL."
},
"apiAccessToken": {
"message": "Идентификатор за достъп до API"
},
"deviceVerification": {
"message": "Потвърждаване на устройствата"
},
@@ -7666,18 +7687,6 @@
"noCollection": {
"message": "Няма колекция"
},
"canView": {
"message": "Може да преглежда"
},
"canViewExceptPass": {
"message": "Може да преглежда, без пароли"
},
"canEdit": {
"message": "Може да редактира"
},
"canEditExceptPass": {
"message": "Може да редактира, без пароли"
},
"noCollectionsAdded": {
"message": "Няма добавени колекции"
},
@@ -8626,7 +8635,7 @@
"message": "Ограничаване на изтриването на колекции, така че да може да се извършва само от собствениците и администраторите"
},
"limitItemDeletionDesc": {
"message": "Limit item deletion to members with the Can manage permission"
"message": "Ограничаване на изтриването на елементи, така че да може да се извършва само от членове с правомощие за управление"
},
"allowAdminAccessToAllCollectionItemsDesc": {
"message": "Собствениците и администраторите могат да управляват всички колекции и елементи"
@@ -8677,9 +8686,6 @@
"message": "Адрес на собствения сървър",
"description": "Label for field requesting a self-hosted integration service URL"
},
"aliasDomain": {
"message": "Псевдонимен домейн"
},
"alreadyHaveAccount": {
"message": "Вече имате регистрация?"
},
@@ -8741,11 +8747,11 @@
"readOnlyCollectionAccess": {
"message": "Нямате достъп за управление на тази колекция."
},
"grantAddAccessCollectionWarningTitle": {
"message": "Липсват правомощия за управление"
"grantManageCollectionWarningTitle": {
"message": "Липсват правомощия за управление на колекции"
},
"grantAddAccessCollectionWarning": {
"message": "Дайте правомощия за управление, за да позволите пълното управление на колекции, включително изтриването им."
"grantManageCollectionWarning": {
"message": "Дайте правомощия за управление на колекциите, за да позволите пълното управление на колекции, включително изтриването им."
},
"grantCollectionAccess": {
"message": "Дайте права на групи и членове до тази колекция."
@@ -10091,6 +10097,15 @@
"descriptorCode": {
"message": "Код от описанието"
},
"cannotRemoveViewOnlyCollections": {
"message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"importantNotice": {
"message": "Важно съобщение"
},
@@ -10281,5 +10296,54 @@
"example": "Acme c"
}
}
},
"accountDeprovisioningNotification": {
"message": "Администраторите вече имат възможността да изтриват акаунтите на членовете, които принадлежат към присвоен домейн."
},
"deleteManagedUserWarningDesc": {
"message": "Това действие ще изтрие акаунта на члена, включително всички елементи в неговия трезор. Това заменя предишното действие за Премахване."
},
"deleteManagedUserWarning": {
"message": "Изтриването е ново действие!"
},
"seatsRemaining": {
"message": "Остават Ви $REMAINING$ от общо $TOTAL$ места в тази организация. Свържете се с доставчика си, ако искате да управлявате абонамента си.",
"placeholders": {
"remaining": {
"content": "$1",
"example": "5"
},
"total": {
"content": "$2",
"example": "10"
}
}
},
"existingOrganization": {
"message": "Съществуваща организация"
},
"selectOrganizationProviderPortal": {
"message": "Изберете организацията, която искате да добавите към своя Портал за доставчици."
},
"noOrganizations": {
"message": "Няма организации за показване"
},
"yourProviderSubscriptionCredit": {
"message": "Вашият абонамент за доставчик ще получи кредит за оставащото време в абонамента на организацията, ако има такова."
},
"doYouWantToAddThisOrg": {
"message": "Искате ли да добавите тази организация към $PROVIDER$?",
"placeholders": {
"provider": {
"content": "$1",
"example": "Cool MSP"
}
}
},
"addedExistingOrganization": {
"message": "Добавена е съществуваща организация"
},
"assignedExceedsAvailable": {
"message": "Назначените места превишават наличния брой."
}
}

View File

@@ -464,6 +464,18 @@
"editFolder": {
"message": "ফোল্ডার সম্পাদনা"
},
"newFolder": {
"message": "New folder"
},
"folderName": {
"message": "Folder name"
},
"folderHintText": {
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
},
"deleteFolderPermanently": {
"message": "Are you sure you want to permanently delete this folder?"
},
"baseDomain": {
"message": "ভিত্তি ডোমেইন",
"description": "Domain name. Example: website.com"
@@ -728,15 +740,6 @@
"itemName": {
"message": "Item name"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"ex": {
"message": "উদাহরণ",
"description": "Short abbreviation for 'example'."
@@ -1182,6 +1185,9 @@
"logInInitiated": {
"message": "Log in initiated"
},
"logInRequestSent": {
"message": "Request sent"
},
"submit": {
"message": "Submit"
},
@@ -1371,12 +1377,39 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the "
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Confirm access"
},
"denyAccess": {
"message": "Deny access"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"notificationSentDeviceComplete": {
"message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"versionNumber": {
"message": "সংস্করণ $VERSION_NUMBER$",
"placeholders": {
@@ -1664,9 +1697,6 @@
"message": "Avoid ambiguous characters",
"description": "Label for the avoid ambiguous characters checkbox."
},
"regeneratePassword": {
"message": "Regenerate password"
},
"length": {
"message": "Length"
},
@@ -2170,8 +2200,20 @@
"manage": {
"message": "Manage"
},
"canManage": {
"message": "Can manage"
"manageCollection": {
"message": "Manage collection"
},
"viewItems": {
"message": "View items"
},
"viewItemsHidePass": {
"message": "View items, hidden passwords"
},
"editItems": {
"message": "Edit items"
},
"editItemsHidePass": {
"message": "Edit items, hidden passwords"
},
"disable": {
"message": "Turn off"
@@ -2368,11 +2410,8 @@
"twoFactorU2fProblemReadingTryAgain": {
"message": "There was a problem reading the security key. Try again."
},
"twoFactorWebAuthnWarning": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:"
},
"twoFactorWebAuthnSupportWeb": {
"message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)."
"twoFactorWebAuthnWarning1": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used."
},
"twoFactorRecoveryYourCode": {
"message": "Your Bitwarden two-step login recovery code"
@@ -4710,9 +4749,6 @@
"passwordGeneratorPolicyDesc": {
"message": "পাসওয়ার্ড উৎপাদকের কনফিগারেশনের জন্য ন্যূনতম প্রয়োজনীয়তা সেট করুন।"
},
"passwordGeneratorPolicyInEffect": {
"message": "এক বা একাধিক সংস্থার নীতিগুলি আপনার উৎপাদকের সেটিংসকে প্রভাবিত করছে।"
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
},
@@ -6681,15 +6717,6 @@
"message": "Generator",
"description": "Short for 'credential generator'."
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
},
"passwordType": {
"message": "Password type"
},
"regenerateUsername": {
"message": "Regenerate username"
},
"generateUsername": {
"message": "Generate username"
},
@@ -6730,9 +6757,6 @@
}
}
},
"usernameType": {
"message": "Username type"
},
"plusAddressedEmail": {
"message": "Plus addressed email",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -6952,9 +6976,6 @@
"message": "Hostname",
"description": "Part of a URL."
},
"apiAccessToken": {
"message": "API access token"
},
"deviceVerification": {
"message": "Device verification"
},
@@ -7666,18 +7687,6 @@
"noCollection": {
"message": "No collection"
},
"canView": {
"message": "Can view"
},
"canViewExceptPass": {
"message": "Can view, except passwords"
},
"canEdit": {
"message": "Can edit"
},
"canEditExceptPass": {
"message": "Can edit, except passwords"
},
"noCollectionsAdded": {
"message": "No collections added"
},
@@ -8677,9 +8686,6 @@
"message": "Self-host server URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"aliasDomain": {
"message": "Alias domain"
},
"alreadyHaveAccount": {
"message": "Already have an account?"
},
@@ -8741,11 +8747,11 @@
"readOnlyCollectionAccess": {
"message": "You do not have access to manage this collection."
},
"grantAddAccessCollectionWarningTitle": {
"message": "Missing Can Manage Permissions"
"grantManageCollectionWarningTitle": {
"message": "Missing Manage Collection Permissions"
},
"grantAddAccessCollectionWarning": {
"message": "Grant Can manage permissions to allow full collection management including deletion of collection."
"grantManageCollectionWarning": {
"message": "Grant Manage collection permissions to allow full collection management including deletion of collection."
},
"grantCollectionAccess": {
"message": "Grant groups or members access to this collection."
@@ -10091,6 +10097,15 @@
"descriptorCode": {
"message": "Descriptor code"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"importantNotice": {
"message": "Important notice"
},
@@ -10281,5 +10296,54 @@
"example": "Acme c"
}
}
},
"accountDeprovisioningNotification": {
"message": "Administrators now have the ability to delete member accounts that belong to a claimed domain."
},
"deleteManagedUserWarningDesc": {
"message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action."
},
"deleteManagedUserWarning": {
"message": "Delete is a new action!"
},
"seatsRemaining": {
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
"placeholders": {
"remaining": {
"content": "$1",
"example": "5"
},
"total": {
"content": "$2",
"example": "10"
}
}
},
"existingOrganization": {
"message": "Existing organization"
},
"selectOrganizationProviderPortal": {
"message": "Select an organization to add to your Provider Portal."
},
"noOrganizations": {
"message": "There are no organizations to list"
},
"yourProviderSubscriptionCredit": {
"message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription."
},
"doYouWantToAddThisOrg": {
"message": "Do you want to add this organization to $PROVIDER$?",
"placeholders": {
"provider": {
"content": "$1",
"example": "Cool MSP"
}
}
},
"addedExistingOrganization": {
"message": "Added existing organization"
},
"assignedExceedsAvailable": {
"message": "Assigned seats exceed available seats."
}
}

View File

@@ -464,6 +464,18 @@
"editFolder": {
"message": "Uredite folder"
},
"newFolder": {
"message": "New folder"
},
"folderName": {
"message": "Folder name"
},
"folderHintText": {
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
},
"deleteFolderPermanently": {
"message": "Are you sure you want to permanently delete this folder?"
},
"baseDomain": {
"message": "Osnovni domen",
"description": "Domain name. Example: website.com"
@@ -728,15 +740,6 @@
"itemName": {
"message": "Item name"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"ex": {
"message": "npr.",
"description": "Short abbreviation for 'example'."
@@ -1182,6 +1185,9 @@
"logInInitiated": {
"message": "Log in initiated"
},
"logInRequestSent": {
"message": "Request sent"
},
"submit": {
"message": "Submit"
},
@@ -1371,12 +1377,39 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
"notificationSentDevicePart1": {
"message": "Unlock Bitwarden on your device or on the "
},
"areYouTryingToAccessYourAccount": {
"message": "Are you trying to access your account?"
},
"accessAttemptBy": {
"message": "Access attempt by $EMAIL$",
"placeholders": {
"email": {
"content": "$1",
"example": "name@example.com"
}
}
},
"confirmAccess": {
"message": "Confirm access"
},
"denyAccess": {
"message": "Deny access"
},
"notificationSentDeviceAnchor": {
"message": "web app"
},
"notificationSentDevicePart2": {
"message": "Make sure the Fingerprint phrase matches the one below before approving."
},
"notificationSentDeviceComplete": {
"message": "Unlock Bitwarden on your device. Make sure the Fingerprint phrase matches the one below before approving."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
},
"versionNumber": {
"message": "Verzija $VERSION_NUMBER$",
"placeholders": {
@@ -1664,9 +1697,6 @@
"message": "Avoid ambiguous characters",
"description": "Label for the avoid ambiguous characters checkbox."
},
"regeneratePassword": {
"message": "Regenerate password"
},
"length": {
"message": "Length"
},
@@ -2170,8 +2200,20 @@
"manage": {
"message": "Manage"
},
"canManage": {
"message": "Can manage"
"manageCollection": {
"message": "Manage collection"
},
"viewItems": {
"message": "View items"
},
"viewItemsHidePass": {
"message": "View items, hidden passwords"
},
"editItems": {
"message": "Edit items"
},
"editItemsHidePass": {
"message": "Edit items, hidden passwords"
},
"disable": {
"message": "Turn off"
@@ -2368,11 +2410,8 @@
"twoFactorU2fProblemReadingTryAgain": {
"message": "There was a problem reading the security key. Try again."
},
"twoFactorWebAuthnWarning": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used. Supported platforms:"
},
"twoFactorWebAuthnSupportWeb": {
"message": "Web vault and browser extensions on a desktop/laptop with a WebAuthn supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)."
"twoFactorWebAuthnWarning1": {
"message": "Due to platform limitations, WebAuthn cannot be used on all Bitwarden applications. You should set up another two-step login provider so that you can access your account when WebAuthn cannot be used."
},
"twoFactorRecoveryYourCode": {
"message": "Your Bitwarden two-step login recovery code"
@@ -4710,9 +4749,6 @@
"passwordGeneratorPolicyDesc": {
"message": "Set requirements for password generator."
},
"passwordGeneratorPolicyInEffect": {
"message": "One or more organization policies are affecting your generator settings."
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
},
@@ -6681,15 +6717,6 @@
"message": "Generator",
"description": "Short for 'credential generator'."
},
"whatWouldYouLikeToGenerate": {
"message": "What would you like to generate?"
},
"passwordType": {
"message": "Password type"
},
"regenerateUsername": {
"message": "Regenerate username"
},
"generateUsername": {
"message": "Generate username"
},
@@ -6730,9 +6757,6 @@
}
}
},
"usernameType": {
"message": "Username type"
},
"plusAddressedEmail": {
"message": "Plus addressed email",
"description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com"
@@ -6952,9 +6976,6 @@
"message": "Hostname",
"description": "Part of a URL."
},
"apiAccessToken": {
"message": "API access token"
},
"deviceVerification": {
"message": "Device verification"
},
@@ -7666,18 +7687,6 @@
"noCollection": {
"message": "No collection"
},
"canView": {
"message": "Can view"
},
"canViewExceptPass": {
"message": "Can view, except passwords"
},
"canEdit": {
"message": "Can edit"
},
"canEditExceptPass": {
"message": "Can edit, except passwords"
},
"noCollectionsAdded": {
"message": "No collections added"
},
@@ -8677,9 +8686,6 @@
"message": "Self-host server URL",
"description": "Label for field requesting a self-hosted integration service URL"
},
"aliasDomain": {
"message": "Alias domain"
},
"alreadyHaveAccount": {
"message": "Already have an account?"
},
@@ -8741,11 +8747,11 @@
"readOnlyCollectionAccess": {
"message": "You do not have access to manage this collection."
},
"grantAddAccessCollectionWarningTitle": {
"message": "Missing Can Manage Permissions"
"grantManageCollectionWarningTitle": {
"message": "Missing Manage Collection Permissions"
},
"grantAddAccessCollectionWarning": {
"message": "Grant Can manage permissions to allow full collection management including deletion of collection."
"grantManageCollectionWarning": {
"message": "Grant Manage collection permissions to allow full collection management including deletion of collection."
},
"grantCollectionAccess": {
"message": "Grant groups or members access to this collection."
@@ -10091,6 +10097,15 @@
"descriptorCode": {
"message": "Descriptor code"
},
"cannotRemoveViewOnlyCollections": {
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
"placeholders": {
"collections": {
"content": "$1",
"example": "Work, Personal"
}
}
},
"importantNotice": {
"message": "Important notice"
},
@@ -10281,5 +10296,54 @@
"example": "Acme c"
}
}
},
"accountDeprovisioningNotification": {
"message": "Administrators now have the ability to delete member accounts that belong to a claimed domain."
},
"deleteManagedUserWarningDesc": {
"message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action."
},
"deleteManagedUserWarning": {
"message": "Delete is a new action!"
},
"seatsRemaining": {
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
"placeholders": {
"remaining": {
"content": "$1",
"example": "5"
},
"total": {
"content": "$2",
"example": "10"
}
}
},
"existingOrganization": {
"message": "Existing organization"
},
"selectOrganizationProviderPortal": {
"message": "Select an organization to add to your Provider Portal."
},
"noOrganizations": {
"message": "There are no organizations to list"
},
"yourProviderSubscriptionCredit": {
"message": "Your provider subscription will receive a credit for any remaining time in the organization's subscription."
},
"doYouWantToAddThisOrg": {
"message": "Do you want to add this organization to $PROVIDER$?",
"placeholders": {
"provider": {
"content": "$1",
"example": "Cool MSP"
}
}
},
"addedExistingOrganization": {
"message": "Added existing organization"
},
"assignedExceedsAvailable": {
"message": "Assigned seats exceed available seats."
}
}

Some files were not shown because too many files have changed in this diff Show More