1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +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

@@ -9,5 +9,6 @@
"dev": {
"port": 8080,
"allowedHosts": "auto"
}
},
"flags": {}
}

View File

@@ -1,7 +1,8 @@
{
"urls": {
"icons": "https://icons.bitwarden.net",
"notifications": "https://notifications.bitwarden.com"
"notifications": "https://notifications.bitwarden.com",
"scim": "https://scim.bitwarden.com"
},
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
@@ -13,5 +14,9 @@
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com"
},
"flags": {
"showTrial": false,
"scim": false
}
}

View File

@@ -1,11 +1,16 @@
{
"urls": {
"notifications": "http://localhost:61840"
"notifications": "http://localhost:61840",
"scim": "http://localhost:44559"
},
"dev": {
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840"
},
"flags": {
"showTrial": true,
"scim": true
}
}

View File

@@ -1,11 +1,16 @@
{
"urls": {
"icons": "https://icons.qa.bitwarden.pw",
"notifications": "https://notifications.qa.bitwarden.pw"
"notifications": "https://notifications.qa.bitwarden.pw",
"scim": "https://scim.qa.bitwarden.pw"
},
"dev": {
"proxyApi": "https://api.qa.bitwarden.pw",
"proxyIdentity": "https://identity.qa.bitwarden.pw",
"proxyEvents": "https://events.qa.bitwarden.pw"
},
"flags": {
"showTrial": true,
"scim": true
}
}

View File

@@ -5,5 +5,9 @@
"proxyEvents": "http://localhost:46274",
"proxyNotifications": "http://localhost:61841",
"port": 8081
},
"flags": {
"showTrial": false,
"scim": false
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2022.6.0",
"version": "2022.6.2",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

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";

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