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:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,3 +17,5 @@
|
||||
<ng-container *ngIf="!loading; else loadingState">
|
||||
<router-outlet></router-outlet>
|
||||
</ng-container>
|
||||
|
||||
<bit-toast-container></bit-toast-container>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
// `,
|
||||
// });
|
||||
260
apps/web/src/app/layouts/header/web-header.stories.ts
Normal file
260
apps/web/src/app/layouts/header/web-header.stories.ts
Normal 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>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
2
apps/web/src/app/tools/send/send-access/index.ts
Normal file
2
apps/web/src/app/tools/send/send-access/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AccessComponent } from "./access.component";
|
||||
export { SendAccessExplainerComponent } from "./send-access-explainer.component";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
@Component({
|
||||
selector: "app-send-access-explainer",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
{{ addInfo.text | i18n }}
|
||||
</a>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Назначените места превишават наличния брой."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user