1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 14:34:02 +00:00

Merge branch 'PS55-6-22' of https://github.com/bitwarden/clients into PS55-6-22

This commit is contained in:
CarleyDiaz-Bitwarden
2022-07-22 11:18:30 -04:00
431 changed files with 13919 additions and 5118 deletions

View File

@@ -28,7 +28,7 @@
</div>
</header>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="container">
<div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
@@ -112,156 +112,10 @@
>
{{ "createOrganizationCreatePersonalAccount" | i18n }}
</app-callout>
<div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
inputmode="email"
appInputVerbatim="false"
/>
<small class="form-text text-muted">{{ "emailAddressDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="name">{{ "yourName" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="name"
[appAutofocus]="email !== ''"
/>
<small class="form-text text-muted">{{ "yourNameDesc" | i18n }}</small>
</div>
<div class="form-group">
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<div class="w-100">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required
appInputVerbatim
/>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{
'bwi-eye': !showPassword,
'bwi-eye-slash': showPassword
}"
></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{ "masterPassDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input
id="hint"
class="form-control"
type="text"
name="Hint"
[(ngModel)]="hint"
/>
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="form-group" *ngIf="showTerms">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="acceptPolicies"
[(ngModel)]="acceptPolicies"
name="AcceptPolicies"
/>
<label class="form-check-label small text-muted" for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</label>
</div>
</div>
<hr />
<div class="d-flex mb-2">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>{{ "submit" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
<app-register-form
[queryParamEmail]="email"
[enforcedPolicyOptions]="enforcedPolicyOptions"
></app-register-form>
</div>
</div>
</div>
@@ -351,5 +205,5 @@
/>
</div>
</div>
</form>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
@@ -7,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
@@ -25,6 +27,7 @@ import { RouterService } from "../services/router.service";
templateUrl: "register.component.html",
})
export class RegisterComponent extends BaseRegisterComponent {
email = "";
showCreateOrgMessage = false;
layout = "";
enforcedPolicyOptions: MasterPasswordPolicyOptions;
@@ -32,6 +35,8 @@ export class RegisterComponent extends BaseRegisterComponent {
private policies: Policy[];
constructor(
formValidationErrorService: FormValidationErrorsService,
formBuilder: FormBuilder,
authService: AuthService,
router: Router,
i18nService: I18nService,
@@ -47,6 +52,8 @@ export class RegisterComponent extends BaseRegisterComponent {
private routerService: RouterService
) {
super(
formValidationErrorService,
formBuilder,
authService,
router,
i18nService,
@@ -126,24 +133,4 @@ export class RegisterComponent extends BaseRegisterComponent {
await super.ngOnInit();
}
async submit() {
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
this.masterPasswordScore,
this.masterPassword,
this.enforcedPolicyOptions
)
) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordPolicyRequirementsNotMet")
);
return;
}
await super.submit();
}
}

View File

@@ -4,6 +4,7 @@ import { DomSanitizer } from "@angular/platform-browser";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
import { IndividualConfig, ToastrService } from "ngx-toastr";
import { Subject, takeUntil } from "rxjs";
import Swal from "sweetalert2";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
@@ -12,7 +13,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { InternalFolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
@@ -23,7 +24,6 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service";
import { TokenService } from "@bitwarden/common/abstractions/token.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout.service";
import { DisableSendPolicy } from "./organizations/policies/disable-send.component";
@@ -49,12 +49,12 @@ export class AppComponent implements OnDestroy, OnInit {
private lastActivity: number = null;
private idleTimer: number = null;
private isIdle = false;
private destroy$ = new Subject<void>();
constructor(
@Inject(DOCUMENT) private document: Document,
private broadcasterService: BroadcasterService,
private tokenService: TokenService,
private folderService: FolderService,
private folderService: InternalFolderService,
private settingsService: SettingsService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationService,
@@ -80,7 +80,9 @@ export class AppComponent implements OnDestroy, OnInit {
) {}
ngOnInit() {
this.document.documentElement.lang = this.i18nService.locale;
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {
this.document.documentElement.lang = locale;
});
this.ngZone.runOutsideAngular(() => {
window.onmousemove = () => this.recordActivity();
@@ -183,7 +185,7 @@ export class AppComponent implements OnDestroy, OnInit {
});
});
this.router.events.subscribe((event) => {
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
if (event instanceof NavigationEnd) {
const modals = Array.from(document.querySelectorAll(".modal"));
for (const modal of modals) {
@@ -199,13 +201,13 @@ export class AppComponent implements OnDestroy, OnInit {
this.policyListService.addPolicies([
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),
new ResetPasswordPolicy(),
new PasswordGeneratorPolicy(),
new SingleOrgPolicy(),
new RequireSsoPolicy(),
new PersonalOwnershipPolicy(),
new DisableSendPolicy(),
new SendOptionsPolicy(),
new ResetPasswordPolicy(),
]);
this.setFullWidth();
@@ -213,6 +215,8 @@ export class AppComponent implements OnDestroy, OnInit {
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.destroy$.next();
this.destroy$.unsubscribe();
}
private async logOut(expired: boolean) {

View File

@@ -204,15 +204,18 @@ export abstract class BasePeopleComponent<
this.edit(null);
}
async remove(user: UserType) {
const confirmed = await this.platformUtilsService.showDialog(
this.deleteWarningMessage(user),
protected async removeUserConfirmationDialog(user: UserType) {
return this.platformUtilsService.showDialog(
this.i18nService.t("removeUserConfirmation"),
this.userNamePipe.transform(user),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
}
async remove(user: UserType) {
const confirmed = await this.removeUserConfirmationDialog(user);
if (!confirmed) {
return false;
}
@@ -235,8 +238,8 @@ export abstract class BasePeopleComponent<
async deactivate(user: UserType) {
const confirmed = await this.platformUtilsService.showDialog(
this.deactivateWarningMessage(),
this.i18nService.t("deactivateUserId", this.userNamePipe.transform(user)),
this.i18nService.t("deactivate"),
this.i18nService.t("revokeUserId", this.userNamePipe.transform(user)),
this.i18nService.t("revokeAccess"),
this.i18nService.t("cancel"),
"warning"
);
@@ -251,7 +254,7 @@ export abstract class BasePeopleComponent<
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("deactivatedUserId", this.userNamePipe.transform(user))
this.i18nService.t("revokedUserId", this.userNamePipe.transform(user))
);
await this.load();
} catch (e) {
@@ -261,25 +264,13 @@ export abstract class BasePeopleComponent<
}
async activate(user: UserType) {
const confirmed = await this.platformUtilsService.showDialog(
this.activateWarningMessage(),
this.i18nService.t("activateUserId", this.userNamePipe.transform(user)),
this.i18nService.t("activate"),
this.i18nService.t("cancel"),
"warning"
);
if (!confirmed) {
return false;
}
this.actionPromise = this.activateUser(user.id);
try {
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("activatedUserId", this.userNamePipe.transform(user))
this.i18nService.t("restoredUserId", this.userNamePipe.transform(user))
);
await this.load();
} catch (e) {
@@ -395,7 +386,7 @@ export abstract class BasePeopleComponent<
}
protected deactivateWarningMessage(): string {
return this.i18nService.t("deactivateUserConfirmation");
return this.i18nService.t("revokeUserConfirmation");
}
protected activateWarningMessage(): string {

View File

@@ -23,7 +23,6 @@ import { FilePasswordPromptComponent } from "../components/file-password-prompt.
import { NestedCheckboxComponent } from "../components/nested-checkbox.component";
import { OrganizationSwitcherComponent } from "../components/organization-switcher.component";
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
import { PasswordStrengthComponent } from "../components/password-strength.component";
import { PremiumBadgeComponent } from "../components/premium-badge.component";
import { UserVerificationPromptComponent } from "../components/user-verification-prompt.component";
import { FooterComponent } from "../layouts/footer.component";
@@ -117,7 +116,6 @@ import { EmergencyAccessComponent } from "../settings/emergency-access.component
import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component";
import { OrganizationPlansComponent } from "../settings/organization-plans.component";
import { PaymentMethodComponent } from "../settings/payment-method.component";
import { PaymentComponent } from "../settings/payment.component";
import { PreferencesComponent } from "../settings/preferences.component";
import { PremiumComponent } from "../settings/premium.component";
import { ProfileComponent } from "../settings/profile.component";
@@ -128,7 +126,6 @@ import { SettingsComponent } from "../settings/settings.component";
import { SponsoredFamiliesComponent } from "../settings/sponsored-families.component";
import { SponsoringOrgRowComponent } from "../settings/sponsoring-org-row.component";
import { SubscriptionComponent } from "../settings/subscription.component";
import { TaxInfoComponent } from "../settings/tax-info.component";
import { TwoFactorAuthenticatorComponent } from "../settings/two-factor-authenticator.component";
import { TwoFactorDuoComponent } from "../settings/two-factor-duo.component";
import { TwoFactorEmailComponent } from "../settings/two-factor-email.component";
@@ -159,7 +156,9 @@ import { CollectionsComponent } from "../vault/collections.component";
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
import { ShareComponent } from "../vault/share.component";
import { OrganizationCreateModule } from "./organizations/create/organization-create.module";
import { PipesModule } from "./pipes/pipes.module";
import { RegisterFormModule } from "./register-form/register-form.module";
import { SharedModule } from "./shared.module";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { OrganizationBadgeModule } from "./vault/modules/organization-badge/organization-badge.module";
@@ -167,7 +166,14 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
// Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left.
// If you are building new functionality, please create or extend a feature module instead.
@NgModule({
imports: [SharedModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
imports: [
SharedModule,
VaultFilterModule,
OrganizationBadgeModule,
PipesModule,
OrganizationCreateModule,
RegisterFormModule,
],
declarations: [
PremiumBadgeComponent,
AcceptEmergencyComponent,
@@ -267,8 +273,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
PasswordRepromptComponent,
FilePasswordPromptComponent,
UserVerificationPromptComponent,
PasswordStrengthComponent,
PaymentComponent,
PaymentMethodComponent,
PersonalOwnershipPolicyComponent,
PreferencesComponent,
@@ -301,7 +305,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
SponsoringOrgRowComponent,
SsoComponent,
SubscriptionComponent,
TaxInfoComponent,
ToolsComponent,
TwoFactorAuthenticationPolicyComponent,
TwoFactorAuthenticatorComponent,
@@ -423,8 +426,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
PasswordGeneratorPolicyComponent,
PasswordRepromptComponent,
FilePasswordPromptComponent,
PasswordStrengthComponent,
PaymentComponent,
PaymentMethodComponent,
PersonalOwnershipPolicyComponent,
PreferencesComponent,
@@ -457,7 +458,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
SponsoringOrgRowComponent,
SsoComponent,
SubscriptionComponent,
TaxInfoComponent,
ToolsComponent,
TwoFactorAuthenticationPolicyComponent,
TwoFactorAuthenticatorComponent,

View File

@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../shared.module";
import { OrganizationInformationComponent } from "./organization-information.component";
@NgModule({
imports: [SharedModule],
declarations: [OrganizationInformationComponent],
exports: [OrganizationInformationComponent],
})
export class OrganizationCreateModule {}

View File

@@ -0,0 +1,38 @@
<form #form [formGroup]="formGroup" *ngIf="nameOnly">
<bit-form-field>
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>
</form>
<form #form [formGroup]="formGroup" *ngIf="!nameOnly">
<h2>{{ "generalInformation" | i18n }}</h2>
<div class="tw-flex tw-w-full tw-space-x-4" *ngIf="createOrganization">
<bit-form-field class="tw-w-1/2">
<bit-label>{{ "organizationName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>
<bit-form-field class="tw-w-1/2">
<bit-label>{{ "billingEmail" | i18n }}</bit-label>
<input bitInput type="email" formControlName="billingEmail" />
</bit-form-field>
<bit-form-field class="tw-w-1/2" *ngIf="isProvider">
<bit-label>{{ "clientOwnerEmail" | i18n }}</bit-label>
<input bitInput type="email" formControlName="clientOwnerEmail" />
</bit-form-field>
</div>
<div *ngIf="!isProvider && !acceptingSponsorship">
<input
type="checkbox"
name="businessOwned"
formControlName="businessOwned"
(change)="changedBusinessOwned.emit()"
/>
<bit-label for="businessOwned" class="tw-mb-3">{{ "accountOwnedBusiness" | i18n }}</bit-label>
<div class="tw-mt-4" *ngIf="formGroup.controls['businessOwned'].value">
<bit-form-field class="tw-w-1/2">
<bit-label>{{ "businessName" | i18n }}</bit-label>
<input bitInput type="text" formControlName="businessName" />
</bit-form-field>
</div>
</div>
</form>

View File

@@ -0,0 +1,15 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { FormGroup } from "@angular/forms";
@Component({
selector: "app-org-info",
templateUrl: "organization-information.component.html",
})
export class OrganizationInformationComponent {
@Input() nameOnly = false;
@Input() createOrganization = true;
@Input() isProvider = false;
@Input() acceptingSponsorship = false;
@Input() formGroup: FormGroup;
@Output() changedBusinessOwned = new EventEmitter<void>();
}

View File

@@ -64,9 +64,7 @@
(click)="filterSelected(true)"
>
{{ "selected" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="selectedCount">{{
selectedCount
}}</span>
<span bitBadge badgeType="info" *ngIf="selectedCount">{{ selectedCount }}</span>
</button>
</div>
</div>
@@ -115,12 +113,14 @@
<td>
{{ u.email }}
<span
class="badge badge-secondary"
bitBadge
badgeType="secondary"
*ngIf="u.status === organizationUserStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span
class="badge badge-warning"
bitBadge
badgeType="warning"
*ngIf="u.status === organizationUserStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>

View File

@@ -0,0 +1,121 @@
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
class="tw-container tw-mx-auto"
[formGroup]="formGroup"
>
<div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
<input bitInput type="email" formControlName="email" />
<bit-hint>{{ "emailAddressDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
<bit-hint>{{ "yourNameDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3">
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
bitInput
(input)="updatePasswordStrength()"
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="masterPassword"
/>
<button type="button" bitSuffix bitButton (click)="togglePassword()">
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<bit-hint>
<span class="tw-font-semibold">Important:</span>
{{ "masterPassImportant" | i18n }}
</bit-hint>
</bit-form-field>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "reTypeMasterPass" | i18n }}</bit-label>
<input
bitInput
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="confirmMasterPassword"
/>
<button type="button" bitSuffix bitButton (click)="togglePassword()">
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "masterPassHint" | i18n }}</bit-label>
<input bitInput type="text" formControlName="hint" />
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
</bit-form-field>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="tw-flex tw-items-start tw-mb-3" *ngIf="showTerms">
<div class="tw-flex tw-items-center tw-h-6">
<input
class="tw-w-4 tw-rounded tw-border tw-border-gray-300"
bitInput
type="checkbox"
formControlName="acceptPolicies"
/>
</div>
<bit-label class="ml-2">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</bit-label>
</div>
<div class="tw-flex tw-mb-3">
<bit-submit-button [loading]="form.loading">{{ "createAccount" | i18n }}</bit-submit-button>
<a
bitButton
buttonType="secondary"
routerLink="/login"
class="tw-inline-flex tw-items-center tw-ml-3 tw-px-3"
>
<i class="bwi bwi-sign-in tw-mr-2"></i>
{{ "logIn" | i18n }}
</a>
</div>
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
</div>
</form>

View File

@@ -0,0 +1,87 @@
import { Component, Input } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions";
@Component({
selector: "app-register-form",
templateUrl: "./register-form.component.html",
})
export class RegisterFormComponent extends BaseRegisterComponent {
@Input() queryParamEmail: string;
@Input() enforcedPolicyOptions: MasterPasswordPolicyOptions;
showErrorSummary = false;
constructor(
formValidationErrorService: FormValidationErrorsService,
formBuilder: FormBuilder,
authService: AuthService,
router: Router,
i18nService: I18nService,
cryptoService: CryptoService,
apiService: ApiService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
private policyService: PolicyService,
environmentService: EnvironmentService,
logService: LogService
) {
super(
formValidationErrorService,
formBuilder,
authService,
router,
i18nService,
cryptoService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService
);
}
async ngOnInit() {
await super.ngOnInit();
if (this.queryParamEmail) {
this.formGroup.get("email")?.setValue(this.queryParamEmail);
}
}
async submit() {
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
this.masterPasswordScore,
this.formGroup.get("masterPassword")?.value,
this.enforcedPolicyOptions
)
) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordPolicyRequirementsNotMet")
);
return;
}
await super.submit(false);
}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../shared.module";
import { RegisterFormComponent } from "./register-form.component";
@NgModule({
imports: [SharedModule],
declarations: [RegisterFormComponent],
exports: [RegisterFormComponent],
})
export class RegisterFormModule {}

View File

@@ -62,10 +62,14 @@ import {
ButtonModule,
CalloutModule,
FormFieldModule,
MenuModule,
SubmitButtonModule,
MenuModule,
} from "@bitwarden/components";
import { PasswordStrengthComponent } from "../components/password-strength.component";
import { PaymentComponent } from "../settings/payment.component";
import { TaxInfoComponent } from "../settings/tax-info.component";
registerLocaleData(localeAf, "af");
registerLocaleData(localeAz, "az");
registerLocaleData(localeBe, "be");
@@ -118,6 +122,7 @@ registerLocaleData(localeZhCn, "zh-CN");
registerLocaleData(localeZhTw, "zh-TW");
@NgModule({
declarations: [PasswordStrengthComponent, PaymentComponent, TaxInfoComponent],
imports: [
CommonModule,
DragDropModule,
@@ -153,6 +158,9 @@ registerLocaleData(localeZhTw, "zh-TW");
MenuModule,
FormFieldModule,
SubmitButtonModule,
PasswordStrengthComponent,
PaymentComponent,
TaxInfoComponent,
],
providers: [DatePipe],
bootstrap: [],

View File

@@ -0,0 +1,48 @@
<form #form [formGroup]="formGroup" [appApiAction]="formPromise" (ngSubmit)="submit()">
<div class="tw-container tw-mb-3">
<div class="tw-mb-6">
<h2 class="tw-text-base tw-font-semibold tw-mb-3">{{ "billingPlanLabel" | i18n }}</h2>
<div class="tw-items-center tw-mb-1" *ngFor="let selectablePlan of selectablePlans">
<label class="tw-block tw- tw-text-main" for="interval{{ selectablePlan.type }}">
<input
checked
class="tw-w-4 tw-h-4 tw-align-middle"
id="interval{{ selectablePlan.type }}"
name="plan"
type="radio"
[value]="selectablePlan.type"
formControlName="plan"
/>
<ng-container *ngIf="selectablePlan.isAnnual">
{{ "annual" | i18n }} -
{{
(selectablePlan.basePrice === 0 ? selectablePlan.seatPrice : selectablePlan.basePrice)
| currency: "$"
}}
/{{ "yr" | i18n }}
</ng-container>
<ng-container *ngIf="!selectablePlan.isAnnual">
{{ "monthly" | i18n }} -
{{
(selectablePlan.basePrice === 0 ? selectablePlan.seatPrice : selectablePlan.basePrice)
| currency: "$"
}}
/{{ "monthAbbr" | i18n }}
</ng-container>
</label>
</div>
</div>
<div class="tw-mb-4">
<h2 class="tw-text-base tw-mb-3 tw-font-semibold">{{ "paymentType" | i18n }}</h2>
<app-payment [hideCredit]="true" [trialFlow]="true"></app-payment>
<app-tax-info [trialFlow]="true" (onCountryChanged)="changedCountry()"></app-tax-info>
</div>
<div class="tw-flex tw-space-x-2">
<bit-submit-button [loading]="form.loading">{{ "startTrial" | i18n }}</bit-submit-button>
<button bitButton type="button" buttonType="secondary" (click)="stepBack()">Back</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,68 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { FormBuilder, FormGroup } from "@angular/forms";
import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { SyncService } from "@bitwarden/common/abstractions/sync.service";
import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component";
@Component({
selector: "app-billing",
templateUrl: "./billing.component.html",
})
export class BillingComponent extends OrganizationPlansComponent {
@Input() orgInfoForm: FormGroup;
@Output() previousStep = new EventEmitter();
constructor(
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService,
router: Router,
syncService: SyncService,
policyService: PolicyService,
organizationService: OrganizationService,
logService: LogService,
messagingService: MessagingService,
formBuilder: FormBuilder
) {
super(
apiService,
i18nService,
platformUtilsService,
cryptoService,
router,
syncService,
policyService,
organizationService,
logService,
messagingService,
formBuilder
);
}
async ngOnInit() {
this.formGroup.patchValue({
name: this.orgInfoForm.get("name")?.value,
billingEmail: this.orgInfoForm.get("email")?.value,
additionalSeats: 1,
plan: this.plan,
product: this.product,
});
this.isInTrialFlow = true;
await super.ngOnInit();
}
stepBack() {
this.previousStep.emit();
}
}

View File

@@ -0,0 +1,17 @@
<div class="tw-pl-6 tw-pb-6">
<p class="tw-text-xl">{{ "trialThankYou" | i18n: orgLabel }}</p>
<ul class="tw-list-disc">
<li>
<p>
{{ "trialConfirmationEmail" | i18n }}
<span class="tw-font-bold">{{ email }}</span
>.
</p>
</li>
<li>
<p>
{{ "trialPaidInfoMessage" | i18n: orgLabel }}
</p>
</li>
</ul>
</div>

View File

@@ -0,0 +1,10 @@
import { Component, Input } from "@angular/core";
@Component({
selector: "app-trial-confirmation-details",
templateUrl: "confirmation-details.component.html",
})
export class ConfirmationDetailsComponent {
@Input() email: string;
@Input() orgLabel: string;
}

View File

@@ -0,0 +1,11 @@
<h1 class="!tw-text-alt2">You've chosen Bitwarden for Enterprise</h1>
<div class="tw-pt-24">
<h2>What you can do with Bitwarden for Enterprise</h2>
</div>
<div class="tw-text-3xl tw-text-main tw-mt-12">
<p class="tw-mt-2.5 tw-mb-20">Collaborate and share securely</p>
<p class="tw-mt-2.5 tw-mb-20">Deploy and manage quickly and easily</p>
<p class="tw-mt-2.5 tw-mb-20">Access anywhere on any device</p>
<p class="tw-mt-2.5 tw-mb-20">Create your account to get started</p>
</div>

View File

@@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-enterprise-content",
templateUrl: "enterprise-content.component.html",
})
export class EnterpriseContentComponent {}

View File

@@ -0,0 +1,13 @@
<h1 class="!tw-text-alt2">You've chosen Bitwarden for Families</h1>
<div class="tw-pt-24">
<h2>
Trusted by millions of individuals, teams, and organizations worldwide for secure password
storage and sharing.
</h2>
</div>
<div class="tw-text-3xl tw-text-main tw-mt-12">
<p class="tw-mt-2.5 tw-mb-20">Collaborate and share securely</p>
<p class="tw-mt-2.5 tw-mb-20">Deploy and manage quickly and easily</p>
<p class="tw-mt-2.5 tw-mb-20">Access anywhere on any device</p>
<p class="tw-mt-2.5 tw-mb-20">Create your account to get started</p>
</div>

View File

@@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-families-content",
templateUrl: "families-content.component.html",
})
export class FamiliesContentComponent {}

View File

@@ -0,0 +1,10 @@
<h1 class="!tw-text-alt2">You've chosen Bitwarden for Teams</h1>
<div class="tw-pt-24">
<h2>What you can do with Btiwarden for Teams</h2>
</div>
<div class="tw-text-3xl tw-text-main tw-mt-12">
<p class="tw-mt-2.5 tw-mb-20">Collaborate and share securely</p>
<p class="tw-mt-2.5 tw-mb-20">Deploy and manage quickly and easily</p>
<p class="tw-mt-2.5 tw-mb-20">Access anywhere on any device</p>
<p class="tw-mt-2.5 tw-mb-20">Create your account to get started</p>
</div>

View File

@@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "app-teams-content",
templateUrl: "teams-content.component.html",
})
export class TeamsContentComponent {}

View File

@@ -0,0 +1,94 @@
<div *ngIf="accountCreateOnly" class="">
<h1 class="tw-text-xl tw-text-center tw-mt-12" *ngIf="!layout">{{ "createAccount" | i18n }}</h1>
<div
class="tw-m-auto tw-rounded tw-border tw-border-solid tw-bg-background tw-border-secondary-300 tw-min-w-xl tw-max-w-xl tw-p-8"
>
<app-register-form
[queryParamEmail]="email"
[enforcedPolicyOptions]="enforcedPolicyOptions"
></app-register-form>
</div>
</div>
<div *ngIf="!accountCreateOnly">
<div
class="tw-bg-background-alt2 tw-h-96 tw--mt-48 tw-absolute tw--skew-y-3 tw-w-full tw--z-10"
></div>
<div class="tw-flex tw-max-w-screen-xl tw-min-w-4xl tw-mx-auto tw-px-4">
<div class="tw-w-1/2">
<img
alt="Bitwarden"
style="height: 50px; width: 335px"
class="tw-mt-6"
src="../../images/register-layout/logo-horizontal-white.svg"
/>
<!-- This is to for illustrative purposes and content will be replaced by marketing -->
<div class="tw-pt-12">
<!-- Teams Body -->
<app-teams-content *ngIf="org === 'teams'"></app-teams-content>
<!-- Enterprise Body -->
<app-enterprise-content *ngIf="org === 'enterprise'"></app-enterprise-content>
<!-- Families Body -->
<app-families-content *ngIf="org === 'families'"></app-families-content>
</div>
</div>
<div class="tw-w-1/2">
<div class="tw-pt-56">
<div class="tw-rounded tw-border tw-border-solid tw-bg-background tw-border-secondary-300">
<div class="tw-h-12 tw-flex tw-items-center tw-rounded-t tw-bg-secondary-100 tw-w-full">
<h2 class="tw-uppercase tw-pl-4 tw-text-base tw-mb-0 tw-font-bold">
Start your 7-Day free trial of Bitwarden for {{ org }}
</h2>
</div>
<app-vertical-stepper #stepper linear (selectionChange)="stepSelectionChange($event)">
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
<app-register-form
[isInTrialFlow]="true"
(createdAccount)="createdAccount($event)"
></app-register-form>
</app-vertical-step>
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
<button
bitButton
buttonType="primary"
[disabled]="orgInfoFormGroup.get('name').hasError('required')"
cdkStepperNext
>
Next
</button>
</app-vertical-step>
<app-vertical-step label="Billing" [subLabel]="billingSubLabel">
<app-billing
*ngIf="stepper.selectedIndex === 2"
[plan]="plan"
[product]="product"
[orgInfoForm]="orgInfoFormGroup"
(previousStep)="previousStep()"
(onTrialBillingSuccess)="billingSuccess($event)"
></app-billing>
</app-vertical-step>
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
<app-trial-confirmation-details
[email]="email"
[orgLabel]="orgLabel"
></app-trial-confirmation-details>
<div class="tw-flex tw-mb-3">
<button bitButton buttonType="primary" (click)="navigateToOrgVault()">
Get Started
</button>
<button
bitButton
buttonType="secondary"
(click)="navigateToOrgInvite()"
class="tw-inline-flex tw-items-center tw-ml-3 tw-px-3"
>
Invite Users
</button>
</div>
</app-vertical-step>
</app-vertical-stepper>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,145 @@
import { StepperSelectionEvent } from "@angular/cdk/stepper";
import { TitleCasePipe } from "@angular/common";
import { Component, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PlanType } from "@bitwarden/common/enums/planType";
import { ProductType } from "@bitwarden/common/enums/productType";
import { PolicyData } from "@bitwarden/common/models/data/policyData";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/models/domain/masterPasswordPolicyOptions";
import { Policy } from "@bitwarden/common/models/domain/policy";
import { VerticalStepperComponent } from "../vertical-stepper/vertical-stepper.component";
@Component({
selector: "app-trial",
templateUrl: "trial-initiation.component.html",
})
export class TrialInitiationComponent implements OnInit {
email = "";
org = "teams";
orgInfoSubLabel = "";
orgId = "";
orgLabel = "";
billingSubLabel = "";
plan: PlanType;
product: ProductType;
accountCreateOnly = true;
policies: Policy[];
enforcedPolicyOptions: MasterPasswordPolicyOptions;
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
orgInfoFormGroup = this.formBuilder.group({
name: ["", [Validators.required]],
email: [""],
});
constructor(
private route: ActivatedRoute,
protected router: Router,
private formBuilder: FormBuilder,
private titleCasePipe: TitleCasePipe,
private stateService: StateService,
private apiService: ApiService,
private logService: LogService,
private policyService: PolicyService,
private i18nService: I18nService
) {}
async ngOnInit(): Promise<void> {
this.route.queryParams.pipe(first()).subscribe((qParams) => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.email = qParams.email;
}
if (!qParams.org) {
return;
}
this.org = qParams.org;
this.orgLabel = this.titleCasePipe.transform(this.org);
this.accountCreateOnly = false;
if (qParams.org === "families") {
this.plan = PlanType.FamiliesAnnually;
this.product = ProductType.Families;
} else if (qParams.org === "teams") {
this.plan = PlanType.TeamsAnnually;
this.product = ProductType.Teams;
} else if (qParams.org === "enterprise") {
this.plan = PlanType.EnterpriseAnnually;
this.product = ProductType.Enterprise;
}
});
const invite = await this.stateService.getOrganizationInvitation();
if (invite != null) {
try {
const policies = await this.apiService.getPoliciesByToken(
invite.organizationId,
invite.token,
invite.email,
invite.organizationUserId
);
if (policies.data != null) {
const policiesData = policies.data.map((p) => new PolicyData(p));
this.policies = policiesData.map((p) => new Policy(p));
}
} catch (e) {
this.logService.error(e);
}
}
if (this.policies != null) {
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(
this.policies
);
}
}
stepSelectionChange(event: StepperSelectionEvent) {
// Set org info sub label
if (event.selectedIndex === 1 && this.orgInfoFormGroup.controls.name.value === "") {
this.orgInfoSubLabel =
"Enter your " + this.titleCasePipe.transform(this.org) + " organization information";
} else if (event.previouslySelectedIndex === 1) {
this.orgInfoSubLabel = this.orgInfoFormGroup.controls.name.value;
}
//set billing sub label
if (event.selectedIndex === 2) {
this.billingSubLabel = this.i18nService.t("billingTrialSubLabel");
}
}
createdAccount(email: string) {
this.email = email;
this.orgInfoFormGroup.get("email")?.setValue(email);
this.verticalStepper.next();
}
billingSuccess(event: any) {
this.orgId = event?.orgId;
this.billingSubLabel = event?.subLabelText;
this.verticalStepper.next();
}
navigateToOrgVault() {
this.router.navigate(["organizations", this.orgId, "vault"]);
}
navigateToOrgInvite() {
this.router.navigate(["organizations", this.orgId, "manage", "people"]);
}
previousStep() {
this.verticalStepper.previous();
}
}

View File

@@ -0,0 +1,39 @@
import { CdkStepperModule } from "@angular/cdk/stepper";
import { TitleCasePipe } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormFieldModule } from "@bitwarden/components";
import { OrganizationCreateModule } from "../organizations/create/organization-create.module";
import { RegisterFormModule } from "../register-form/register-form.module";
import { SharedModule } from "../shared.module";
import { VerticalStepperModule } from "../vertical-stepper/vertical-stepper.module";
import { BillingComponent } from "./billing.component";
import { ConfirmationDetailsComponent } from "./confirmation-details.component";
import { EnterpriseContentComponent } from "./enterprise-content.component";
import { FamiliesContentComponent } from "./families-content.component";
import { TeamsContentComponent } from "./teams-content.component";
import { TrialInitiationComponent } from "./trial-initiation.component";
@NgModule({
imports: [
SharedModule,
CdkStepperModule,
VerticalStepperModule,
FormFieldModule,
RegisterFormModule,
OrganizationCreateModule,
],
declarations: [
TrialInitiationComponent,
EnterpriseContentComponent,
FamiliesContentComponent,
TeamsContentComponent,
ConfirmationDetailsComponent,
BillingComponent,
],
exports: [TrialInitiationComponent],
providers: [TitleCasePipe],
})
export class TrialInitiationModule {}

View File

@@ -23,7 +23,7 @@
<li
*ngFor="let c of collections"
[ngClass]="{
active: c.node.id === activeFilter.selectedCollectionId
active: c.node.id === activeFilter.selectedCollectionId && activeFilter.selectedCollection
}"
class="filter-option"
>

View File

@@ -129,6 +129,12 @@
<button class="filter-button" (click)="applyOrganizationFilter(organization)">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
{{ organization.name }}
<i
*ngIf="!organization.enabled"
class="bwi bwi-fw bwi-exclamation-triangle text-danger"
aria-label="{{ 'organizationIsDisabled' | i18n }}"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
></i>
</button>
<ng-container>
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">

View File

@@ -1,6 +1,9 @@
import { Component } from "@angular/core";
import { OrganizationFilterComponent as BaseOrganizationFilterComponent } from "@bitwarden/angular/modules/vault-filter/components/organization-filter.component";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
@Component({
selector: "app-organization-filter",
@@ -8,4 +11,24 @@ import { OrganizationFilterComponent as BaseOrganizationFilterComponent } from "
})
export class OrganizationFilterComponent extends BaseOrganizationFilterComponent {
displayText = "allVaults";
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
) {
super();
}
async applyOrganizationFilter(organization: Organization) {
if (organization.enabled) {
//proceed with default behaviour for enabled organizations
super.applyOrganizationFilter(organization);
} else {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("disabledOrganizationFilterError")
);
}
}
}

View File

@@ -120,6 +120,7 @@ export class OrganizationOptionsComponent {
},
});
} else {
// Remove reset password
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.masterPasswordHash = "ignored";
request.resetPasswordKey = null;

View File

@@ -57,7 +57,7 @@
[hide]="hideFolders"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[folderNodes]="folders"
[folderNodes]="folders$ | async"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
(onAddFolder)="addFolder()"

View File

@@ -6,7 +6,8 @@ import { VaultFilterService as BaseVaultFilterService } from "@bitwarden/angular
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
@@ -27,6 +28,7 @@ export class VaultFilterService extends BaseVaultFilterService {
cipherService: CipherService,
collectionService: CollectionService,
policyService: PolicyService,
private i18nService: I18nService,
protected apiService: ApiService
) {
super(
@@ -69,6 +71,11 @@ export class VaultFilterService extends BaseVaultFilterService {
result = await this.collectionService.decryptMany(collectionDomains);
}
const noneCollection = new CollectionView();
noneCollection.name = this.i18nService.t("unassigned");
noneCollection.organizationId = organizationId;
result.push(noneCollection);
const nestedCollections = await this.collectionService.getAllNested(result);
return new DynamicTreeNode<CollectionView>({
fullList: result,

View File

@@ -173,7 +173,10 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
async applyVaultFilter(vaultFilter: VaultFilter) {
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
this.activeFilter = vaultFilter;
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
await this.ciphersComponent.reload(
this.activeFilter.buildFilter(),
vaultFilter.status === "trash"
);
this.filterComponent.searchPlaceholder = this.vaultService.calculateSearchBarLocalizationString(
this.activeFilter
);
@@ -196,40 +199,6 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
this.ciphersComponent.search(200);
}
private buildFilter(): (cipher: CipherView) => boolean {
return (cipher) => {
let cipherPassesFilter = true;
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
cipherPassesFilter = cipher.favorite;
}
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolder &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
}
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null &&
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
}
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
}
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
async editCipherAttachments(cipher: CipherView) {
const canAccessPremium = await this.stateService.getCanAccessPremium();
if (cipher.organizationId == null && !canAccessPremium) {

View File

@@ -162,46 +162,15 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
async applyVaultFilter(vaultFilter: VaultFilter) {
this.ciphersComponent.showAddNew = vaultFilter.status !== "trash";
this.activeFilter = vaultFilter;
await this.ciphersComponent.reload(this.buildFilter(), vaultFilter.status === "trash");
await this.ciphersComponent.reload(
this.activeFilter.buildFilter(),
vaultFilter.status === "trash"
);
this.vaultFilterComponent.searchPlaceholder =
this.vaultService.calculateSearchBarLocalizationString(this.activeFilter);
this.go();
}
private buildFilter(): (cipher: CipherView) => boolean {
return (cipher) => {
let cipherPassesFilter = true;
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
cipherPassesFilter = cipher.favorite;
}
if (this.deleted && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolder != null &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
}
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null &&
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
}
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
}
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);
@@ -242,7 +211,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
if (this.organization.canEditAnyCollection) {
comp.collectionIds = cipher.collectionIds;
comp.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly
(c) => !c.readOnly && c.id != null
);
}
comp.organization = this.organization;
@@ -261,7 +230,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
component.type = this.type;
if (this.organization.canEditAnyCollection) {
component.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly
(c) => !c.readOnly && c.id != null
);
}
if (this.collectionId != null) {
@@ -316,7 +285,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
component.organizationId = this.organization.id;
if (this.organization.canEditAnyCollection) {
component.collections = this.vaultFilterComponent.collections.fullList.filter(
(c) => !c.readOnly
(c) => !c.readOnly && c.id != null
);
}
// Regardless of Admin state, the collection Ids need to passed manually as they are not assigned value

View File

@@ -14,7 +14,7 @@ export class VaultService {
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId != "none") {
return "searchFolder";
}
if (vaultFilter.selectedCollectionId != null) {
if (vaultFilter.selectedCollection) {
return "searchCollection";
}
if (vaultFilter.selectedOrganizationId != null) {

View File

@@ -0,0 +1,45 @@
<div class="tw-m-2.5 tw-text-center tw-h-16">
<button
(click)="selectStep()"
[disabled]="disabled"
class="tw-w-full tw-flex tw-border-none tw-bg-transparent tw-items-center"
[ngClass]="{
'hover:tw-bg-secondary-100': !disabled && step.editable
}"
[attr.aria-expanded]="selected"
>
<span
class="tw-rounded-full tw-font-bold tw-leading-9 tw-mr-3.5 tw-w-9"
*ngIf="!step.completed"
[ngClass]="{
'tw-text-contrast tw-bg-primary-500': selected,
'tw-text-main tw-bg-secondary-300': !selected && !disabled && step.editable,
'tw-text-muted tw-bg-transparent': disabled
}"
>
{{ stepNumber }}
</span>
<span
class="tw-text-contrast tw-bg-primary-500 tw-rounded-full tw-font-bold tw-leading-9 tw-mr-3.5 tw-w-9"
*ngIf="step.completed"
>
<i class="bwi bwi-fw bwi-check tw-p-1" aria-hidden="true"></i>
</span>
<div
class="tw-text-left tw-txt-main tw-leading-snug tw-h-12 tw-mt-3.5"
[ngClass]="{
'tw-font-bold': selected
}"
>
<p
class="main-label text tw-text-main tw-mb-1"
[ngClass]="{
'tw-mt-1': !step.subLabel
}"
>
{{ step.label }}
</p>
<p class="sub-label small tw-text-muted">{{ step.subLabel }}</p>
</div>
</button>
</div>

View File

@@ -0,0 +1,20 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { VerticalStep } from "./vertical-step.component";
@Component({
selector: "app-vertical-step-content",
templateUrl: "vertical-step-content.component.html",
})
export class VerticalStepContentComponent {
@Output() onSelectStep = new EventEmitter<void>();
@Input() disabled = false;
@Input() selected = false;
@Input() step: VerticalStep;
@Input() stepNumber: number;
selectStep() {
this.onSelectStep.emit();
}
}

View File

@@ -0,0 +1,8 @@
<ng-template>
<div
class="tw-pl-7 tw-inline-block tw-w-11/12"
[ngClass]="{ 'tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300': applyBorder }"
>
<ng-content></ng-content>
</div>
</ng-template>

View File

@@ -0,0 +1,12 @@
import { CdkStep } from "@angular/cdk/stepper";
import { Component, Input } from "@angular/core";
@Component({
selector: "app-vertical-step",
templateUrl: "vertical-step.component.html",
providers: [{ provide: CdkStep, useExisting: VerticalStep }],
})
export class VerticalStep extends CdkStep {
@Input() subLabel = "";
@Input() applyBorder = true;
}

View File

@@ -0,0 +1,22 @@
<div>
<ul class="tw-flex tw-list-none tw-flex-col tw-flex-wrap tw-p-5">
<li *ngFor="let step of steps; let i = index; let isLast = last">
<app-vertical-step-content
[disabled]="isStepDisabled(i)"
[selected]="selectedIndex === i"
[step]="step"
[stepNumber]="i + 1"
(onSelectStep)="selectStepByIndex(i)"
></app-vertical-step-content>
<div
class="tw-pl-7 tw-inline-block"
*ngIf="selectedIndex === i"
[ngTemplateOutlet]="selected ? selected.content : null"
></div>
<div
class="tw-h-6 tw-ml-8 tw-border-0 tw-border-l tw-border-solid tw-border-secondary-300"
*ngIf="!isLast && !(selectedIndex === i)"
></div>
</li>
</ul>
</div>

View File

@@ -0,0 +1,29 @@
import { CdkStepper } from "@angular/cdk/stepper";
import { Component, Input } from "@angular/core";
@Component({
selector: "app-vertical-stepper",
templateUrl: "vertical-stepper.component.html",
providers: [{ provide: CdkStepper, useExisting: VerticalStepperComponent }],
})
export class VerticalStepperComponent extends CdkStepper {
@Input()
activeClass = "active";
isNextButtonHidden() {
return !(this.steps.length === this.selectedIndex + 1);
}
isStepDisabled(index: number) {
if (this.selectedIndex !== index) {
return this.selectedIndex === index - 1
? !this.steps.find((_, i) => i == index - 1)?.completed
: true;
}
return false;
}
selectStepByIndex(index: number): void {
this.selectedIndex = index;
}
}

View File

@@ -0,0 +1,14 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../shared.module";
import { VerticalStepContentComponent } from "./vertical-step-content.component";
import { VerticalStep } from "./vertical-step.component";
import { VerticalStepperComponent } from "./vertical-stepper.component";
@NgModule({
imports: [SharedModule],
declarations: [VerticalStepperComponent, VerticalStep, VerticalStepContentComponent],
exports: [VerticalStepperComponent, VerticalStep],
})
export class VerticalStepperModule {}

View File

@@ -86,6 +86,9 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
case this.organization.canManageSso:
route = "manage/sso";
break;
case this.organization.canManageScim:
route = "manage/scim";
break;
case this.organization.canAccessEventLogs:
route = "manage/events";
break;

View File

@@ -22,8 +22,8 @@
{{ error }}
</app-callout>
<ng-container *ngIf="!done">
<app-callout type="warning" *ngIf="users.length > 0 && !error">
{{ usersWarning }}
<app-callout type="warning" *ngIf="users.length > 0 && !error && isDeactivating">
{{ "revokeUsersWarning" | i18n }}
</app-callout>
<table class="table table-hover table-list">
<thead>

View File

@@ -1,6 +1,5 @@
import { Component } from "@angular/core";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalConfig } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@@ -26,7 +25,6 @@ export class BulkDeactivateComponent {
constructor(
protected apiService: ApiService,
protected i18nService: I18nService,
private modalRef: ModalRef,
config: ModalConfig
) {
this.isDeactivating = config.data.isDeactivating;
@@ -35,21 +33,16 @@ export class BulkDeactivateComponent {
}
get bulkTitle() {
const titleKey = this.isDeactivating ? "deactivateUsers" : "activateUsers";
const titleKey = this.isDeactivating ? "revokeUsers" : "restoreUsers";
return this.i18nService.t(titleKey);
}
get usersWarning() {
const warningKey = this.isDeactivating ? "deactivateUsersWarning" : "activateUsersWarning";
return this.i18nService.t(warningKey);
}
async submit() {
this.loading = true;
try {
const response = await this.performBulkUserAction();
const bulkMessage = this.isDeactivating ? "bulkDeactivatedMessage" : "bulkActivatedMessage";
const bulkMessage = this.isDeactivating ? "bulkRevokedMessage" : "bulkRestoredMessage";
response.data.forEach((entry) => {
const error = entry.error !== "" ? entry.error : this.i18nService.t(bulkMessage);
this.statuses.set(entry.id, error);
@@ -60,7 +53,6 @@ export class BulkDeactivateComponent {
}
this.loading = false;
this.modalRef.close();
}
protected async performBulkUserAction() {

View File

@@ -23,7 +23,7 @@
</app-callout>
<ng-container *ngIf="!done">
<app-callout type="warning" *ngIf="users.length > 0 && !error">
{{ "removeUsersWarning" | i18n }}
{{ removeUsersWarning }}
</app-callout>
<table class="table table-hover table-list">
<thead>

View File

@@ -43,4 +43,8 @@ export class BulkRemoveComponent {
const request = new OrganizationUserBulkRequest(this.users.map((user) => user.id));
return await this.apiService.deleteManyOrganizationUsers(this.organizationId, request);
}
protected get removeUsersWarning() {
return this.i18nService.t("removeOrgUsersConfirmation");
}
}

View File

@@ -44,6 +44,14 @@
>
{{ "singleSignOn" | i18n }}
</a>
<a
routerLink="scim"
class="list-group-item"
routerLinkActive="active"
*ngIf="organization.canManageScim && accessScim"
>
{{ "scim" | i18n }}
</a>
<a
routerLink="events"
class="list-group-item"

View File

@@ -4,6 +4,8 @@ import { ActivatedRoute } from "@angular/router";
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { flagEnabled } from "../../../utils/flags";
@Component({
selector: "app-org-manage",
templateUrl: "manage.component.html",
@@ -14,6 +16,7 @@ export class ManageComponent implements OnInit {
accessGroups = false;
accessEvents = false;
accessSso = false;
accessScim = false;
constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {}
@@ -24,6 +27,12 @@ export class ManageComponent implements OnInit {
this.accessSso = this.organization.useSso;
this.accessEvents = this.organization.useEvents;
this.accessGroups = this.organization.useGroups;
if (flagEnabled("scim")) {
this.accessScim = this.organization.useScim;
} else {
this.accessScim = false;
}
});
}
}

View File

@@ -9,7 +9,7 @@
(click)="filter(null)"
>
{{ "all" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="allCount">{{ allCount }}</span>
<span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
</button>
<button
type="button"
@@ -18,7 +18,7 @@
(click)="filter(userStatusType.Invited)"
>
{{ "invited" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{ invitedCount }}</span>
<span bitBadge badgeType="info" *ngIf="invitedCount">{{ invitedCount }}</span>
</button>
<button
type="button"
@@ -27,9 +27,7 @@
(click)="filter(userStatusType.Accepted)"
>
{{ "accepted" | i18n }}
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{
acceptedCount
}}</span>
<span bitBadge badgeType="warning" *ngIf="acceptedCount">{{ acceptedCount }}</span>
</button>
<button
type="button"
@@ -37,10 +35,8 @@
[ngClass]="{ active: status == userStatusType.Deactivated }"
(click)="filter(userStatusType.Deactivated)"
>
{{ "deactivated" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="deactivatedCount">{{
deactivatedCount
}}</span>
{{ "revoked" | i18n }}
<span bitBadge badgeType="info" *ngIf="deactivatedCount">{{ deactivatedCount }}</span>
</button>
</div>
<div class="ml-3">
@@ -81,11 +77,11 @@
</button>
<button class="dropdown-item" appStopClick (click)="bulkActivate()">
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "activate" | i18n }}
{{ "restoreAccess" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="bulkDeactivate()">
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "deactivate" | i18n }}
{{ "revokeAccess" | i18n }}
</button>
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
@@ -156,14 +152,14 @@
</td>
<td>
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
<span class="badge badge-secondary" *ngIf="u.status === userStatusType.Invited">{{
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Invited">{{
"invited" | i18n
}}</span>
<span class="badge badge-warning" *ngIf="u.status === userStatusType.Accepted">{{
<span bitBadge badgeType="warning" *ngIf="u.status === userStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<span class="badge badge-secondary" *ngIf="u.status === userStatusType.Deactivated">{{
"deactivated" | i18n
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Deactivated">{{
"revoked" | i18n
}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
</td>
@@ -263,7 +259,7 @@
*ngIf="u.status === userStatusType.Deactivated"
>
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "activate" | i18n }}
{{ "restoreAccess" | i18n }}
</a>
<a
class="dropdown-item"
@@ -273,7 +269,7 @@
*ngIf="u.status !== userStatusType.Deactivated"
>
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "deactivate" | i18n }}
{{ "revokeAccess" | i18n }}
</a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>

View File

@@ -397,12 +397,18 @@ export class PeopleComponent
);
}
protected deleteWarningMessage(user: OrganizationUserUserDetailsResponse): string {
if (user.usesKeyConnector) {
return this.i18nService.t("removeUserConfirmationKeyConnector");
}
protected async removeUserConfirmationDialog(user: OrganizationUserUserDetailsResponse) {
const warningMessage = user.usesKeyConnector
? this.i18nService.t("removeUserConfirmationKeyConnector")
: this.i18nService.t("removeOrgUserConfirmation");
return super.deleteWarningMessage(user);
return this.platformUtilsService.showDialog(
warningMessage,
this.i18nService.t("removeUserIdAccess", this.userNamePipe.transform(user)),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
}
private async showBulkStatus(

View File

@@ -14,8 +14,8 @@
<tr *ngFor="let p of policies">
<td *ngIf="p.display(organization)">
<a href="#" appStopClick (click)="edit(p)">{{ p.name | i18n }}</a>
<span class="badge badge-success" *ngIf="policiesEnabledMap.get(p.type)">{{
"enabled" | i18n
<span bitBadge badgeType="success" *ngIf="policiesEnabledMap.get(p.type)">{{
"on" | i18n
}}</span>
<small class="text-muted d-block">{{ p.description | i18n }}</small>
</td>

View File

@@ -11,7 +11,7 @@
<h2 class="modal-title" id="userAddEditTitle">
{{ title }}
<small class="text-muted" *ngIf="name">{{ name }}</small>
<span class="badge badge-dark" *ngIf="isDeactivated">{{ "deactivated" | i18n }}</span>
<span bitBadge badgeType="secondary" *ngIf="isDeactivated">{{ "revoked" | i18n }}</span>
</h2>
<button
type="button"
@@ -383,41 +383,31 @@
type="button"
(click)="activate()"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'activate' | i18n }}"
*ngIf="editMode && isDeactivated"
[disabled]="form.loading"
>
<i
class="bwi bwi-plus-circle bwi-lg bwi-fw"
[hidden]="form.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "restoreAccess" | i18n }}</span>
</button>
<button
type="button"
(click)="deactivate()"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'deactivate' | i18n }}"
*ngIf="editMode && !isDeactivated"
[disabled]="form.loading"
>
<i
class="bwi bwi-minus-circle bwi-lg bwi-fw"
[hidden]="form.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!form.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "revokeAccess" | i18n }}</span>
</button>
<button
#deleteBtn

View File

@@ -187,7 +187,7 @@ export class UserAddEditComponent implements OnInit {
);
} else {
const request = new OrganizationUserInviteRequest();
request.emails = this.emails.trim().split(/\s*,\s*/);
request.emails = [...new Set(this.emails.trim().split(/\s*,\s*/))];
request.accessAll = this.access === "all";
request.type = this.type;
request.permissions = this.setRequestPermissions(
@@ -216,10 +216,10 @@ export class UserAddEditComponent implements OnInit {
const message = this.usesKeyConnector
? "removeUserConfirmationKeyConnector"
: "removeUserConfirmation";
: "removeOrgUserConfirmation";
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t(message),
this.name,
this.i18nService.t("removeUserIdAccess", this.name),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
@@ -251,9 +251,9 @@ export class UserAddEditComponent implements OnInit {
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("deactivateUserConfirmation"),
this.i18nService.t("deactivateUserId", this.name),
this.i18nService.t("deactivate"),
this.i18nService.t("revokeUserConfirmation"),
this.i18nService.t("revokeUserId", this.name),
this.i18nService.t("revokeAccess"),
this.i18nService.t("cancel"),
"warning"
);
@@ -270,7 +270,7 @@ export class UserAddEditComponent implements OnInit {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("deactivatedUserId", this.name)
this.i18nService.t("revokedUserId", this.name)
);
this.isDeactivated = true;
this.onDeactivatedUser.emit();
@@ -284,17 +284,6 @@ export class UserAddEditComponent implements OnInit {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("activateUserConfirmation"),
this.i18nService.t("activateUserId", this.name),
this.i18nService.t("activate"),
this.i18nService.t("cancel"),
"warning"
);
if (!confirmed) {
return false;
}
try {
this.formPromise = this.apiService.activateOrganizationUser(
this.organizationId,
@@ -304,7 +293,7 @@ export class UserAddEditComponent implements OnInit {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("activatedUserId", this.name)
this.i18nService.t("restoredUserId", this.name)
);
this.isDeactivated = false;
this.onActivatedUser.emit();

View File

@@ -11,6 +11,6 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -12,7 +12,7 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -8,7 +8,7 @@ import { PolicyType } from "@bitwarden/common/enums/policyType";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export class MasterPasswordPolicy extends BasePolicy {
name = "masterPass";
name = "masterPassPolicyTitle";
description = "masterPassPolicyDesc";
type = PolicyType.MasterPassword;
component = MasterPasswordPolicyComponent;

View File

@@ -8,7 +8,7 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -11,8 +11,6 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{
"personalOwnershipCheckboxDesc" | i18n
}}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -14,6 +14,6 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -15,7 +15,7 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -11,7 +11,7 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -11,6 +11,6 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -11,6 +11,6 @@
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</div>

View File

@@ -5,7 +5,7 @@ import { PolicyType } from "@bitwarden/common/enums/policyType";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export class TwoFactorAuthenticationPolicy extends BasePolicy {
name = "twoStepLogin";
name = "twoStepLoginPolicyTitle";
description = "twoStepLoginPolicyDesc";
type = PolicyType.TwoFactorAuthentication;
component = TwoFactorAuthenticationPolicyComponent;

View File

@@ -13,6 +13,7 @@ const permissions = {
Permissions.ManageUsers,
Permissions.ManagePolicies,
Permissions.ManageSso,
Permissions.ManageScim,
],
tools: [Permissions.AccessImportExport, Permissions.AccessReports],
settings: [Permissions.ManageOrganization],

View File

@@ -62,7 +62,7 @@
<span class="text-capitalize">{{
isSponsoredSubscription ? "sponsored" : subscription.status || "-"
}}</span>
<span class="badge badge-warning" *ngIf="subscriptionMarkedForCancel">{{
<span bitBadge badgeType="warning" *ngIf="subscriptionMarkedForCancel">{{
"pendingCancellation" | i18n
}}</span>
</dd>

View File

@@ -7,7 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ImportService as ImportServiceAbstraction } from "@bitwarden/common/abstractions/import.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";

View File

@@ -28,7 +28,7 @@
<a
href="#"
appStopClick
class="badge badge-primary"
bitBadge
*ngIf="!accessReports"
(click)="upgradeOrganization()"
>

View File

@@ -5,7 +5,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";

View File

@@ -1,10 +1,12 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { Route, RouterModule, Routes } from "@angular/router";
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
import { LockGuard } from "@bitwarden/angular/guards/lock.guard";
import { UnauthGuard } from "@bitwarden/angular/guards/unauth.guard";
import { flagEnabled, FlagName } from "../utils/flags";
import { AcceptEmergencyComponent } from "./accounts/accept-emergency.component";
import { AcceptOrganizationComponent } from "./accounts/accept-organization.component";
import { HintComponent } from "./accounts/hint.component";
@@ -24,6 +26,7 @@ import { VerifyRecoverDeleteComponent } from "./accounts/verify-recover-delete.c
import { HomeGuard } from "./guards/home.guard";
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
import { UserLayoutComponent } from "./layouts/user-layout.component";
import { TrialInitiationComponent } from "./modules/trial-initiation/trial-initiation.component";
import { IndividualVaultModule } from "./modules/vault/modules/individual-vault/individual-vault.module";
import { OrganizationsRoutingModule } from "./organizations/organization-routing.module";
import { AcceptFamilySponsorshipComponent } from "./organizations/sponsorships/accept-family-sponsorship.component";
@@ -60,7 +63,7 @@ const routes: Routes = [
{ path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] },
{
path: "register",
component: RegisterComponent,
component: flagEnabled("showTrial") ? TrialInitiationComponent : RegisterComponent,
canActivate: [UnauthGuard],
data: { titleId: "createAccount" },
},
@@ -251,3 +254,12 @@ const routes: Routes = [
exports: [RouterModule],
})
export class OssRoutingModule {}
export function buildFlaggedRoute(flagName: FlagName, route: Route): Route {
return flagEnabled(flagName)
? route
: {
path: route.path,
redirectTo: "/",
};
}

View File

@@ -1,10 +1,12 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "./modules/loose-components.module";
import { OrganizationCreateModule } from "./modules/organizations/create/organization-create.module";
import { OrganizationManageModule } from "./modules/organizations/manage/organization-manage.module";
import { OrganizationUserModule } from "./modules/organizations/users/organization-user.module";
import { PipesModule } from "./modules/pipes/pipes.module";
import { SharedModule } from "./modules/shared.module";
import { TrialInitiationModule } from "./modules/trial-initiation/trial-initiation.module";
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
import { OrganizationBadgeModule } from "./modules/vault/modules/organization-badge/organization-badge.module";
@@ -12,13 +14,22 @@ import { OrganizationBadgeModule } from "./modules/vault/modules/organization-ba
imports: [
SharedModule,
LooseComponentsModule,
TrialInitiationModule,
VaultFilterModule,
OrganizationBadgeModule,
PipesModule,
OrganizationManageModule,
OrganizationUserModule,
OrganizationCreateModule,
],
exports: [
SharedModule,
LooseComponentsModule,
TrialInitiationModule,
VaultFilterModule,
OrganizationBadgeModule,
PipesModule,
],
exports: [LooseComponentsModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
bootstrap: [],
})
export class OssModule {}

View File

@@ -50,7 +50,7 @@
<small>{{ c.subTitle }}</small>
</td>
<td class="text-right">
<span class="badge badge-warning">
<span bitBadge badgeType="warning">
{{ "exposedXTimes" | i18n: (exposedPasswordMap.get(c.id) | number) }}
</span>
</td>

View File

@@ -61,7 +61,7 @@
</td>
<td class="text-right">
<a
class="badge badge-primary"
bitBadge
href="{{ cipherDocs.get(c.id) }}"
target="_blank"
rel="noopener"

View File

@@ -65,7 +65,7 @@
<small>{{ c.subTitle }}</small>
</td>
<td class="text-right">
<span class="badge badge-warning">
<span bitBadge badgeType="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(c.login.password) }}
</span>
</td>

View File

@@ -65,7 +65,7 @@
<small>{{ c.subTitle }}</small>
</td>
<td class="text-right">
<span class="badge badge-{{ passwordStrengthMap.get(c.id)[1] }}">
<span bitBadge [badgeType]="passwordStrengthMap.get(c.id)[1]">
{{ passwordStrengthMap.get(c.id)[0] | i18n }}
</span>
</td>

View File

@@ -280,6 +280,20 @@ export class EventService {
this.getShortId(ev.organizationUserId)
);
break;
case EventType.OrganizationUser_Deactivated:
msg = this.i18nService.t("revokedUserId", this.formatOrgUserId(ev));
humanReadableMsg = this.i18nService.t(
"revokedUserId",
this.getShortId(ev.organizationUserId)
);
break;
case EventType.OrganizationUser_Activated:
msg = this.i18nService.t("restoredUserId", this.formatOrgUserId(ev));
humanReadableMsg = this.i18nService.t(
"restoredUserId",
this.getShortId(ev.organizationUserId)
);
break;
// Org
case EventType.Organization_Updated:
msg = humanReadableMsg = this.i18nService.t("editedOrgSettings");

View File

@@ -64,6 +64,17 @@
</div>
</div>
</div>
<div class="form-group">
<label for="masterPasswordHint">{{ "masterPassHintLabel" | i18n }}</label>
<input
id="masterPasswordHint"
class="form-control"
maxlength="50"
type="text"
name="MasterPasswordHint"
[(ngModel)]="masterPasswordHint"
/>
</div>
<div class="form-group">
<div class="form-check">
<input

View File

@@ -1,11 +1,12 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitwarden/angular/components/change-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
@@ -35,6 +36,7 @@ import { UpdateKeyRequest } from "@bitwarden/common/models/request/updateKeyRequ
export class ChangePasswordComponent extends BaseChangePasswordComponent {
rotateEncKey = false;
currentMasterPassword: string;
masterPasswordHint: string;
constructor(
i18nService: I18nService,
@@ -68,6 +70,8 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
if (await this.keyConnectorService.getUsesKeyConnector()) {
this.router.navigate(["/settings/security/two-factor"]);
}
this.masterPasswordHint = (await this.apiService.getProfile()).masterPasswordHint;
await super.ngOnInit();
}
@@ -155,6 +159,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
this.currentMasterPassword,
null
);
request.masterPasswordHint = this.masterPasswordHint;
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = newEncKey[1].encryptedString;
@@ -192,7 +197,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
request.key = encKey[1].encryptedString;
request.masterPasswordHash = masterPasswordHash;
const folders = await this.folderService.getAllDecrypted();
const folders = await firstValueFrom(this.folderService.folderViews$);
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;
@@ -224,7 +229,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
await this.updateEmergencyAccesses(encKey[0]);
await this.updateAllResetPasswordKeys(encKey[0]);
await this.updateAllResetPasswordKeys(encKey[0], masterPasswordHash);
}
private async updateEmergencyAccesses(encKey: SymmetricCryptoKey) {
@@ -252,7 +257,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
}
}
private async updateAllResetPasswordKeys(encKey: SymmetricCryptoKey) {
private async updateAllResetPasswordKeys(encKey: SymmetricCryptoKey, masterPasswordHash: string) {
const orgs = await this.organizationService.getAll();
for (const org of orgs) {
@@ -270,6 +275,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
// Create/Execute request
const request = new OrganizationUserResetPasswordEnrollmentRequest();
request.masterPasswordHash = masterPasswordHash;
request.resetPasswordKey = encryptedKey.encryptedString;
await this.apiService.putOrganizationUserResetPasswordEnrollment(org.id, org.userId, request);

View File

@@ -46,28 +46,29 @@
<td>
<a href="#" appStopClick (click)="edit(c)">{{ c.email }}</a>
<span
class="badge badge-secondary"
bitBadge
badgeType="secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span class="badge badge-warning" *ngIf="c.status === emergencyAccessStatusType.Accepted">{{
"accepted" | i18n
}}</span>
<span
class="badge badge-warning"
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span
class="badge badge-success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.View">{{
"view" | i18n
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.RecoveryApproved">{{
"emergencyAccessRecoveryApproved" | i18n
}}</span>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.Takeover">{{
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
"takeover" | i18n
}}</span>
@@ -159,29 +160,30 @@
</td>
<td>
<span>{{ c.email }}</span>
<span
class="badge badge-secondary"
*ngIf="c.status === emergencyAccessStatusType.Invited"
>{{ "invited" | i18n }}</span
>
<span class="badge badge-warning" *ngIf="c.status === emergencyAccessStatusType.Accepted">{{
"accepted" | i18n
<span bitBadge *ngIf="c.status === emergencyAccessStatusType.Invited">{{
"invited" | i18n
}}</span>
<span
class="badge badge-warning"
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.Accepted"
>{{ "accepted" | i18n }}</span
>
<span
bitBadge
badgeType="warning"
*ngIf="c.status === emergencyAccessStatusType.RecoveryInitiated"
>{{ "emergencyAccessRecoveryInitiated" | i18n }}</span
>
<span
class="badge badge-success"
bitBadge
badgeType="success"
*ngIf="c.status === emergencyAccessStatusType.RecoveryApproved"
>{{ "emergencyAccessRecoveryApproved" | i18n }}</span
>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.View">{{
"view" | i18n
}}</span>
<span class="badge badge-primary" *ngIf="c.type === emergencyAccessType.Takeover">{{
<span bitBadge *ngIf="c.type === emergencyAccessType.View">{{ "view" | i18n }}</span>
<span bitBadge *ngIf="c.type === emergencyAccessType.Takeover">{{
"takeover" | i18n
}}</span>

View File

@@ -4,7 +4,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";

View File

@@ -24,68 +24,20 @@
</ng-container>
<form
#form
[formGroup]="formGroup"
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="!loading && !selfHosted && this.plans"
class="tw-pt-6"
>
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
<div class="row" *ngIf="createOrganization">
<div class="form-group col-6">
<label for="name">{{ "organizationName" | i18n }}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required />
</div>
<div class="form-group col-6">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="billingEmail"
required
/>
</div>
<div class="form-group col-6" *ngIf="!!providerId">
<label for="email">{{ "clientOwnerEmail" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="clientOwnerEmail"
required
/>
<small class="text-muted">{{ "clientOwnerDesc" | i18n: "20" }}</small>
</div>
</div>
<div *ngIf="!providerId && !acceptingSponsorship">
<div class="form-group form-check">
<input
id="ownedBusiness"
class="form-check-input"
type="checkbox"
name="OwnedBusiness"
[(ngModel)]="ownedBusiness"
(change)="changedOwnedBusiness()"
/>
<label for="ownedBusiness" class="form-check-label">{{
"accountOwnedBusiness" | i18n
}}</label>
</div>
<div class="row" *ngIf="ownedBusiness">
<div class="form-group col-6">
<label for="businessName">{{ "businessName" | i18n }}</label>
<input
id="businessName"
class="form-control"
type="text"
name="BusinessName"
[(ngModel)]="businessName"
/>
</div>
</div>
</div>
<app-org-info
(changedBusinessOwned)="changedOwnedBusiness()"
[formGroup]="formGroup"
[createOrganization]="createOrganization"
[isProvider]="!!providerId"
[acceptingSponsorship]="acceptingSponsorship"
></app-org-info>
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2>
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
<input
@@ -94,7 +46,7 @@
name="product"
id="product{{ selectableProduct.product }}"
[value]="selectableProduct.product"
[(ngModel)]="product"
formControlName="product"
(change)="changedProduct()"
/>
<label class="form-check-label" for="product{{ selectableProduct.product }}">
@@ -167,7 +119,7 @@
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
</label>
</div>
<div *ngIf="product !== productTypes.Free">
<div *ngIf="formGroup.controls['product'].value !== productTypes.Free">
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
<h2 class="mt-5">{{ "users" | i18n }}</h2>
<div class="row">
@@ -177,10 +129,8 @@
id="additionalSeats"
class="form-control"
type="number"
name="AdditionalSeats"
[(ngModel)]="additionalSeats"
min="1"
max="100000"
name="additionalSeats"
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
required
/>
@@ -196,10 +146,8 @@
id="additionalSeats"
class="form-control"
type="number"
name="AdditionalSeats"
[(ngModel)]="additionalSeats"
min="0"
max="100000"
name="additionalSeats"
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
@@ -215,10 +163,8 @@
id="additionalStorage"
class="form-control"
type="number"
name="AdditionalStorageGb"
[(ngModel)]="additionalStorage"
min="0"
max="99"
name="additionalStorageGb"
formControlName="additionalStorage"
step="1"
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
/>
@@ -238,8 +184,8 @@
id="premiumAccess"
class="form-check-input"
type="checkbox"
name="PremiumAccessAddon"
[(ngModel)]="premiumAccessAddon"
name="premiumAccessAddon"
formControlName="premiumAccessAddon"
/>
<label for="premiumAccess" class="form-check-label bold">{{
"premiumAccess" | i18n
@@ -255,10 +201,10 @@
<input
class="form-check-input"
type="radio"
name="BillingInterval"
name="plan"
id="interval{{ selectablePlan.type }}"
[value]="selectablePlan.type"
[(ngModel)]="plan"
formControlName="plan"
/>
<label class="form-check-label" for="interval{{ selectablePlan.type }}">
<ng-container *ngIf="selectablePlan.isAnnual">
@@ -281,14 +227,15 @@
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{ "additionalUsers" | i18n }}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
{{ additionalSeats || 0 }} &times;
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ selectablePlan.seatPrice / 12 | currency: "$" }} &times; 12
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency: "$" }} /{{
"year" | i18n
}}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} &times;
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ selectablePlan.additionalStoragePricePerGb / 12 | currency: "$" }} &times; 12
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
@@ -314,18 +261,23 @@
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.baseSeats">{{ "additionalUsers" | i18n }}:</span>
<span *ngIf="!selectablePlan.baseSeats">{{ "users" | i18n }}:</span>
{{ additionalSeats || 0 }} &times; {{ selectablePlan.seatPrice | currency: "$" }}
{{ "monthAbbr" | i18n }} = {{ seatTotal(selectablePlan) | currency: "$" }} /{{
"month" | i18n
}}
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{ selectablePlan.seatPrice | currency: "$" }} {{ "monthAbbr" | i18n }} =
{{ seatTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
</small>
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} &times;
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ selectablePlan.additionalStoragePricePerGb | currency: "$" }}
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
</small>
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
<small
*ngIf="
selectablePlan.hasPremiumAccessOption &&
formGroup.controls['premiumAccessAddon'].value
"
>
{{ "premiumAccess" | i18n }}:
{{ selectablePlan.premiumAccessOptionCost | currency: "$" }} {{ "monthAbbr" | i18n }} =
{{ 40 | currency: "$" }}
@@ -366,10 +318,9 @@
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
<bit-submit-button [loading]="form.loading" [disabled]="!formGroup.valid">{{
"submit" | i18n
}}</bit-submit-button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{ "cancel" | i18n }}
</button>

View File

@@ -1,4 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -42,22 +43,29 @@ export class OrganizationPlansComponent implements OnInit {
@Input() providerId: string;
@Output() onSuccess = new EventEmitter();
@Output() onCanceled = new EventEmitter();
@Output() onTrialBillingSuccess = new EventEmitter();
loading = true;
selfHosted = false;
ownedBusiness = false;
premiumAccessAddon = false;
additionalStorage = 0;
additionalSeats = 0;
name: string;
billingEmail: string;
clientOwnerEmail: string;
businessName: string;
productTypes = ProductType;
formPromise: Promise<any>;
singleOrgPolicyBlock = false;
isInTrialFlow = false;
discount = 0;
formGroup = this.formBuilder.group({
name: [""],
billingEmail: ["", [Validators.email]],
businessOwned: [false],
premiumAccessAddon: [false],
additionalStorage: [0, [Validators.min(0), Validators.max(99)]],
additionalSeats: [0, [Validators.min(0), Validators.max(100000)]],
clientOwnerEmail: ["", [Validators.email]],
businessName: [""],
plan: [this.plan],
product: [this.product],
});
plans: PlanResponse[];
constructor(
@@ -70,7 +78,8 @@ export class OrganizationPlansComponent implements OnInit {
private policyService: PolicyService,
private organizationService: OrganizationService,
private logService: LogService,
private messagingService: MessagingService
private messagingService: MessagingService,
private formBuilder: FormBuilder
) {
this.selfHosted = platformUtilsService.isSelfHost();
}
@@ -80,15 +89,25 @@ export class OrganizationPlansComponent implements OnInit {
const plans = await this.apiService.getPlans();
this.plans = plans.data;
if (this.product === ProductType.Enterprise || this.product === ProductType.Teams) {
this.ownedBusiness = true;
this.formGroup.controls.businessOwned.setValue(true);
}
}
if (this.providerId) {
this.ownedBusiness = true;
this.formGroup.controls.businessOwned.setValue(true);
this.changedOwnedBusiness();
}
if (!this.createOrganization || this.acceptingSponsorship) {
this.formGroup.controls.product.setValue(ProductType.Families);
this.changedProduct();
}
if (this.createOrganization) {
this.formGroup.controls.name.addValidators(Validators.required);
this.formGroup.controls.billingEmail.addValidators(Validators.required);
}
this.loading = false;
}
@@ -97,7 +116,7 @@ export class OrganizationPlansComponent implements OnInit {
}
get selectedPlan() {
return this.plans.find((plan) => plan.type === this.plan);
return this.plans.find((plan) => plan.type === this.formGroup.controls.plan.value);
}
get selectedPlanInterval() {
@@ -107,7 +126,7 @@ export class OrganizationPlansComponent implements OnInit {
get selectableProducts() {
let validPlans = this.plans.filter((plan) => plan.type !== PlanType.Custom);
if (this.ownedBusiness) {
if (this.formGroup.controls.businessOwned.value) {
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
}
@@ -132,8 +151,9 @@ export class OrganizationPlansComponent implements OnInit {
}
get selectablePlans() {
return this.plans.filter(
(plan) => !plan.legacyYear && !plan.disabled && plan.product === this.product
return this.plans?.filter(
(plan) =>
!plan.legacyYear && !plan.disabled && plan.product === this.formGroup.controls.product.value
);
}
@@ -156,7 +176,10 @@ export class OrganizationPlansComponent implements OnInit {
return 0;
}
return plan.additionalStoragePricePerGb * Math.abs(this.additionalStorage || 0);
return (
plan.additionalStoragePricePerGb *
Math.abs(this.formGroup.controls.additionalStorage.value || 0)
);
}
seatTotal(plan: PlanResponse): number {
@@ -164,18 +187,27 @@ export class OrganizationPlansComponent implements OnInit {
return 0;
}
return plan.seatPrice * Math.abs(this.additionalSeats || 0);
return plan.seatPrice * Math.abs(this.formGroup.controls.additionalSeats.value || 0);
}
get subtotal() {
let subTotal = this.selectedPlan.basePrice;
if (this.selectedPlan.hasAdditionalSeatsOption && this.additionalSeats) {
if (
this.selectedPlan.hasAdditionalSeatsOption &&
this.formGroup.controls.additionalSeats.value
) {
subTotal += this.seatTotal(this.selectedPlan);
}
if (this.selectedPlan.hasAdditionalStorageOption && this.additionalStorage) {
if (
this.selectedPlan.hasAdditionalStorageOption &&
this.formGroup.controls.additionalStorage.value
) {
subTotal += this.additionalStorageTotal(this.selectedPlan);
}
if (this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon) {
if (
this.selectedPlan.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value
) {
subTotal += this.selectedPlan.premiumAccessOptionPrice;
}
return subTotal - this.discount;
@@ -206,30 +238,31 @@ export class OrganizationPlansComponent implements OnInit {
}
changedProduct() {
this.plan = this.selectablePlans[0].type;
this.formGroup.controls.plan.setValue(this.selectablePlans[0].type);
if (!this.selectedPlan.hasPremiumAccessOption) {
this.premiumAccessAddon = false;
this.formGroup.controls.premiumAccessAddon.setValue(false);
}
if (!this.selectedPlan.hasAdditionalStorageOption) {
this.additionalStorage = 0;
this.formGroup.controls.additionalStorage.setValue(0);
}
if (!this.selectedPlan.hasAdditionalSeatsOption) {
this.additionalSeats = 0;
this.formGroup.controls.additionalSeats.setValue(0);
} else if (
!this.additionalSeats &&
!this.formGroup.controls.additionalSeats.value &&
!this.selectedPlan.baseSeats &&
this.selectedPlan.hasAdditionalSeatsOption
) {
this.additionalSeats = 1;
this.formGroup.controls.additionalSeats.setValue(1);
}
}
changedOwnedBusiness() {
if (!this.ownedBusiness || this.selectedPlan.canBeUsedByBusiness) {
if (!this.formGroup.controls.businessOwned.value || this.selectedPlan.canBeUsedByBusiness) {
return;
}
this.product = ProductType.Teams;
this.plan = PlanType.TeamsAnnually;
this.formGroup.controls.product.setValue(ProductType.Teams);
this.formGroup.controls.plan.setValue(PlanType.TeamsAnnually);
this.changedProduct();
}
changedCountry() {
@@ -290,10 +323,18 @@ export class OrganizationPlansComponent implements OnInit {
await this.apiService.refreshIdentityToken();
await this.syncService.fullSync(true);
if (!this.acceptingSponsorship) {
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
this.router.navigate(["/organizations/" + orgId]);
}
if (this.isInTrialFlow) {
this.onTrialBillingSuccess.emit({
orgId: orgId,
subLabelText: this.billingSubLabelText(),
});
}
return orgId;
};
@@ -312,11 +353,13 @@ export class OrganizationPlansComponent implements OnInit {
private async updateOrganization(orgId: string) {
const request = new OrganizationUpgradeRequest();
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.businessName = this.formGroup.controls.businessOwned.value
? this.formGroup.controls.businessName.value
: null;
request.additionalSeats = this.formGroup.controls.additionalSeats.value;
request.additionalStorageGb = this.formGroup.controls.additionalStorage.value;
request.premiumAccessAddon =
this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon;
this.selectedPlan.hasPremiumAccessOption && this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
@@ -345,8 +388,8 @@ export class OrganizationPlansComponent implements OnInit {
const request = new OrganizationCreateRequest();
request.key = key;
request.collectionName = collectionCt;
request.name = this.name;
request.billingEmail = this.billingEmail;
request.name = this.formGroup.controls.name.value;
request.billingEmail = this.formGroup.controls.billingEmail.value;
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
if (this.selectedPlan.type === PlanType.Free) {
@@ -356,11 +399,14 @@ export class OrganizationPlansComponent implements OnInit {
request.paymentToken = tokenResult[0];
request.paymentMethodType = tokenResult[1];
request.businessName = this.ownedBusiness ? this.businessName : null;
request.additionalSeats = this.additionalSeats;
request.additionalStorageGb = this.additionalStorage;
request.businessName = this.formGroup.controls.businessOwned.value
? this.formGroup.controls.businessName.value
: null;
request.additionalSeats = this.formGroup.controls.additionalSeats.value;
request.additionalStorageGb = this.formGroup.controls.additionalStorage.value;
request.premiumAccessAddon =
this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon;
this.selectedPlan.hasPremiumAccessOption &&
this.formGroup.controls.premiumAccessAddon.value;
request.planType = this.selectedPlan.type;
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
request.billingAddressCountry = this.taxComponent.taxInfo.country;
@@ -374,7 +420,10 @@ export class OrganizationPlansComponent implements OnInit {
}
if (this.providerId) {
const providerRequest = new ProviderOrganizationCreateRequest(this.clientOwnerEmail, request);
const providerRequest = new ProviderOrganizationCreateRequest(
this.formGroup.controls.clientOwnerEmail.value,
request
);
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
providerRequest.organizationCreateRequest.key = (
await this.cryptoService.encrypt(orgKey.key, providerKey)
@@ -409,4 +458,18 @@ export class OrganizationPlansComponent implements OnInit {
return orgId;
}
private billingSubLabelText(): string {
const selectedPlan = this.selectedPlan;
const price = selectedPlan.basePrice === 0 ? selectedPlan.seatPrice : selectedPlan.basePrice;
let text = "";
if (selectedPlan.isAnnual) {
text += `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`;
} else {
text += `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`;
}
return text;
}
}

View File

@@ -58,11 +58,11 @@
</div>
<ng-container *ngIf="showMethods && method === paymentMethodType.Card">
<div class="row">
<div class="form-group col-4">
<div [ngClass]="trialFlow ? 'col-5' : 'col-4'" class="form-group">
<label for="stripe-card-number-element">{{ "number" | i18n }}</label>
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
</div>
<div class="form-group col-8 d-flex align-items-end">
<div *ngIf="!trialFlow" class="form-group col-8 d-flex align-items-end">
<img
src="../../images/cards.png"
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
@@ -70,7 +70,7 @@
height="32"
/>
</div>
<div class="form-group col-4">
<div [ngClass]="trialFlow ? 'col-3' : 'col-4'" class="form-group">
<label for="stripe-card-expiry-element">{{ "expiration" | i18n }}</label>
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
</div>

View File

@@ -25,6 +25,7 @@ export class PaymentComponent implements OnInit, OnDestroy {
@Input() hideBank = false;
@Input() hidePaypal = false;
@Input() hideCredit = false;
@Input() trialFlow = false;
private destroy$: Subject<void> = new Subject<void>();

View File

@@ -30,16 +30,6 @@
readonly
/>
</div>
<div class="form-group" *ngIf="!hidePasswordHint">
<label for="masterPasswordHint">{{ "masterPassHintLabel" | i18n }}</label>
<input
id="masterPasswordHint"
class="form-control"
type="text"
name="MasterPasswordHint"
[(ngModel)]="profile.masterPasswordHint"
/>
</div>
</div>
<div class="col-6">
<div class="mb-3">

View File

@@ -18,7 +18,6 @@ export class ProfileComponent implements OnInit {
loading = true;
profile: ProfileResponse;
fingerprint: string;
hidePasswordHint = false;
formPromise: Promise<any>;
@@ -41,7 +40,6 @@ export class ProfileComponent implements OnInit {
if (fingerprint != null) {
this.fingerprint = fingerprint.join("-");
}
this.hidePasswordHint = await this.keyConnectorService.getUsesKeyConnector();
}
async submit() {

View File

@@ -1,5 +1,6 @@
import { formatDate } from "@angular/common";
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@@ -23,6 +24,8 @@ export class SponsoringOrgRowComponent implements OnInit {
revokeSponsorshipPromise: Promise<any>;
resendEmailPromise: Promise<any>;
private locale = "";
constructor(
private apiService: ApiService,
private i18nService: I18nService,
@@ -30,7 +33,9 @@ export class SponsoringOrgRowComponent implements OnInit {
private platformUtilsService: PlatformUtilsService
) {}
ngOnInit(): void {
async ngOnInit() {
this.locale = await firstValueFrom(this.i18nService.locale$);
this.setStatus(
this.isSelfHosted,
this.sponsoringOrg.familySponsorshipToDelete,
@@ -98,7 +103,7 @@ export class SponsoringOrgRowComponent implements OnInit {
// They want to delete but there is a valid until date which means there is an active sponsorship
this.statusMessage = this.i18nService.t(
"revokeWhenExpired",
formatDate(validUntil, "MM/dd/yyyy", this.i18nService.locale)
formatDate(validUntil, "MM/dd/yyyy", this.locale)
);
this.statusClass = "text-danger";
} else if (toDelete) {

View File

@@ -1,5 +1,5 @@
<div class="row">
<div class="col-6">
<div [ngClass]="trialFlow ? 'col-7' : 'col-6'">
<div class="form-group">
<label for="addressCountry">{{ "country" | i18n }}</label>
<select
@@ -265,7 +265,7 @@
</select>
</div>
</div>
<div class="col-3">
<div [ngClass]="trialFlow ? 'col-5' : 'col-3'">
<div class="form-group">
<label for="addressPostalCode">{{ "zipPostalCode" | i18n }}</label>
<input

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Output } from "@angular/core";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -12,6 +12,7 @@ import { TaxRateResponse } from "@bitwarden/common/models/response/taxRateRespon
templateUrl: "tax-info.component.html",
})
export class TaxInfoComponent {
@Input() trialFlow = false;
@Output() onCountryChanged = new EventEmitter();
loading = true;

View File

@@ -1,9 +1,10 @@
import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
@@ -81,7 +82,7 @@ export class UpdateKeyComponent {
await this.syncService.fullSync(true);
const folders = await this.folderService.getAllDecrypted();
const folders = await firstValueFrom(this.folderService.folderViews$);
for (let i = 0; i < folders.length; i++) {
if (folders[i].id == null) {
continue;

View File

@@ -7,7 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ImportService as ImportServiceAbstraction } from "@bitwarden/common/abstractions/import.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";

View File

@@ -60,7 +60,7 @@
class="form-control"
[disabled]="cipher.isDeleted || viewOnly"
>
<option *ngFor="let f of folders" [ngValue]="f.id">{{ f.name }}</option>
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
</select>
</div>
</div>
@@ -194,7 +194,9 @@
<a
href="#"
appStopClick
class="badge badge-primary ml-3"
bitBadge
badgeType="primary"
class="tw-ml-4"
(click)="upgradeOrganization()"
*ngIf="
(organization && !organization.useTotp) ||

View File

@@ -5,7 +5,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { EventService } from "@bitwarden/common/abstractions/event.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";

View File

@@ -19,7 +19,7 @@
<div class="form-group">
<label for="folder">{{ "folder" | i18n }}</label>
<select id="folder" name="FolderId" [(ngModel)]="folderId" class="form-control">
<option *ngFor="let f of folders" [ngValue]="f.id">{{ f.name }}</option>
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
</select>
</div>
</div>

View File

@@ -1,7 +1,8 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { FolderView } from "@bitwarden/common/models/view/folderView";
@@ -15,7 +16,7 @@ export class BulkMoveComponent implements OnInit {
@Output() onMoved = new EventEmitter();
folderId: string = null;
folders: FolderView[] = [];
folders$: Observable<FolderView[]>;
formPromise: Promise<any>;
constructor(
@@ -26,8 +27,8 @@ export class BulkMoveComponent implements OnInit {
) {}
async ngOnInit() {
this.folders = await this.folderService.getAllDecrypted();
this.folderId = this.folders[0].id;
this.folders$ = this.folderService.folderViews$;
this.folderId = (await firstValueFrom(this.folders$))[0].id;
}
async submit() {

View File

@@ -1,7 +1,8 @@
import { Component } from "@angular/core";
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/components/folder-add-edit.component";
import { FolderService } from "@bitwarden/common/abstractions/folder.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@@ -13,10 +14,11 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
export class FolderAddEditComponent extends BaseFolderAddEditComponent {
constructor(
folderService: FolderService,
folderApiService: FolderApiServiceAbstraction,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(folderService, i18nService, platformUtilsService, logService);
super(folderService, folderApiService, i18nService, platformUtilsService, logService);
}
}

View File

@@ -572,6 +572,9 @@
"createAccount": {
"message": "Skep rekening"
},
"startTrial": {
"message": "Start Trial"
},
"logIn": {
"message": "Teken aan"
},
@@ -593,6 +596,9 @@
"masterPassDesc": {
"message": "Die hoofwagwoord is die wagwoord wat u gaan gebruik vir toegang tot u kluis. Dit is baie belangrik dat u u hoofwagwoord onthou. Daar is geen manier om dit terug te kry ingeval u dit vergeet het nie."
},
"masterPassImportant": {
"message": "Master passwords cannot be recovered if you forget it!"
},
"masterPassHintDesc": {
"message": "n Hoofwagwoordwenk kan u help om u wagwoord te onthou, sou u dit vergeet."
},
@@ -623,11 +629,14 @@
"invalidEmail": {
"message": "Ongeldige e-posadres."
},
"masterPassRequired": {
"message": "Hoofwagwoord word benodig."
"masterPasswordRequired": {
"message": "Master password is required."
},
"masterPassLength": {
"message": "Hoofwagwoord moet ten minste 8 karakters lank wees."
"confirmMasterPasswordRequired": {
"message": "Master password retype is required."
},
"masterPasswordMinLength": {
"message": "Master password must be at least 8 characters long."
},
"masterPassDoesntMatch": {
"message": "Hoofwagwoordbevestiging stem nie ooreen nie."
@@ -3159,8 +3168,8 @@
"acceptPolicies": {
"message": "Deur hierdie kassie af te merk stem u in tot die volgende:"
},
"acceptPoliciesError": {
"message": "Gebruiksvoorwaardes en privaatheidsbeleid is nie erken nie."
"acceptPoliciesRequired": {
"message": "Terms of Service and Privacy Policy have not been acknowledged."
},
"termsOfService": {
"message": "Gebruiksvoorwaardes"
@@ -3209,6 +3218,9 @@
"organizationIsDisabled": {
"message": "Organisasie is gedeaktiveer."
},
"disabledOrganizationFilterError": {
"message": "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
},
"licenseIsExpired": {
"message": "Lisensie het verstryk."
},
@@ -3333,14 +3345,20 @@
"clone": {
"message": "Kloon"
},
"masterPassPolicyTitle": {
"message": "Master password requirements"
},
"masterPassPolicyDesc": {
"message": "Stel minimum vereistes vir hoofwagwoordsterkte."
},
"twoStepLoginPolicyTitle": {
"message": "Require two-step login"
},
"twoStepLoginPolicyDesc": {
"message": "Vereis tweestapaantekening op gebruikers se persoonlike rekeninge."
},
"twoStepLoginPolicyWarning": {
"message": "Organization members who are not Owners or Administrators and do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change."
"message": "Organization members who are not Owners or Administrators and do not have two-step login turned on for their account will be removed from the organization and will receive an email notifying them about the change."
},
"twoStepLoginPolicyUserWarning": {
"message": "You are a member of an organization that requires two-step login to be enabled on your user account. If you disable all two-step login providers you will be automatically removed from these organizations."
@@ -3567,7 +3585,7 @@
"message": "Enkele organisasie"
},
"singleOrgDesc": {
"message": "Restrict users from being able to join any other organizations."
"message": "Restrict members from joining other organizations."
},
"singleOrgBlockCreateMessage": {
"message": "U huidige organisasie het n beleid wat u nie toelaat om deel te neem aan meer as een organisasie nie. Kontak u organisasie se beheerders of teken aan metn ander Bitwarden-rekening."
@@ -3579,13 +3597,13 @@
"message": "Enkelaantekenwaarmerking"
},
"requireSsoPolicyDesc": {
"message": "Require users to log in with the Enterprise Single Sign-On method."
"message": "Require members to log in with the Enterprise Single Sign-On method."
},
"prerequisite": {
"message": "Voorvereiste"
},
"requireSsoPolicyReq": {
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
"message": "The Single Organization enterprise policy must be turned on before activating this policy."
},
"requireSsoPolicyReqError": {
"message": "Single Organization policy not enabled."
@@ -3883,7 +3901,7 @@
"message": "Persoonlike eienaarskap"
},
"personalOwnershipPolicyDesc": {
"message": "Require users to save vault items to an organization by removing the personal ownership option."
"message": "Require members to save items to an organization by removing the individual vault option."
},
"personalOwnershipExemption": {
"message": "Organisasie-eienaars en -administrateurs is vrygestel van die afdwing van hierdie beleid."
@@ -3895,7 +3913,7 @@
"message": "Deaktiveer Send"
},
"disableSendPolicyDesc": {
"message": "Do not allow users to create or edit a Bitwarden Send. Deleting an existing Send is still allowed.",
"message": "Do not allow members to create or edit sends.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"disableSendExemption": {
@@ -3921,7 +3939,7 @@
"message": "Organization users that can manage the organization's policies are exempt from this policy's enforcement."
},
"disableHideEmail": {
"message": "Do not allow users to hide their email address from recipients when creating or editing a Send.",
"message": "Always show members email address with recipients when creating or editing a send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendOptionsPolicyInEffect": {
@@ -4099,7 +4117,7 @@
}
},
"viewSendHiddenEmailWarning": {
"message": "The Bitwarden user who created this Send has chosen to hide their email address. You should ensure you trust the source of this link before using or downloading its content.",
"message": "Die Bitwarden-gebruiker wat hierdie Send geskep het, het gekies om hul e-posadres te verberg. U moet verseker dat u die bron van hierdie skakel vertrou voordat u die inhoud gebruik of aflaai.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"expirationDateIsInvalid": {
@@ -4220,7 +4238,7 @@
"message": "Bestuur wagwoordherstel"
},
"resetPasswordPolicyDescription": {
"message": "Allow administrators in the organization to reset organization users' master password."
"message": "Allow admins to reset master passwords for members."
},
"resetPasswordPolicyWarning": {
"message": "Gebruikers in die organisasie sal self moet inskryf of moet outomaties ingeskryf word voor beheerders hul hoofwagwoord sal kan terugstel."
@@ -4467,7 +4485,7 @@
"message": "Kluis-uittel"
},
"maximumVaultTimeoutDesc": {
"message": "Configure a maximum vault timeout for all users."
"message": "Set a maximum vault timeout for members."
},
"maximumVaultTimeoutLabel": {
"message": "Maximum Vault Timeout"
@@ -4507,10 +4525,10 @@
"message": "Vault Timeout is not within allowed range."
},
"disablePersonalVaultExport": {
"message": "Disable Personal Vault Export"
"message": "Deaktiveer uitstuur van persoonlike kluis"
},
"disablePersonalVaultExportDesc": {
"message": "Prohibits users from exporting their private vault data."
"message": "Verbied gebruikers om hul privaatkluisdata uit te stuur."
},
"vaultExportDisabled": {
"message": "Kluisuitstuur gedeaktiveer"
@@ -4802,15 +4820,15 @@
},
"ssoPolicyHelpStart": {
"message": "Aktiveer die",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpLink": {
"message": "SSO-waarmerkbeleid",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpEnd": {
"message": "om aantekening met SSO vir alle lede te verplig.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpKeyConnector": {
"message": "SSO-waarmerking en Enkelorganisasiebeleide word vereis om Key Connector-dekripsie op te stel."
@@ -5165,5 +5183,35 @@
"example": "My Email"
}
}
},
"inputRequired": {
"message": "Input is required."
},
"inputEmail": {
"message": "Input is not an email-address."
},
"inputMinLength": {
"message": "Input must be at least $COUNT$ characters long.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
}
}
},
"fieldsNeedAttention": {
"message": "$COUNT$ veld(e) hierbo benodig u aandag.",
"placeholders": {
"count": {
"content": "$1",
"example": "4"
}
}
},
"turnOn": {
"message": "Turn on"
},
"on": {
"message": "On"
}
}

View File

@@ -572,6 +572,9 @@
"createAccount": {
"message": "إنشاء حساب"
},
"startTrial": {
"message": "Start Trial"
},
"logIn": {
"message": "تسجيل الدخول"
},
@@ -593,6 +596,9 @@
"masterPassDesc": {
"message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it."
},
"masterPassImportant": {
"message": "Master passwords cannot be recovered if you forget it!"
},
"masterPassHintDesc": {
"message": "يمكن أن يساعدك تلميح كلمة المرور الرئيسية في تذكر كلمة المرور الخاصة بك في حال نسيتها."
},
@@ -623,11 +629,14 @@
"invalidEmail": {
"message": "عنوان البريد الإلكتروني غير صالح."
},
"masterPassRequired": {
"message": "كلمة المرور الرئيسية مطلوبة."
"masterPasswordRequired": {
"message": "Master password is required."
},
"masterPassLength": {
"message": "يجب أن يكون طول كلمة المرور الرئيسية 8 أحرف على الأقل."
"confirmMasterPasswordRequired": {
"message": "Master password retype is required."
},
"masterPasswordMinLength": {
"message": "Master password must be at least 8 characters long."
},
"masterPassDoesntMatch": {
"message": "لا يتطابق تأكيد كلمة المرور مع كلمة المرور."
@@ -1067,7 +1076,7 @@
"message": "حذف الحساب"
},
"deleteAccountDesc": {
"message": "Proceed below to delete your account and all associated data."
"message": "Proceed below to delete your account and all vault data."
},
"deleteAccountWarning": {
"message": "Deleting your account is permanent. It cannot be undone."
@@ -1076,7 +1085,7 @@
"message": "Account Deleted"
},
"accountDeletedDesc": {
"message": "Your account has been closed and all associated data has been deleted."
"message": "Your Bitwarden account and vault data were permanently deleted."
},
"myAccount": {
"message": "حسابي"
@@ -1214,7 +1223,7 @@
"message": "Domains updated"
},
"twoStepLogin": {
"message": "Two-step Login"
"message": "Two-step login"
},
"twoStepLoginDesc": {
"message": "Secure your account by requiring an additional step when logging in."
@@ -3159,7 +3168,7 @@
"acceptPolicies": {
"message": "By checking this box you agree to the following:"
},
"acceptPoliciesError": {
"acceptPoliciesRequired": {
"message": "Terms of Service and Privacy Policy have not been acknowledged."
},
"termsOfService": {
@@ -3209,6 +3218,9 @@
"organizationIsDisabled": {
"message": "Organization is disabled."
},
"disabledOrganizationFilterError": {
"message": "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
},
"licenseIsExpired": {
"message": "License is expired."
},
@@ -3333,20 +3345,26 @@
"clone": {
"message": "استنساخ"
},
"masterPassPolicyTitle": {
"message": "Master password requirements"
},
"masterPassPolicyDesc": {
"message": "Set minimum requirements for master password strength."
"message": "Set requirements for master password strength."
},
"twoStepLoginPolicyTitle": {
"message": "Require two-step login"
},
"twoStepLoginPolicyDesc": {
"message": "Require users to set up two-step login on their personal accounts."
"message": "Require members to set up two-step login."
},
"twoStepLoginPolicyWarning": {
"message": "Organization members who are not Owners or Administrators and do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change."
"message": "Organization members who are not Owners or Administrators and do not have two-step login turned on for their account will be removed from the organization and will receive an email notifying them about the change."
},
"twoStepLoginPolicyUserWarning": {
"message": "You are a member of an organization that requires two-step login to be enabled on your user account. If you disable all two-step login providers you will be automatically removed from these organizations."
},
"passwordGeneratorPolicyDesc": {
"message": "Set minimum requirements for password generator configuration."
"message": "Set requirements for password generator."
},
"passwordGeneratorPolicyInEffect": {
"message": "One or more organization policies are affecting your generator settings."
@@ -3564,10 +3582,10 @@
"message": "Link SSO"
},
"singleOrg": {
"message": "Single Organization"
"message": "Single organization"
},
"singleOrgDesc": {
"message": "Restrict users from being able to join any other organizations."
"message": "Restrict members from joining other organizations."
},
"singleOrgBlockCreateMessage": {
"message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account."
@@ -3576,16 +3594,16 @@
"message": "Organization members who are not Owners or Administrators and are already a member of another organization will be removed from your organization."
},
"requireSso": {
"message": "Single Sign-On Authentication"
"message": "Require single sign-on authentication"
},
"requireSsoPolicyDesc": {
"message": "Require users to log in with the Enterprise Single Sign-On method."
"message": "Require members to log in with the Enterprise Single Sign-On method."
},
"prerequisite": {
"message": "Prerequisite"
},
"requireSsoPolicyReq": {
"message": "The Single Organization enterprise policy must be enabled before activating this policy."
"message": "The Single Organization enterprise policy must be turned on before activating this policy."
},
"requireSsoPolicyReqError": {
"message": "Single Organization policy not enabled."
@@ -3880,10 +3898,10 @@
}
},
"personalOwnership": {
"message": "Personal Ownership"
"message": "Remove individual vault"
},
"personalOwnershipPolicyDesc": {
"message": "Require users to save vault items to an organization by removing the personal ownership option."
"message": "Require members to save items to an organization by removing the individual vault option."
},
"personalOwnershipExemption": {
"message": "Organization Owners and Administrators are exempt from this policy's enforcement."
@@ -3892,10 +3910,10 @@
"message": "Due to an enterprise policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available Collections."
},
"disableSend": {
"message": "Disable Send"
"message": "Remove Send"
},
"disableSendPolicyDesc": {
"message": "Do not allow users to create or edit a Bitwarden Send. Deleting an existing Send is still allowed.",
"message": "Do not allow members to create or edit sends.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"disableSendExemption": {
@@ -3910,7 +3928,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendOptions": {
"message": "Send Options",
"message": "Send options",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendOptionsPolicyDesc": {
@@ -3921,7 +3939,7 @@
"message": "Organization users that can manage the organization's policies are exempt from this policy's enforcement."
},
"disableHideEmail": {
"message": "Do not allow users to hide their email address from recipients when creating or editing a Send.",
"message": "Always show members email address with recipients when creating or editing a send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendOptionsPolicyInEffect": {
@@ -4217,10 +4235,10 @@
"message": "Enrollment will allow organization administrators to change your master password"
},
"resetPasswordPolicy": {
"message": "Master Password Reset"
"message": "Master password reset"
},
"resetPasswordPolicyDescription": {
"message": "Allow administrators in the organization to reset organization users' master password."
"message": "Allow admins to reset master passwords for members."
},
"resetPasswordPolicyWarning": {
"message": "Users in the organization will need to self-enroll or be auto-enrolled before administrators can reset their master password."
@@ -4464,10 +4482,10 @@
"message": "Your Master Password does not meet the policy requirements of this organization. In order to join the organization, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"maximumVaultTimeout": {
"message": "Vault Timeout"
"message": "Vault timeout"
},
"maximumVaultTimeoutDesc": {
"message": "Configure a maximum vault timeout for all users."
"message": "Set a maximum vault timeout for members."
},
"maximumVaultTimeoutLabel": {
"message": "Maximum Vault Timeout"
@@ -4507,10 +4525,10 @@
"message": "Vault Timeout is not within allowed range."
},
"disablePersonalVaultExport": {
"message": "Disable Personal Vault Export"
"message": "Remove individual vault export"
},
"disablePersonalVaultExportDesc": {
"message": "Prohibits users from exporting their private vault data."
"message": "Do not allow members to export their individual vault data."
},
"vaultExportDisabled": {
"message": "Vault Export Disabled"
@@ -4801,19 +4819,19 @@
"message": "Once set up, your configuration will be saved and members will be able to authenticate using their Identity Provider credentials."
},
"ssoPolicyHelpStart": {
"message": "Enable the",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"message": "Use the",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpLink": {
"message": "SSO Authentication policy",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"message": "require single-sign-on authentication policy",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpEnd": {
"message": "to require all members to log in with SSO.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpKeyConnector": {
"message": "SSO Authentication and Single Organization policies are required to set up Key Connector decryption."
"message": "The require SSO authentication and single organization policies are required to set up Key Connector decryption."
},
"memberDecryptionOption": {
"message": "Member Decryption Options"
@@ -5165,5 +5183,35 @@
"example": "My Email"
}
}
},
"inputRequired": {
"message": "Input is required."
},
"inputEmail": {
"message": "Input is not an email-address."
},
"inputMinLength": {
"message": "Input must be at least $COUNT$ characters long.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
}
}
},
"fieldsNeedAttention": {
"message": "$COUNT$ field(s) above need your attention.",
"placeholders": {
"count": {
"content": "$1",
"example": "4"
}
}
},
"turnOn": {
"message": "Turn on"
},
"on": {
"message": "On"
}
}

View File

@@ -572,6 +572,9 @@
"createAccount": {
"message": "Hesab yarat"
},
"startTrial": {
"message": "Sınağa Başla"
},
"logIn": {
"message": "Giriş et"
},
@@ -593,6 +596,9 @@
"masterPassDesc": {
"message": "Ana parol, anbarınıza müraciət etmək üçün istifadə edəcəyiniz şifrədir. Ana parolu yadda saxlamaq çox vacibdir. Unutsanız, parolu bərpa etməyin heç bir yolu yoxdur."
},
"masterPassImportant": {
"message": "Unutsanız, Ana parollar bərpa edilə bilməz!"
},
"masterPassHintDesc": {
"message": "Ana parol məsləhəti, unutduğunuz parolu xatırlamağınıza kömək edir."
},
@@ -623,10 +629,13 @@
"invalidEmail": {
"message": "Etibarsız e-poçt ünvanı."
},
"masterPassRequired": {
"masterPasswordRequired": {
"message": "Ana parol lazımdır."
},
"masterPassLength": {
"confirmMasterPasswordRequired": {
"message": "Ana parolun yenidən yazılması lazımdır."
},
"masterPasswordMinLength": {
"message": "Ana parol ən azı 8 simvol uzunluğunda olmalıdır."
},
"masterPassDoesntMatch": {
@@ -3159,7 +3168,7 @@
"acceptPolicies": {
"message": "Bu qutunu işarələyərək aşağıdakılarla razılaşırsınız:"
},
"acceptPoliciesError": {
"acceptPoliciesRequired": {
"message": "Xidmət Şərtləri və Gizlilik Siyasəti qəbul edilməyib."
},
"termsOfService": {
@@ -3209,6 +3218,9 @@
"organizationIsDisabled": {
"message": "Təşkilat sıradan çıxarıldı."
},
"disabledOrganizationFilterError": {
"message": "Sıradan çıxarılmış Təşkilatlardakı elementlərə müraciət edilə bilmir. Kömək üçün Təşkilatınızın sahibi ilə əlaqə saxlayın."
},
"licenseIsExpired": {
"message": "Lisenziyanın vaxtı bitib."
},
@@ -3333,9 +3345,15 @@
"clone": {
"message": "Klonla"
},
"masterPassPolicyTitle": {
"message": "Ana parol tələbləri"
},
"masterPassPolicyDesc": {
"message": "Ana parol gücü üçün minimum tələbləri tənzimlə."
},
"twoStepLoginPolicyTitle": {
"message": "İki mərhələli girişi tələb et"
},
"twoStepLoginPolicyDesc": {
"message": "İstifadəçilərin fərdi hesablarında \"iki mərhələli giriş\"i istifadə etmələrini məcburi et."
},
@@ -4802,15 +4820,15 @@
},
"ssoPolicyHelpStart": {
"message": "Bütün üzvlərin SSO ilə",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpLink": {
"message": "giriş etməsini məcburi etmək üçün",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpEnd": {
"message": "SSO Siyasətini fəallaşdırın.",
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enable the SSO Authentication policy to require all members to log in with SSO.'"
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'"
},
"ssoPolicyHelpKeyConnector": {
"message": "Açar Bağlayıcı şifrə açmanı quraşdırmaq üçün SSO Kimlik Təsdiqləmə və Tək Təşkilat siyasətləri tələb olunur."
@@ -5165,5 +5183,35 @@
"example": "My Email"
}
}
},
"inputRequired": {
"message": "Giriş lazımdır."
},
"inputEmail": {
"message": "Giriş, bir e-poçt ünvanı deyil."
},
"inputMinLength": {
"message": "Giriş, ən azı $COUNT$ simvol uzunluğunda olmalıdır.",
"placeholders": {
"count": {
"content": "$1",
"example": "8"
}
}
},
"fieldsNeedAttention": {
"message": "Yuxarıdakı $COUNT$ sahənin diqqətinizə ehtiyacı var.",
"placeholders": {
"count": {
"content": "$1",
"example": "4"
}
}
},
"turnOn": {
"message": "İşə sal"
},
"on": {
"message": "Açıqdır"
}
}

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