mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
44 Commits
update-sel
...
v2022.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06227f27ab | ||
|
|
66391b4376 | ||
|
|
1c09629bbc | ||
|
|
e2d9498167 | ||
|
|
ec80782d8f | ||
|
|
3acee89646 | ||
|
|
589faf3608 | ||
|
|
639cc461ae | ||
|
|
4562920d30 | ||
|
|
c6f4e4de64 | ||
|
|
f323963a4e | ||
|
|
f6c8b6d8cb | ||
|
|
11f34be18e | ||
|
|
647e9555d0 | ||
|
|
ad7b6e6388 | ||
|
|
718ca197b5 | ||
|
|
338d593cd1 | ||
|
|
aa29a07678 | ||
|
|
0722522801 | ||
|
|
5731c6449b | ||
|
|
eaf083be44 | ||
|
|
8435c85c3c | ||
|
|
3d1c2dad74 | ||
|
|
2f632f7096 | ||
|
|
9b63c81b56 | ||
|
|
34cb463e94 | ||
|
|
7bbe3d7b28 | ||
|
|
0564b52f1f | ||
|
|
cb1f08d74b | ||
|
|
f3bf972d26 | ||
|
|
02256aad87 | ||
|
|
466e0c8c35 | ||
|
|
0543e76432 | ||
|
|
34527160fc | ||
|
|
f590fe5435 | ||
|
|
0f64c16c18 | ||
|
|
1632a24b1e | ||
|
|
0691af1dcd | ||
|
|
3e59617fb1 | ||
|
|
268b16ca98 | ||
|
|
9f977cfc68 | ||
|
|
9627782a04 | ||
|
|
c5877cd063 | ||
|
|
136e8897ae |
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -19,8 +19,8 @@ jobs:
|
||||
name: Setup
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
release_version: ${{ steps.version.outputs.package }}
|
||||
tag_version: ${{ steps.version.outputs.tag }}
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
tag_version: ${{ steps.version.outputs.version }}
|
||||
branch_name: ${{ steps.branch.outputs.branch_name }}
|
||||
steps:
|
||||
- name: Branch check
|
||||
@@ -38,20 +38,11 @@ jobs:
|
||||
|
||||
- name: Check Release Version
|
||||
id: version
|
||||
run: |
|
||||
version=$( jq -r ".version" package.json)
|
||||
previous_release_tag_version=$(
|
||||
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
|
||||
)
|
||||
|
||||
if [ "v$version" == "$previous_release_tag_version" ] && \
|
||||
[ "${{ github.event.inputs.release_type }}" == "Initial Release" ]; then
|
||||
echo "[!] Already released v$version. Please bump version to continue"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "::set-output name=package::$version"
|
||||
echo "::set-output name=tag::v$version"
|
||||
uses: bitwarden/gh-actions/release-version-check@ea9fab01d76940267b4147cc1c4542431246b9f6
|
||||
with:
|
||||
release-type: ${{ github.event.inputs.release_type }}
|
||||
project-type: ts
|
||||
file: package.json
|
||||
|
||||
- name: Get branch name
|
||||
id: branch
|
||||
|
||||
@@ -22,9 +22,9 @@ const routes: Routes = [
|
||||
component: ManageComponent,
|
||||
canActivate: [PermissionsGuard],
|
||||
data: {
|
||||
permissions: [
|
||||
NavigationPermissionsService.getPermissions("manage").concat(Permissions.ManageSso),
|
||||
],
|
||||
permissions: NavigationPermissionsService.getPermissions("manage").concat(
|
||||
Permissions.ManageSso
|
||||
),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: 00deb38de5...6f117b9901
1304
package-lock.json
generated
1304
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2.28.1",
|
||||
"version": "2022.5.1",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/bitwarden/web",
|
||||
"scripts": {
|
||||
|
||||
@@ -55,7 +55,7 @@ export class LockComponent extends BaseLockComponent {
|
||||
await super.ngOnInit();
|
||||
this.onSuccessfulSubmit = async () => {
|
||||
const previousUrl = this.routerService.getPreviousUrl();
|
||||
if (previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
|
||||
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) {
|
||||
this.successRoute = previousUrl;
|
||||
}
|
||||
this.router.navigateByUrl(this.successRoute);
|
||||
|
||||
@@ -74,7 +74,7 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
if (qParams.premium != null) {
|
||||
this.routerService.setPreviousUrl("/settings/premium");
|
||||
} else if (qParams.org != null) {
|
||||
const route = this.router.createUrlTree(["settings/create-organization"], {
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
|
||||
@@ -71,7 +71,7 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
} else if (qParams.org != null) {
|
||||
this.showCreateOrgMessage = true;
|
||||
this.referenceData.flow = qParams.org;
|
||||
const route = this.router.createUrlTree(["settings/create-organization"], {
|
||||
const route = this.router.createUrlTree(["create-organization"], {
|
||||
queryParams: { plan: qParams.org },
|
||||
});
|
||||
this.routerService.setPreviousUrl(route.toString());
|
||||
|
||||
@@ -122,17 +122,20 @@ export abstract class BaseEventsComponent {
|
||||
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
|
||||
const eventInfo = await this.eventService.getEventInfo(r);
|
||||
const user = this.getUserName(r, userId);
|
||||
const userName = user != null ? user.name : this.i18nService.t("unknown");
|
||||
|
||||
return new EventView({
|
||||
message: eventInfo.message,
|
||||
humanReadableMessage: eventInfo.humanReadableMessage,
|
||||
appIcon: eventInfo.appIcon,
|
||||
appName: eventInfo.appName,
|
||||
userId: userId,
|
||||
userName: user != null ? user.name : this.i18nService.t("unknown"),
|
||||
userName: r.installationId != null ? `Installation: ${r.installationId}` : userName,
|
||||
userEmail: user != null ? user.email : "",
|
||||
date: r.date,
|
||||
ip: r.ipAddress,
|
||||
type: r.type,
|
||||
installationId: r.installationId,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</li>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<li class="tw-list-none" role="none">
|
||||
<a bit-menu-item routerLink="/settings/create-organization">
|
||||
<a bit-menu-item routerLink="/create-organization">
|
||||
<i class="bwi bwi-plus mr-2"></i>
|
||||
{{ "newOrganization" | i18n }}</a
|
||||
>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<li>
|
||||
<button
|
||||
[bitMenuTriggerFor]="accountMenu"
|
||||
class="tw-border-0 tw-bg-transparent tw-text-contrast tw-opacity-70 hover:tw-opacity-90"
|
||||
class="tw-border-0 tw-bg-transparent tw-text-alt2 tw-opacity-70 hover:tw-opacity-90"
|
||||
>
|
||||
<i class="bwi bwi-user-circle bwi-lg" aria-hidden="true"></i>
|
||||
<i class="bwi bwi-caret-down bwi-sm" aria-hidden="true"></i>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, NgZone, OnInit } from "@angular/core";
|
||||
|
||||
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
@@ -31,7 +32,9 @@ export class NavbarComponent implements OnInit {
|
||||
private providerService: ProviderService,
|
||||
private syncService: SyncService,
|
||||
private organizationService: OrganizationService,
|
||||
private i18nService: I18nService
|
||||
private i18nService: I18nService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
@@ -49,8 +52,24 @@ export class NavbarComponent implements OnInit {
|
||||
}
|
||||
this.providers = await this.providerService.getAll();
|
||||
|
||||
this.organizations = await this.buildOrganizations();
|
||||
|
||||
this.broadcasterService.subscribe(this.constructor.name, async (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
case "organizationCreated":
|
||||
if (this.organizations.length < 1) {
|
||||
this.organizations = await this.buildOrganizations();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async buildOrganizations() {
|
||||
const allOrgs = await this.organizationService.getAll();
|
||||
this.organizations = allOrgs
|
||||
return allOrgs
|
||||
.filter((org) => OrgNavigationPermissionsService.canAccessAdmin(org))
|
||||
.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import { SingleOrgPolicyComponent } from "../organizations/policies/single-org.c
|
||||
import { TwoFactorAuthenticationPolicyComponent } from "../organizations/policies/two-factor-authentication.component";
|
||||
import { AccountComponent as OrgAccountComponent } from "../organizations/settings/account.component";
|
||||
import { AdjustSubscription } from "../organizations/settings/adjust-subscription.component";
|
||||
import { BillingSyncApiKeyComponent } from "../organizations/settings/billing-sync-api-key.component";
|
||||
import { ChangePlanComponent } from "../organizations/settings/change-plan.component";
|
||||
import { DeleteOrganizationComponent } from "../organizations/settings/delete-organization.component";
|
||||
import { DownloadLicenseComponent } from "../organizations/settings/download-license.component";
|
||||
@@ -66,6 +67,7 @@ import { OrganizationBillingComponent } from "../organizations/settings/organiza
|
||||
import { OrganizationSubscriptionComponent } from "../organizations/settings/organization-subscription.component";
|
||||
import { SettingsComponent as OrgSettingComponent } from "../organizations/settings/settings.component";
|
||||
import { TwoFactorSetupComponent as OrgTwoFactorSetupComponent } from "../organizations/settings/two-factor-setup.component";
|
||||
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
import { ExportComponent as OrgExportComponent } from "../organizations/tools/export.component";
|
||||
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
|
||||
@@ -98,6 +100,7 @@ import { AddCreditComponent } from "../settings/add-credit.component";
|
||||
import { AdjustPaymentComponent } from "../settings/adjust-payment.component";
|
||||
import { AdjustStorageComponent } from "../settings/adjust-storage.component";
|
||||
import { ApiKeyComponent } from "../settings/api-key.component";
|
||||
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
|
||||
import { ChangeEmailComponent } from "../settings/change-email.component";
|
||||
import { ChangeKdfComponent } from "../settings/change-kdf.component";
|
||||
import { ChangePasswordComponent } from "../settings/change-password.component";
|
||||
@@ -112,7 +115,6 @@ import { EmergencyAccessTakeoverComponent } from "../settings/emergency-access-t
|
||||
import { EmergencyAccessViewComponent } from "../settings/emergency-access-view.component";
|
||||
import { EmergencyAccessComponent } from "../settings/emergency-access.component";
|
||||
import { EmergencyAddEditComponent } from "../settings/emergency-add-edit.component";
|
||||
import { LinkSsoComponent } from "../settings/link-sso.component";
|
||||
import { OrganizationPlansComponent } from "../settings/organization-plans.component";
|
||||
import { PaymentMethodComponent } from "../settings/payment-method.component";
|
||||
import { PaymentComponent } from "../settings/payment.component";
|
||||
@@ -171,6 +173,7 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||
declarations: [
|
||||
PremiumBadgeComponent,
|
||||
AcceptEmergencyComponent,
|
||||
AcceptFamilySponsorshipComponent,
|
||||
AcceptOrganizationComponent,
|
||||
AccessComponent,
|
||||
AccountComponent,
|
||||
@@ -183,6 +186,8 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||
AdjustSubscription,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BillingSyncApiKeyComponent,
|
||||
BillingSyncKeyComponent,
|
||||
BreachReportComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
@@ -218,7 +223,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||
HintComponent,
|
||||
ImportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
LinkSsoComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
@@ -379,7 +383,6 @@ import { OrganizationBadgeModule } from "./vault/modules/organization-badge/orga
|
||||
HintComponent,
|
||||
ImportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
LinkSsoComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
<h3 class="filter-title">{{ collectionsGrouping.name | i18n }}</h3>
|
||||
<h3 class="filter-title"> {{ collectionsGrouping.name | i18n }}</h3>
|
||||
</div>
|
||||
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
|
||||
<ng-template #recursiveCollections let-collections>
|
||||
@@ -51,7 +51,7 @@
|
||||
class="bwi bwi-collection bwi-fw"
|
||||
aria-hidden="true"
|
||||
></i
|
||||
>{{ c.node.name }}
|
||||
> {{ c.node.name }}
|
||||
</button>
|
||||
</span>
|
||||
<ul
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="!hide && !activeFilter.selectedOrganizationId">
|
||||
<ng-container *ngIf="!hide">
|
||||
<div class="filter-heading">
|
||||
<button
|
||||
class="toggle-button"
|
||||
@@ -16,9 +16,7 @@
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
<h3 class="filter-title">
|
||||
{{ "folders" | i18n }}
|
||||
</h3>
|
||||
<h3 class="filter-title"> {{ "folders" | i18n }}</h3>
|
||||
<button
|
||||
class="text-muted ml-auto add-button"
|
||||
(click)="addFolder()"
|
||||
@@ -56,7 +54,7 @@
|
||||
</button>
|
||||
<button class="filter-button" (click)="applyFilter(f.node)">
|
||||
<i *ngIf="f.children.length === 0" class="bwi bwi-fw bwi-folder" aria-hidden="true"></i
|
||||
>{{ f.node.name }}
|
||||
> {{ f.node.name }}
|
||||
</button>
|
||||
<button
|
||||
class="edit-button"
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
</li>
|
||||
<li class="filter-option">
|
||||
<span class="filter-buttons">
|
||||
<a href="#" routerLink="/settings/create-organization" class="filter-button">
|
||||
<a href="#" routerLink="/create-organization" class="filter-button">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newOrganization" | i18n }}
|
||||
{{ "newOrganization" | i18n }}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
@@ -45,14 +45,6 @@
|
||||
>
|
||||
{{ organizationGrouping.name | i18n }}
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
routerLink="/settings/create-organization"
|
||||
class="text-muted ml-auto create-organization-link"
|
||||
appA11yTitle="{{ 'addOrganization' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||
<li
|
||||
@@ -75,19 +67,25 @@
|
||||
</ng-container>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
|
||||
<ul class="filter-options">
|
||||
<li class="filter-option active">
|
||||
<button class="filter-button">
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organizations[0].name }}
|
||||
</button>
|
||||
<li class="filter-option">
|
||||
<span class="filter-buttons">
|
||||
<a href="#" routerLink="/create-organization" class="filter-button">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newOrganization" | i18n }}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'organizationMember'">
|
||||
<ng-container *ngSwitchCase="'singleOrganizationAndPersonalOwnershipPolicies'">
|
||||
<div class="filter-heading">
|
||||
<button class="filter-button active">
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organizations[0].name }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<div class="filter-heading">
|
||||
<button
|
||||
class="toggle-button"
|
||||
@@ -112,14 +110,6 @@
|
||||
>
|
||||
{{ organizationGrouping.name | i18n }}
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
routerLink="/settings/create-organization"
|
||||
class="text-muted ml-auto create-organization-link"
|
||||
appA11yTitle="{{ 'addOrganization' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
|
||||
@@ -140,7 +130,7 @@
|
||||
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
|
||||
{{ organization.name }}
|
||||
</button>
|
||||
<ng-container *ngIf="organization.id === activeFilter.selectedOrganizationId">
|
||||
<ng-container>
|
||||
<button [bitMenuTriggerFor]="orgMenu" class="org-options ml-auto">
|
||||
<i class="bwi bwi-ellipsis-v" aria-hidden="true"></i>
|
||||
</button>
|
||||
@@ -150,6 +140,14 @@
|
||||
</ng-container>
|
||||
</span>
|
||||
</li>
|
||||
<li class="filter-option" *ngIf="!(displayMode === 'singleOrganizationPolicy')">
|
||||
<span class="filter-buttons">
|
||||
<a href="#" routerLink="/create-organization" class="filter-button">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newOrganization" | i18n }}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
@@ -35,7 +35,6 @@ export class OrganizationOptionsComponent {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.syncService.fullSync(true);
|
||||
await this.load();
|
||||
}
|
||||
|
||||
@@ -83,6 +82,7 @@ export class OrganizationOptionsComponent {
|
||||
this.platformUtilsService.showToast("success", null, "Unlinked SSO");
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,7 @@ export class OrganizationOptionsComponent {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization"));
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
@@ -174,6 +175,7 @@ export class OrganizationOptionsComponent {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t(toastStringRef));
|
||||
await this.load();
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message);
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
}"
|
||||
></i>
|
||||
</button>
|
||||
<h3>
|
||||
{{ "types" | i18n }}
|
||||
</h3>
|
||||
<h3> {{ "types" | i18n }}</h3>
|
||||
</div>
|
||||
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||
<li
|
||||
@@ -26,14 +24,14 @@
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Login)">
|
||||
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>{{ "typeLogin" | i18n }}
|
||||
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i> {{ "typeLogin" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
|
||||
<span class="filter-buttons">
|
||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Card)">
|
||||
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>{{ "typeCard" | i18n }}
|
||||
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i> {{ "typeCard" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
@@ -43,7 +41,7 @@
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Identity)">
|
||||
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>{{ "typeIdentity" | i18n }}
|
||||
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i> {{ "typeIdentity" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
@@ -53,7 +51,9 @@
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)">
|
||||
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>{{ "typeSecureNote" | i18n }}
|
||||
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i> {{
|
||||
"typeSecureNote" | i18n
|
||||
}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
})
|
||||
export class OrganizationVaultFilterComponent extends VaultFilterComponent {
|
||||
hideOrganizations = true;
|
||||
hideFavorites = true;
|
||||
hideFolders = true;
|
||||
|
||||
organization: Organization;
|
||||
|
||||
async initCollections() {
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
return await this.vaultFilterService.buildAdminCollections(this.organization.id);
|
||||
}
|
||||
return await this.vaultFilterService.buildCollections(this.organization.id);
|
||||
}
|
||||
|
||||
async reloadCollectionsAndFolders() {
|
||||
this.collections = await this.initCollections();
|
||||
}
|
||||
}
|
||||
@@ -26,22 +26,19 @@
|
||||
autocomplete="off"
|
||||
appAutofocus
|
||||
/>
|
||||
<div class="filter">
|
||||
<app-organization-filter
|
||||
*ngIf="showOrgFilter"
|
||||
[hide]="hideOrganizations"
|
||||
[activeFilter]="activeFilter"
|
||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||
[organizations]="organizations"
|
||||
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
|
||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
></app-organization-filter>
|
||||
</div>
|
||||
<app-organization-filter
|
||||
[hide]="hideOrganizations"
|
||||
[activeFilter]="activeFilter"
|
||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||
[organizations]="organizations"
|
||||
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
|
||||
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
|
||||
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
></app-organization-filter>
|
||||
<div class="filter">
|
||||
<app-status-filter
|
||||
[hideFavorites]="!showFavorites"
|
||||
[hideFavorites]="hideFavorites"
|
||||
[hideTrash]="hideTrash"
|
||||
[activeFilter]="activeFilter"
|
||||
(onFilterChange)="applyFilter($event)"
|
||||
@@ -57,7 +54,7 @@
|
||||
</div>
|
||||
<div class="filter">
|
||||
<app-folder-filter
|
||||
[hide]="!showFolders"
|
||||
[hide]="hideFolders"
|
||||
[activeFilter]="activeFilter"
|
||||
[collapsedFilterNodes]="collapsedFilterNodes"
|
||||
[folderNodes]="folders"
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "jslib-angular/modules/vault-filter/vault-filter.component";
|
||||
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-filter",
|
||||
templateUrl: "vault-filter.component.html",
|
||||
})
|
||||
export class VaultFilterComponent extends BaseVaultFilterComponent {
|
||||
@Input() showOrgFilter = true;
|
||||
@Input() showFolders = true;
|
||||
@Input() showFavorites = true;
|
||||
|
||||
@Output() onSearchTextChanged = new EventEmitter<string>();
|
||||
|
||||
searchPlaceholder: string;
|
||||
searchText = "";
|
||||
|
||||
organization: Organization;
|
||||
|
||||
constructor(vaultFilterService: VaultFilterService) {
|
||||
constructor(protected vaultFilterService: VaultFilterService) {
|
||||
// This empty constructor is required to provide the web vaultFilterService subclass to super()
|
||||
// TODO: refactor this to use proper dependency injection
|
||||
super(vaultFilterService);
|
||||
}
|
||||
|
||||
@@ -32,9 +28,9 @@ export class VaultFilterComponent extends BaseVaultFilterComponent {
|
||||
// It should be removed as soon as doing so makes sense.
|
||||
async reloadOrganizations() {
|
||||
this.organizations = await this.vaultFilterService.buildOrganizations();
|
||||
}
|
||||
|
||||
async initCollections() {
|
||||
return await this.vaultFilterService.buildCollections(this.organization?.id);
|
||||
this.activePersonalOwnershipPolicy =
|
||||
await this.vaultFilterService.checkForPersonalOwnershipPolicy();
|
||||
this.activeSingleOrganizationPolicy =
|
||||
await this.vaultFilterService.checkForSingleOrganizationPolicy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
|
||||
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||
import { CollectionService } from "jslib-common/abstractions/collection.service";
|
||||
import { FolderService } from "jslib-common/abstractions/folder.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
import { SharedModule } from "../shared.module";
|
||||
|
||||
import { CollectionFilterComponent } from "./components/collection-filter.component";
|
||||
import { FolderFilterComponent } from "./components/folder-filter.component";
|
||||
import { LinkSsoComponent } from "./components/link-sso.component";
|
||||
import { OrganizationFilterComponent } from "./components/organization-filter.component";
|
||||
import { OrganizationOptionsComponent } from "./components/organization-options.component";
|
||||
import { StatusFilterComponent } from "./components/status-filter.component";
|
||||
import { TypeFilterComponent } from "./components/type-filter.component";
|
||||
import { OrganizationVaultFilterComponent } from "./organization-vault-filter.component";
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
@@ -28,21 +23,10 @@ import { VaultFilterComponent } from "./vault-filter.component";
|
||||
OrganizationOptionsComponent,
|
||||
StatusFilterComponent,
|
||||
TypeFilterComponent,
|
||||
OrganizationVaultFilterComponent,
|
||||
LinkSsoComponent,
|
||||
],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultFilterService,
|
||||
useClass: VaultFilterService,
|
||||
deps: [
|
||||
StateService,
|
||||
OrganizationService,
|
||||
FolderService,
|
||||
CipherService,
|
||||
CollectionService,
|
||||
PolicyService,
|
||||
],
|
||||
},
|
||||
],
|
||||
exports: [VaultFilterComponent, OrganizationVaultFilterComponent],
|
||||
providers: [VaultFilterService],
|
||||
})
|
||||
export class VaultFilterModule {}
|
||||
|
||||
@@ -1,3 +1,54 @@
|
||||
import { VaultFilterService as BaseVaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
export class VaultFilterService extends BaseVaultFilterService {}
|
||||
import { DynamicTreeNode } from "jslib-angular/modules/vault-filter/models/dynamic-tree-node.model";
|
||||
import { VaultFilterService as BaseVaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { CipherService } from "jslib-common/abstractions/cipher.service";
|
||||
import { CollectionService } from "jslib-common/abstractions/collection.service";
|
||||
import { FolderService } from "jslib-common/abstractions/folder.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { CollectionData } from "jslib-common/models/data/collectionData";
|
||||
import { Collection } from "jslib-common/models/domain/collection";
|
||||
import { CollectionDetailsResponse } from "jslib-common/models/response/collectionResponse";
|
||||
import { CollectionView } from "jslib-common/models/view/collectionView";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService extends BaseVaultFilterService {
|
||||
constructor(
|
||||
stateService: StateService,
|
||||
organizationService: OrganizationService,
|
||||
folderService: FolderService,
|
||||
cipherService: CipherService,
|
||||
collectionService: CollectionService,
|
||||
policyService: PolicyService,
|
||||
protected apiService: ApiService
|
||||
) {
|
||||
super(
|
||||
stateService,
|
||||
organizationService,
|
||||
folderService,
|
||||
cipherService,
|
||||
collectionService,
|
||||
policyService
|
||||
);
|
||||
}
|
||||
|
||||
async buildAdminCollections(organizationId: string) {
|
||||
let result: CollectionView[] = [];
|
||||
const collectionResponse = await this.apiService.getCollections(organizationId);
|
||||
if (collectionResponse?.data != null && collectionResponse.data.length) {
|
||||
const collectionDomains = collectionResponse.data.map(
|
||||
(r: CollectionDetailsResponse) => new Collection(new CollectionData(r))
|
||||
);
|
||||
result = await this.collectionService.decryptMany(collectionDomains);
|
||||
}
|
||||
|
||||
const nestedCollections = await this.collectionService.getAllNested(result);
|
||||
return new DynamicTreeNode<CollectionView>({
|
||||
fullList: result,
|
||||
nestedList: nestedCollections,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,19 +32,26 @@
|
||||
</small>
|
||||
</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [deleted]="deleted">
|
||||
<app-vault-bulk-actions
|
||||
[ciphersComponent]="ciphersComponent"
|
||||
[deleted]="activeFilter.status === 'trash'"
|
||||
>
|
||||
</app-vault-bulk-actions>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
(click)="addCipher()"
|
||||
*ngIf="!deleted"
|
||||
*ngIf="activeFilter.status !== 'trash'"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-callout type="warning" *ngIf="deleted" icon="bwi-exclamation-triangle">
|
||||
<app-callout
|
||||
type="warning"
|
||||
*ngIf="activeFilter.status === 'trash'"
|
||||
icon="bwi-exclamation-triangle"
|
||||
>
|
||||
{{ trashCleanupWarning }}
|
||||
</app-callout>
|
||||
<app-vault-ciphers
|
||||
@@ -95,7 +102,10 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||
<a class="btn btn-block btn-outline-secondary" routerLink="/settings/premium">
|
||||
<a
|
||||
class="btn btn-block btn-outline-secondary"
|
||||
routerLink="/settings/subscription/premium"
|
||||
>
|
||||
{{ "goPremium" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -209,7 +209,7 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
|
||||
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
|
||||
}
|
||||
if (
|
||||
this.activeFilter.selectedFolderId != null &&
|
||||
this.activeFilter.selectedFolder &&
|
||||
this.activeFilter.selectedFolderId != "none" &&
|
||||
cipherPassesFilter
|
||||
) {
|
||||
@@ -352,7 +352,7 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
async editCipherId(id: string) {
|
||||
const cipher = await this.cipherService.get(id);
|
||||
if (cipher.reprompt != 0) {
|
||||
if (cipher != null && cipher.reprompt != 0) {
|
||||
if (!(await this.passwordRepromptService.showPasswordPrompt())) {
|
||||
this.go({ cipherId: null });
|
||||
return;
|
||||
|
||||
@@ -4,15 +4,12 @@
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-vault-filter
|
||||
<app-organization-vault-filter
|
||||
#vaultFilter
|
||||
[showFolders]="false"
|
||||
[showFavorites]="false"
|
||||
[activeFilter]="activeFilter"
|
||||
[showOrgFilter]="false"
|
||||
(onFilterChange)="applyVaultFilter($event)"
|
||||
(onSearchTextChanged)="filterSearchText($event)"
|
||||
></app-vault-filter>
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ import { AddEditComponent } from "../../../../organizations/vault/add-edit.compo
|
||||
import { AttachmentsComponent } from "../../../../organizations/vault/attachments.component";
|
||||
import { CiphersComponent } from "../../../../organizations/vault/ciphers.component";
|
||||
import { CollectionsComponent } from "../../../../organizations/vault/collections.component";
|
||||
import { VaultFilterComponent } from "../../../vault-filter/vault-filter.component";
|
||||
import { OrganizationVaultFilterComponent } from "../../../vault-filter/organization-vault-filter.component";
|
||||
import { VaultService } from "../../vault.service";
|
||||
|
||||
const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||
@@ -39,7 +39,8 @@ const BroadcasterSubscriptionId = "OrgVaultComponent";
|
||||
templateUrl: "organization-vault.component.html",
|
||||
})
|
||||
export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("vaultFilter", { static: true }) vaultFilterComponent: VaultFilterComponent;
|
||||
@ViewChild("vaultFilter", { static: true })
|
||||
vaultFilterComponent: OrganizationVaultFilterComponent;
|
||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
@@ -57,6 +58,11 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
trashCleanupWarning: string = null;
|
||||
activeFilter: VaultFilter = new VaultFilter();
|
||||
|
||||
// This is a hack to avoid redundant api calls that fetch OrganizationVaultFilterComponent collections
|
||||
// When it makes sense to do so we should leverage some other communication method for change events that isn't directly tied to the query param for organizationId
|
||||
// i.e. exposing the VaultFiltersService to the OrganizationSwitcherComponent to make relevant updates from a change event instead of just depending on the router
|
||||
firstLoaded = true;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
@@ -86,7 +92,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
// this.ciphersComponent.searchText = this.vaultFilterComponent.search = qParams.search;
|
||||
this.ciphersComponent.searchText = this.vaultFilterComponent.searchText = qParams.search;
|
||||
if (!this.organization.canViewAllCollections) {
|
||||
await this.syncService.fullSync(false);
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
@@ -95,11 +101,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
case "syncCompleted":
|
||||
if (message.successfully) {
|
||||
await Promise.all([
|
||||
this.vaultFilterComponent.reloadCollectionsAndFolders(
|
||||
new VaultFilter({
|
||||
selectedOrganizationId: this.organization.id,
|
||||
} as Partial<VaultFilter>)
|
||||
),
|
||||
this.vaultFilterComponent.reloadCollectionsAndFolders(),
|
||||
this.ciphersComponent.refresh(),
|
||||
]);
|
||||
this.changeDetectorRef.detectChanges();
|
||||
@@ -109,9 +111,12 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
}
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders(
|
||||
new VaultFilter({ selectedOrganizationId: this.organization.id } as Partial<VaultFilter>)
|
||||
);
|
||||
|
||||
if (!this.firstLoaded) {
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders();
|
||||
}
|
||||
this.firstLoaded = false;
|
||||
|
||||
await this.ciphersComponent.reload();
|
||||
|
||||
if (qParams.viewEvents != null) {
|
||||
@@ -123,7 +128,11 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.route.queryParams.subscribe(async (params) => {
|
||||
if (params.cipherId) {
|
||||
if ((await this.cipherService.get(params.cipherId)) != null) {
|
||||
if (
|
||||
// Handle users with implicit collection access since they use the admin endpoint
|
||||
this.organization.canEditAnyCollection ||
|
||||
(await this.cipherService.get(params.cipherId)) != null
|
||||
) {
|
||||
this.editCipherId(params.cipherId);
|
||||
} else {
|
||||
this.platformUtilsService.showToast(
|
||||
@@ -168,7 +177,7 @@ export class OrganizationVaultComponent implements OnInit, OnDestroy {
|
||||
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
|
||||
}
|
||||
if (
|
||||
this.activeFilter.selectedFolderId != null &&
|
||||
this.activeFilter.selectedFolder != null &&
|
||||
this.activeFilter.selectedFolderId != "none" &&
|
||||
cipherPassesFilter
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
@@ -17,7 +17,7 @@ export class PermissionsGuard implements CanActivate {
|
||||
private syncService: SyncService
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot) {
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
// TODO: We need to fix this issue once and for all.
|
||||
if ((await this.syncService.getLastSync()) == null) {
|
||||
await this.syncService.fullSync(false);
|
||||
@@ -39,6 +39,16 @@ export class PermissionsGuard implements CanActivate {
|
||||
|
||||
const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]);
|
||||
if (permissions != null && !org.hasAnyPermission(permissions)) {
|
||||
// Handle linkable ciphers for organizations the user only has view access to
|
||||
// https://bitwarden.atlassian.net/browse/EC-203
|
||||
if (state.root.queryParamMap.has("cipherId")) {
|
||||
return this.router.createUrlTree(["/vault"], {
|
||||
queryParams: {
|
||||
cipherId: state.root.queryParamMap.get("cipherId"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
|
||||
return this.router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<app-navbar></app-navbar>
|
||||
<div class="org-nav" *ngIf="organization">
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
@@ -35,3 +36,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
<app-footer></app-footer>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncApiKeyTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="billingSyncApiKeyTitle">
|
||||
{{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<app-user-verification
|
||||
[(ngModel)]="masterPassword"
|
||||
ngDefaultControl
|
||||
name="secret"
|
||||
*ngIf="!clientSecret"
|
||||
>
|
||||
</app-user-verification>
|
||||
<ng-container *ngIf="clientSecret && showRotateScreen">
|
||||
<p>{{ "rotateBillingSyncTokenTitle" | i18n }}</p>
|
||||
<app-callout type="warning">
|
||||
{{ "rotateBillingSyncTokenWarning" | i18n }}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="clientSecret && !showRotateScreen">
|
||||
<p>{{ "copyPasteBillingSync" | i18n }}</p>
|
||||
<label for="clientSecret">Billing Sync Key</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="clientSecret"
|
||||
class="form-control text-monospace"
|
||||
type="text"
|
||||
[(ngModel)]="clientSecret"
|
||||
name="clientSecret"
|
||||
disabled
|
||||
/>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="copy()"
|
||||
[appA11yTitle]="'copy' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted mt-2" *ngIf="showLastSyncText">
|
||||
<b class="font-weight-semibold">{{ "lastSync" | i18n }}:</b>
|
||||
{{ lastSyncDate | date: "medium" }}
|
||||
</div>
|
||||
<div class="small text-danger mt-2" *ngIf="showAwaitingSyncText">
|
||||
<i class="bwi bwi-error"></i>
|
||||
{{
|
||||
(daysBetween === 1 ? "awaitingSyncSingular" : "awaitingSyncPlural")
|
||||
| i18n: daysBetween
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="form.loading"
|
||||
*ngIf="!clientSecret || showRotateScreen"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="form.loading"
|
||||
></i>
|
||||
<span>
|
||||
{{ submitButtonText }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
data-dismiss="modal"
|
||||
*ngIf="!showRotateScreen"
|
||||
>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
*ngIf="showRotateScreen"
|
||||
(click)="cancelRotate()"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
*ngIf="clientSecret && !showRotateScreen"
|
||||
(click)="rotateToken()"
|
||||
>
|
||||
{{ "rotateToken" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
108
src/app/organizations/settings/billing-sync-api-key.component.ts
Normal file
108
src/app/organizations/settings/billing-sync-api-key.component.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { UserVerificationService } from "jslib-common/abstractions/userVerification.service";
|
||||
import { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
|
||||
import { OrganizationApiKeyRequest } from "jslib-common/models/request/organizationApiKeyRequest";
|
||||
import { ApiKeyResponse } from "jslib-common/models/response/apiKeyResponse";
|
||||
import { Verification } from "jslib-common/types/verification";
|
||||
|
||||
@Component({
|
||||
selector: "app-billing-sync-api-key",
|
||||
templateUrl: "billing-sync-api-key.component.html",
|
||||
})
|
||||
export class BillingSyncApiKeyComponent {
|
||||
organizationId: string;
|
||||
hasBillingToken: boolean;
|
||||
|
||||
showRotateScreen: boolean;
|
||||
masterPassword: Verification;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientSecret?: string;
|
||||
keyRevisionDate?: Date;
|
||||
lastSyncDate?: Date = null;
|
||||
|
||||
constructor(
|
||||
private userVerificationService: UserVerificationService,
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService
|
||||
) {}
|
||||
|
||||
copy() {
|
||||
this.platformUtilsService.copyToClipboard(this.clientSecret);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.showRotateScreen) {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
|
||||
.then((request) => {
|
||||
request.type = OrganizationApiKeyType.BillingSync;
|
||||
return this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
|
||||
});
|
||||
const response = await this.formPromise;
|
||||
await this.load(response);
|
||||
this.showRotateScreen = false;
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("billingSyncApiKeyRotated")
|
||||
);
|
||||
} else {
|
||||
this.formPromise = this.userVerificationService
|
||||
.buildRequest(this.masterPassword, OrganizationApiKeyRequest)
|
||||
.then((request) => {
|
||||
request.type = OrganizationApiKeyType.BillingSync;
|
||||
return this.apiService.postOrganizationApiKey(this.organizationId, request);
|
||||
});
|
||||
const response = await this.formPromise;
|
||||
await this.load(response);
|
||||
}
|
||||
}
|
||||
|
||||
async load(response: ApiKeyResponse) {
|
||||
this.clientSecret = response.apiKey;
|
||||
this.keyRevisionDate = response.revisionDate;
|
||||
this.hasBillingToken = true;
|
||||
const syncStatus = await this.apiService.getSponsorshipSyncStatus(this.organizationId);
|
||||
this.lastSyncDate = syncStatus.lastSyncDate;
|
||||
}
|
||||
|
||||
cancelRotate() {
|
||||
this.showRotateScreen = false;
|
||||
}
|
||||
|
||||
rotateToken() {
|
||||
this.showRotateScreen = true;
|
||||
}
|
||||
|
||||
private dayDiff(date1: Date, date2: Date): number {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.round(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
get submitButtonText(): string {
|
||||
if (this.showRotateScreen) {
|
||||
return this.i18nService.t("rotateToken");
|
||||
}
|
||||
|
||||
return this.i18nService.t(this.hasBillingToken ? "continue" : "generateToken");
|
||||
}
|
||||
|
||||
get showLastSyncText(): boolean {
|
||||
// If the keyRevisionDate is later than the lastSyncDate we need to show
|
||||
// a warning that they need to put the billing sync key in their self hosted install
|
||||
return this.lastSyncDate && this.lastSyncDate > this.keyRevisionDate;
|
||||
}
|
||||
|
||||
get showAwaitingSyncText(): boolean {
|
||||
return this.lastSyncDate && this.lastSyncDate <= this.keyRevisionDate;
|
||||
}
|
||||
|
||||
get daysBetween(): number {
|
||||
return this.dayDiff(this.keyRevisionDate, new Date());
|
||||
}
|
||||
}
|
||||
@@ -188,10 +188,10 @@
|
||||
></app-adjust-storage>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
|
||||
<!--Switch to i18n-->
|
||||
<h2 class="spaced-header">{{ "selfHostingTitle" | i18n }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ "additionalOptionsDesc" | i18n }}
|
||||
{{ "selfHostingEnterpriseOrganizationSectionCopy" | i18n }}
|
||||
</p>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
@@ -203,6 +203,27 @@
|
||||
>
|
||||
{{ "downloadLicense" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary ml-1"
|
||||
(click)="manageBillingSync()"
|
||||
*ngIf="canManageBillingSync"
|
||||
>
|
||||
{{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3" *ngIf="showDownloadLicense">
|
||||
<app-download-license
|
||||
[organizationId]="organizationId"
|
||||
(onDownloaded)="closeDownloadLicense()"
|
||||
(onCanceled)="closeDownloadLicense()"
|
||||
></app-download-license>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{ "additionalOptions" | i18n }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ "additionalOptionsDesc" | i18n }}
|
||||
</p>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
#cancelBtn
|
||||
type="button"
|
||||
@@ -216,13 +237,6 @@
|
||||
<span>{{ "cancelSubscription" | i18n }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3" *ngIf="showDownloadLicense">
|
||||
<app-download-license
|
||||
[organizationId]="organizationId"
|
||||
(onDownloaded)="closeDownloadLicense()"
|
||||
(onCanceled)="closeDownloadLicense()"
|
||||
></app-download-license>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<dl>
|
||||
@@ -269,5 +283,31 @@
|
||||
></app-update-license>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showBillingSyncKey">
|
||||
<h2 class="mt-5">
|
||||
{{ "billingSync" | i18n }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ "billingSyncDesc" | i18n }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
(click)="manageBillingSyncSelfHosted()"
|
||||
>
|
||||
{{ "manageBillingSync" | i18n }}
|
||||
</button>
|
||||
<small class="form-text text-muted" *ngIf="billingSyncSetUp">
|
||||
{{ "lastSync" | i18n }}:
|
||||
<span *ngIf="userOrg.familySponsorshipLastSyncDate != null">
|
||||
{{ userOrg.familySponsorshipLastSyncDate | date: "medium" }}
|
||||
</span>
|
||||
<span *ngIf="userOrg.familySponsorshipLastSyncDate == null">
|
||||
{{ "never" | i18n | lowercase }}
|
||||
</span>
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #setupBillingSyncTemplate></ng-template>
|
||||
<ng-template #rotateBillingSyncKeyTemplate></ng-template>
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
|
||||
import { ModalService } from "jslib-angular/services/modal.service";
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { OrganizationApiKeyType } from "jslib-common/enums/organizationApiKeyType";
|
||||
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
|
||||
import { PlanType } from "jslib-common/enums/planType";
|
||||
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
|
||||
import { OrganizationSubscriptionResponse } from "jslib-common/models/response/organizationSubscriptionResponse";
|
||||
|
||||
import { BillingSyncKeyComponent } from "src/app/settings/billing-sync-key.component";
|
||||
|
||||
import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-subscription",
|
||||
templateUrl: "organization-subscription.component.html",
|
||||
})
|
||||
export class OrganizationSubscriptionComponent implements OnInit {
|
||||
@ViewChild("setupBillingSyncTemplate", { read: ViewContainerRef, static: true })
|
||||
setupBillingSyncModalRef: ViewContainerRef;
|
||||
|
||||
loading = false;
|
||||
firstLoaded = false;
|
||||
organizationId: string;
|
||||
@@ -25,17 +38,24 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
adjustStorageAdd = true;
|
||||
showAdjustStorage = false;
|
||||
showUpdateLicense = false;
|
||||
showBillingSyncKey = false;
|
||||
showDownloadLicense = false;
|
||||
showChangePlan = false;
|
||||
sub: OrganizationSubscriptionResponse;
|
||||
selfHosted = false;
|
||||
hasBillingSyncToken: boolean;
|
||||
|
||||
userOrg: Organization;
|
||||
existingBillingSyncConnection: OrganizationConnectionResponse<BillingSyncConfigApi>;
|
||||
|
||||
removeSponsorshipPromise: Promise<any>;
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
|
||||
@ViewChild("rotateBillingSyncKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
billingSyncKeyViewContainerRef: ViewContainerRef;
|
||||
billingSyncKeyRef: [ModalRef, BillingSyncKeyComponent];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -43,7 +63,8 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
private messagingService: MessagingService,
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private logService: LogService
|
||||
private logService: LogService,
|
||||
private modalService: ModalService
|
||||
) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
@@ -63,10 +84,27 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
|
||||
this.loading = true;
|
||||
this.userOrg = await this.organizationService.get(this.organizationId);
|
||||
|
||||
if (this.userOrg.canManageBilling) {
|
||||
this.sub = await this.apiService.getOrganizationSubscription(this.organizationId);
|
||||
}
|
||||
const apiKeyResponse = await this.apiService.getOrganizationApiKeyInformation(
|
||||
this.organizationId
|
||||
);
|
||||
this.hasBillingSyncToken = apiKeyResponse.data.some(
|
||||
(i) => i.keyType === OrganizationApiKeyType.BillingSync
|
||||
);
|
||||
|
||||
if (this.selfHosted) {
|
||||
this.showBillingSyncKey = await this.apiService.getCloudCommunicationsEnabled();
|
||||
}
|
||||
|
||||
if (this.showBillingSyncKey) {
|
||||
this.existingBillingSyncConnection = await this.apiService.getOrganizationConnection(
|
||||
this.organizationId,
|
||||
OrganizationConnectionType.CloudBillingSync,
|
||||
BillingSyncConfigApi
|
||||
);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -138,6 +176,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
this.showDownloadLicense = !this.showDownloadLicense;
|
||||
}
|
||||
|
||||
async manageBillingSync() {
|
||||
const [ref] = await this.modalService.openViewRef(
|
||||
BillingSyncApiKeyComponent,
|
||||
this.setupBillingSyncModalRef,
|
||||
(comp) => {
|
||||
comp.organizationId = this.organizationId;
|
||||
comp.hasBillingToken = this.hasBillingSyncToken;
|
||||
}
|
||||
);
|
||||
ref.onClosed.subscribe(async () => {
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
closeDownloadLicense() {
|
||||
this.showDownloadLicense = false;
|
||||
}
|
||||
@@ -200,6 +252,24 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async manageBillingSyncSelfHosted() {
|
||||
this.billingSyncKeyRef = await this.modalService.openViewRef(
|
||||
BillingSyncKeyComponent,
|
||||
this.billingSyncKeyViewContainerRef,
|
||||
(comp) => {
|
||||
comp.entityId = this.organizationId;
|
||||
comp.existingConnectionId = this.existingBillingSyncConnection?.id;
|
||||
comp.billingSyncKey = this.existingBillingSyncConnection?.config?.billingSyncKey;
|
||||
comp.setParentConnection = (
|
||||
connection: OrganizationConnectionResponse<BillingSyncConfigApi>
|
||||
) => {
|
||||
this.existingBillingSyncConnection = connection;
|
||||
this.billingSyncKeyRef[0].close();
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get isExpired() {
|
||||
return (
|
||||
this.sub != null && this.sub.expiration != null && new Date(this.sub.expiration) < new Date()
|
||||
@@ -266,6 +336,16 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
get canManageBillingSync() {
|
||||
return (
|
||||
!this.selfHosted &&
|
||||
(this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually2019 ||
|
||||
this.sub.planType === PlanType.EnterpriseMonthly2019)
|
||||
);
|
||||
}
|
||||
|
||||
get subscriptionDesc() {
|
||||
if (this.sub.planType === PlanType.Free) {
|
||||
return this.i18nService.t("subscriptionFreePlan", this.sub.seats.toString());
|
||||
@@ -293,4 +373,8 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
get showChangePlanButton() {
|
||||
return this.subscription == null && this.sub.planType === PlanType.Free && !this.showChangePlan;
|
||||
}
|
||||
|
||||
get billingSyncSetUp() {
|
||||
return this.existingBillingSyncConnection?.id != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { CipherView } from "jslib-common/models/view/cipherView";
|
||||
import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent } from "../../reports/exposed-passwords-report.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-exposed-passwords-report",
|
||||
selector: "app-org-exposed-passwords-report",
|
||||
templateUrl: "../../reports/exposed-passwords-report.component.html",
|
||||
})
|
||||
export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent {
|
||||
@@ -41,12 +41,10 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const dynamicSuper = Object.getPrototypeOf(this.constructor.prototype);
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.organizationService.get(params.organizationId);
|
||||
this.manageableCiphers = await this.cipherService.getAll();
|
||||
// TODO: We should do something about this, calling super in an async function is bad
|
||||
dynamicSuper.ngOnInit();
|
||||
await this.checkAccess();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,8 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
);
|
||||
}
|
||||
|
||||
async load(filter: (cipher: CipherView) => boolean = null) {
|
||||
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||
this.deleted = deleted || false;
|
||||
if (this.organization.canEditAnyCollection) {
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
this.allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id);
|
||||
|
||||
@@ -155,6 +155,11 @@ const routes: Routes = [
|
||||
.IndividualVaultModule,
|
||||
},
|
||||
{ path: "sends", component: SendComponent, data: { title: "Send" } },
|
||||
{
|
||||
path: "create-organization",
|
||||
component: CreateOrganizationComponent,
|
||||
data: { titleId: "newOrganization" },
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
component: SettingsComponent,
|
||||
@@ -181,11 +186,6 @@ const routes: Routes = [
|
||||
loadChildren: async () =>
|
||||
(await import("./settings/subscription-routing.module")).SubscriptionRoutingModule,
|
||||
},
|
||||
{
|
||||
path: "create-organization",
|
||||
component: CreateOrganizationComponent,
|
||||
data: { titleId: "newOrganization" },
|
||||
},
|
||||
{
|
||||
path: "emergency-access",
|
||||
children: [
|
||||
@@ -229,15 +229,15 @@ const routes: Routes = [
|
||||
(await import("./reports/reports-routing.module")).ReportsRoutingModule,
|
||||
},
|
||||
{ path: "setup/families-for-enterprise", component: FamiliesForEnterpriseSetupComponent },
|
||||
{
|
||||
path: "organizations",
|
||||
loadChildren: () =>
|
||||
import("./organizations/organization-routing.module").then(
|
||||
(m) => m.OrganizationsRoutingModule
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "organizations",
|
||||
loadChildren: () =>
|
||||
import("./organizations/organization-routing.module").then(
|
||||
(m) => m.OrganizationsRoutingModule
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -307,6 +307,9 @@ export class EventService {
|
||||
case EventType.Organization_DisabledKeyConnector:
|
||||
msg = humanReadableMsg = this.i18nService.t("disabledKeyConnector");
|
||||
break;
|
||||
case EventType.Organization_SponsorshipsSynced:
|
||||
msg = humanReadableMsg = this.i18nService.t("sponsorshipsSynced");
|
||||
break;
|
||||
// Policies
|
||||
case EventType.Policy_Updated: {
|
||||
msg = this.i18nService.t("modifiedPolicyId", this.formatPolicyId(ev));
|
||||
|
||||
69
src/app/settings/billing-sync-key.component.html
Normal file
69
src/app/settings/billing-sync-key.component.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="billingSyncTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<form
|
||||
class="modal-content"
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="billingSyncTitle">{{ "manageBillingSync" | i18n }}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ "billingSyncKeyDesc" | i18n }}</p>
|
||||
<div class="form-group">
|
||||
<label for="billingSyncKey"
|
||||
>{{ "billingSyncKey" | i18n }} <small>(</small><small>{{ "required" | i18n }}</small
|
||||
><small>)</small></label
|
||||
>
|
||||
<input
|
||||
id="billingSyncKey"
|
||||
type="input"
|
||||
name="billingSyncKey"
|
||||
class="form-control"
|
||||
[(ngModel)]="billingSyncKey"
|
||||
required
|
||||
appAutofocus
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<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>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="ml-auto">
|
||||
<button
|
||||
#deleteBtn
|
||||
type="button"
|
||||
(click)="deleteConnection()"
|
||||
class="btn btn-outline-danger"
|
||||
appA11yTitle="{{ 'delete' | i18n }}"
|
||||
[disabled]="form.loading"
|
||||
>
|
||||
<i class="bwi bwi-trash 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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
61
src/app/settings/billing-sync-key.component.ts
Normal file
61
src/app/settings/billing-sync-key.component.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { OrganizationConnectionType } from "jslib-common/enums/organizationConnectionType";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { BillingSyncConfigApi } from "jslib-common/models/api/billingSyncConfigApi";
|
||||
import { BillingSyncConfigRequest } from "jslib-common/models/request/billingSyncConfigRequest";
|
||||
import { OrganizationConnectionRequest } from "jslib-common/models/request/organizationConnectionRequest";
|
||||
import { OrganizationConnectionResponse } from "jslib-common/models/response/organizationConnectionResponse";
|
||||
|
||||
@Component({
|
||||
selector: "app-billing-sync-key",
|
||||
templateUrl: "billing-sync-key.component.html",
|
||||
})
|
||||
export class BillingSyncKeyComponent {
|
||||
entityId: string;
|
||||
existingConnectionId: string;
|
||||
billingSyncKey: string;
|
||||
setParentConnection: (connection: OrganizationConnectionResponse<BillingSyncConfigApi>) => void;
|
||||
|
||||
formPromise: Promise<OrganizationConnectionResponse<BillingSyncConfigApi>> | Promise<void>;
|
||||
|
||||
constructor(private apiService: ApiService, private logService: LogService) {}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new OrganizationConnectionRequest(
|
||||
this.entityId,
|
||||
OrganizationConnectionType.CloudBillingSync,
|
||||
true,
|
||||
new BillingSyncConfigRequest(this.billingSyncKey)
|
||||
);
|
||||
if (this.existingConnectionId == null) {
|
||||
this.formPromise = this.apiService.createOrganizationConnection(
|
||||
request,
|
||||
BillingSyncConfigApi
|
||||
);
|
||||
} else {
|
||||
this.formPromise = this.apiService.updateOrganizationConnection(
|
||||
request,
|
||||
BillingSyncConfigApi,
|
||||
this.existingConnectionId
|
||||
);
|
||||
}
|
||||
const response = (await this
|
||||
.formPromise) as OrganizationConnectionResponse<BillingSyncConfigApi>;
|
||||
this.existingConnectionId = response?.id;
|
||||
this.billingSyncKey = response?.config?.billingSyncKey;
|
||||
this.setParentConnection(response);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteConnection() {
|
||||
this.formPromise = this.apiService.deleteOrganizationConnection(this.existingConnectionId);
|
||||
await this.formPromise;
|
||||
this.setParentConnection(null);
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
if (await this.keyConnectorService.getUsesKeyConnector()) {
|
||||
this.router.navigate(["/settings/security/two-factor"]);
|
||||
}
|
||||
await super.ngOnInit();
|
||||
}
|
||||
|
||||
async rotateEncKeyClicked() {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<div class="page-header">
|
||||
<h1>{{ "newOrganization" | i18n }}</h1>
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="page-header">
|
||||
<h1>{{ "newOrganization" | i18n }}</h1>
|
||||
</div>
|
||||
<p>{{ "newOrganizationDesc" | i18n }}</p>
|
||||
<app-organization-plans></app-organization-plans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ "newOrganizationDesc" | i18n }}</p>
|
||||
<app-organization-plans></app-organization-plans>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||
@@ -68,7 +69,8 @@ export class OrganizationPlansComponent implements OnInit {
|
||||
private syncService: SyncService,
|
||||
private policyService: PolicyService,
|
||||
private organizationService: OrganizationService,
|
||||
private logService: LogService
|
||||
private logService: LogService,
|
||||
private messagingService: MessagingService
|
||||
) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
@@ -298,6 +300,7 @@ export class OrganizationPlansComponent implements OnInit {
|
||||
this.formPromise = doSubmit();
|
||||
const organizationId = await this.formPromise;
|
||||
this.onSuccess.emit({ organizationId: organizationId });
|
||||
this.messagingService.send("organizationCreated", organizationId);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.premium = await this.tokenService.getPremium();
|
||||
this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships();
|
||||
const hasPremiumFromOrg = await this.stateService.getCanAccessPremium();
|
||||
const billing = await this.apiService.getUserBillingHistory();
|
||||
this.hideSubscription = !this.premium && hasPremiumFromOrg && billing.hasNoHistory;
|
||||
let billing = null;
|
||||
if (!this.selfHosted) {
|
||||
billing = await this.apiService.getUserBillingHistory();
|
||||
}
|
||||
this.hideSubscription =
|
||||
!this.premium && hasPremiumFromOrg && (this.selfHosted || billing?.hasNoHistory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,33 +20,55 @@
|
||||
#form
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
[formGroup]="sponsorshipForm"
|
||||
ngNativeValidate
|
||||
*ngIf="anyOrgsAvailable"
|
||||
>
|
||||
<div *ngIf="moreThanOneOrgAvailable" class="form-group col-7">
|
||||
<div class="form-group col-7">
|
||||
<label for="availableSponsorshipOrg">{{ "familiesSponsoringOrgSelect" | i18n }}</label>
|
||||
<select
|
||||
id="availableSponsorshipOrg"
|
||||
name="Available Sponsorship Organization"
|
||||
[(ngModel)]="selectedSponsorshipOrgId"
|
||||
formControlName="selectedSponsorshipOrgId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="">-- {{ "select" | i18n }} --</option>
|
||||
<option disabled="true" value="">-- {{ "select" | i18n }} --</option>
|
||||
<option *ngFor="let o of availableSponsorshipOrgs" [ngValue]="o.id">{{ o.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-7">
|
||||
<label for="accountEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
|
||||
<label for="sponsorshipEmail">{{ "sponsoredFamiliesEmail" | i18n }}:</label>
|
||||
<input
|
||||
id="accountEmail"
|
||||
id="sponsorshipEmail"
|
||||
class="form-control"
|
||||
inputmode="email"
|
||||
[(ngModel)]="sponsorshipEmail"
|
||||
formControlName="sponsorshipEmail"
|
||||
name="sponsorshipEmail"
|
||||
required
|
||||
[attr.aria-invalid]="sponsorshipEmailControl.invalid"
|
||||
/>
|
||||
<button class="btn btn-primary btn-submit mt-4" type="submit" [disabled]="form.loading">
|
||||
<small
|
||||
aria-errormessage="sponsorshipEmail"
|
||||
*ngIf="sponsorshipEmailControl.errors?.notAllowedValue"
|
||||
class="error-inline"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||
{{ "cannotSponsorSelf" | i18n }}
|
||||
</small>
|
||||
<small
|
||||
aria-errormessage="sponsorshipEmail"
|
||||
*ngIf="sponsorshipEmailControl.errors?.email"
|
||||
class="error-inline"
|
||||
role="alert"
|
||||
>
|
||||
<i class="bwi bwi-error" aria-hidden="true"></i>
|
||||
{{ "invalidEmail" | i18n }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group col-7">
|
||||
<button class="btn btn-primary btn-submit mt-2" type="submit" [disabled]="form.loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "redeem" | i18n }}</span>
|
||||
</button>
|
||||
@@ -59,12 +81,18 @@
|
||||
<tr>
|
||||
<th>{{ "recipient" | i18n }}</th>
|
||||
<th>{{ "sponsoringOrg" | i18n }}</th>
|
||||
<th>{{ "status" | i18n }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ng-container *ngFor="let o of activeSponsorshipOrgs">
|
||||
<tr sponsoring-org-row [sponsoringOrg]="o" (sponsorshipRemoved)="load(true)"></tr>
|
||||
<tr
|
||||
sponsoring-org-row
|
||||
[sponsoringOrg]="o"
|
||||
[isSelfHosted]="isSelfHosted"
|
||||
(sponsorshipRemoved)="load(true)"
|
||||
></tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { notAllowedValueAsync } from "jslib-angular/validators/notAllowedValueAsync.validator";
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { OrganizationService } from "jslib-common/abstractions/organization.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { SyncService } from "jslib-common/abstractions/sync.service";
|
||||
import { PlanSponsorshipType } from "jslib-common/enums/planSponsorshipType";
|
||||
import { Organization } from "jslib-common/models/domain/organization";
|
||||
@@ -17,30 +20,54 @@ export class SponsoredFamiliesComponent implements OnInit {
|
||||
|
||||
availableSponsorshipOrgs: Organization[] = [];
|
||||
activeSponsorshipOrgs: Organization[] = [];
|
||||
selectedSponsorshipOrgId = "";
|
||||
sponsorshipEmail = "";
|
||||
|
||||
// Conditional display properties
|
||||
formPromise: Promise<any>;
|
||||
|
||||
sponsorshipForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private syncService: SyncService,
|
||||
private organizationService: OrganizationService
|
||||
) {}
|
||||
private organizationService: OrganizationService,
|
||||
private formBuilder: FormBuilder,
|
||||
private stateService: StateService
|
||||
) {
|
||||
this.sponsorshipForm = this.formBuilder.group({
|
||||
selectedSponsorshipOrgId: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.required],
|
||||
},
|
||||
],
|
||||
sponsorshipEmail: [
|
||||
"",
|
||||
{
|
||||
validators: [Validators.email],
|
||||
asyncValidators: [
|
||||
notAllowedValueAsync(async () => await this.stateService.getEmail(), true),
|
||||
],
|
||||
updateOn: "blur",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
this.formPromise = this.apiService.postCreateSponsorship(this.selectedSponsorshipOrgId, {
|
||||
sponsoredEmail: this.sponsorshipEmail,
|
||||
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
|
||||
friendlyName: this.sponsorshipEmail,
|
||||
});
|
||||
this.formPromise = this.apiService.postCreateSponsorship(
|
||||
this.sponsorshipForm.value.selectedSponsorshipOrgId,
|
||||
{
|
||||
sponsoredEmail: this.sponsorshipForm.value.sponsorshipEmail,
|
||||
planSponsorshipType: PlanSponsorshipType.FamiliesForEnterprise,
|
||||
friendlyName: this.sponsorshipForm.value.sponsorshipEmail,
|
||||
}
|
||||
);
|
||||
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated"));
|
||||
@@ -67,14 +94,19 @@ export class SponsoredFamiliesComponent implements OnInit {
|
||||
);
|
||||
|
||||
if (this.availableSponsorshipOrgs.length === 1) {
|
||||
this.selectedSponsorshipOrgId = this.availableSponsorshipOrgs[0].id;
|
||||
this.sponsorshipForm.patchValue({
|
||||
selectedSponsorshipOrgId: this.availableSponsorshipOrgs[0].id,
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get sponsorshipEmailControl() {
|
||||
return this.sponsorshipForm.controls["sponsorshipEmail"];
|
||||
}
|
||||
|
||||
private async resetForm() {
|
||||
this.sponsorshipEmail = "";
|
||||
this.selectedSponsorshipOrgId = "";
|
||||
this.sponsorshipForm.reset();
|
||||
}
|
||||
|
||||
get anyActiveSponsorships(): boolean {
|
||||
@@ -85,7 +117,7 @@ export class SponsoredFamiliesComponent implements OnInit {
|
||||
return this.availableSponsorshipOrgs.length > 0;
|
||||
}
|
||||
|
||||
get moreThanOneOrgAvailable(): boolean {
|
||||
return this.availableSponsorshipOrgs.length > 1;
|
||||
get isSelfHosted(): boolean {
|
||||
return this.platformUtilsService.isSelfHost();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
{{ sponsoringOrg.familySponsorshipFriendlyName }}
|
||||
</td>
|
||||
<td>{{ sponsoringOrg.name }}</td>
|
||||
<td>
|
||||
<span [ngClass]="statusClass">{{ statusMessage }}</span>
|
||||
</td>
|
||||
<td class="table-action-right">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button
|
||||
*ngIf="!sponsoringOrg.familySponsorshipToDelete"
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownMenuButton"
|
||||
@@ -18,6 +22,7 @@
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<button
|
||||
#resendEmailBtn
|
||||
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
|
||||
[appApiAction]="resendEmailPromise"
|
||||
class="dropdown-item btn-submit"
|
||||
[disabled]="resendEmailBtn.loading"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { formatDate } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
@@ -10,11 +11,15 @@ import { Organization } from "jslib-common/models/domain/organization";
|
||||
selector: "[sponsoring-org-row]",
|
||||
templateUrl: "sponsoring-org-row.component.html",
|
||||
})
|
||||
export class SponsoringOrgRowComponent {
|
||||
export class SponsoringOrgRowComponent implements OnInit {
|
||||
@Input() sponsoringOrg: Organization = null;
|
||||
@Input() isSelfHosted = false;
|
||||
|
||||
@Output() sponsorshipRemoved = new EventEmitter();
|
||||
|
||||
statusMessage = "loading";
|
||||
statusClass: "text-success" | "text-danger" = "text-success";
|
||||
|
||||
revokeSponsorshipPromise: Promise<any>;
|
||||
resendEmailPromise: Promise<any>;
|
||||
|
||||
@@ -25,6 +30,15 @@ export class SponsoringOrgRowComponent {
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setStatus(
|
||||
this.isSelfHosted,
|
||||
this.sponsoringOrg.familySponsorshipToDelete,
|
||||
this.sponsoringOrg.familySponsorshipValidUntil,
|
||||
this.sponsoringOrg.familySponsorshipLastSyncDate
|
||||
);
|
||||
}
|
||||
|
||||
async revokeSponsorship() {
|
||||
try {
|
||||
this.revokeSponsorshipPromise = this.doRevokeSponsorship();
|
||||
@@ -43,6 +57,10 @@ export class SponsoringOrgRowComponent {
|
||||
this.resendEmailPromise = null;
|
||||
}
|
||||
|
||||
get isSentAwaitingSync() {
|
||||
return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate;
|
||||
}
|
||||
|
||||
private async doRevokeSponsorship() {
|
||||
const isConfirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t("revokeSponsorshipConfirmation"),
|
||||
@@ -60,4 +78,53 @@ export class SponsoringOrgRowComponent {
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("reclaimedFreePlan"));
|
||||
this.sponsorshipRemoved.emit();
|
||||
}
|
||||
|
||||
private setStatus(
|
||||
selfHosted: boolean,
|
||||
toDelete?: boolean,
|
||||
validUntil?: Date,
|
||||
lastSyncDate?: Date
|
||||
) {
|
||||
/*
|
||||
* Possible Statuses:
|
||||
* Requested (self-hosted only)
|
||||
* Sent
|
||||
* Active
|
||||
* RequestRevoke
|
||||
* RevokeWhenExpired
|
||||
*/
|
||||
|
||||
if (toDelete && validUntil) {
|
||||
// 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)
|
||||
);
|
||||
this.statusClass = "text-danger";
|
||||
} else if (toDelete) {
|
||||
// They want to delete and we don't have a valid until date so we can
|
||||
// this should only happen on a self-hosted install
|
||||
this.statusMessage = this.i18nService.t("requestRemoved");
|
||||
this.statusClass = "text-danger";
|
||||
} else if (validUntil) {
|
||||
// They don't want to delete and they have a valid until date
|
||||
// that means they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("active");
|
||||
this.statusClass = "text-success";
|
||||
} else if (selfHosted && lastSyncDate) {
|
||||
// We are on a self-hosted install and it has been synced but we have not gotten
|
||||
// a valid until date so we can't know if they are actively sponsoring someone
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
} else if (!selfHosted) {
|
||||
// We are in cloud and all other status checks have been false therefore we have
|
||||
// sent the request but it hasn't been accepted yet
|
||||
this.statusMessage = this.i18nService.t("sent");
|
||||
this.statusClass = "text-success";
|
||||
} else {
|
||||
// We are on a self-hosted install and we have not synced yet
|
||||
this.statusMessage = this.i18nService.t("requested");
|
||||
this.statusClass = "text-success";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,14 @@ export class TaxInfoComponent {
|
||||
this.logService.error(e);
|
||||
}
|
||||
} else {
|
||||
const taxInfo = await this.apiService.getTaxInfo();
|
||||
if (taxInfo) {
|
||||
this.taxInfo.postalCode = taxInfo.postalCode;
|
||||
this.taxInfo.country = taxInfo.country || "US";
|
||||
try {
|
||||
const taxInfo = await this.apiService.getTaxInfo();
|
||||
if (taxInfo) {
|
||||
this.taxInfo.postalCode = taxInfo.postalCode;
|
||||
this.taxInfo.country = taxInfo.country || "US";
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
this.pristine = Object.assign({}, this.taxInfo);
|
||||
@@ -86,9 +90,16 @@ export class TaxInfoComponent {
|
||||
}
|
||||
});
|
||||
|
||||
const taxRates = await this.apiService.getTaxRates();
|
||||
this.taxRates = taxRates.data;
|
||||
this.loading = false;
|
||||
try {
|
||||
const taxRates = await this.apiService.getTaxRates();
|
||||
if (taxRates) {
|
||||
this.taxRates = taxRates.data;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
get taxRate() {
|
||||
|
||||
@@ -295,16 +295,6 @@
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="simplelogin-hostname">{{ "hostname" | i18n }}</label>
|
||||
<input
|
||||
id="simplelogin-hostname"
|
||||
class="form-control"
|
||||
type="text"
|
||||
[(ngModel)]="usernameOptions.forwardedSimpleLoginHostname"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" *ngIf="usernameOptions.forwardedService === 'anonaddy'">
|
||||
<div class="form-group col-4">
|
||||
|
||||
@@ -40,11 +40,13 @@ export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
route,
|
||||
window
|
||||
);
|
||||
// Cannot use Firefox Relay on the web vault yet due to CORS issues with Firefox Relay API
|
||||
this.forwardOptions.splice(
|
||||
this.forwardOptions.findIndex((o) => o.value === "firefoxrelay"),
|
||||
1
|
||||
);
|
||||
if (platformUtilsService.isSelfHost()) {
|
||||
// Cannot use Firefox Relay on self hosted web vaults due to CORS issues with Firefox Relay API
|
||||
this.forwardOptions.splice(
|
||||
this.forwardOptions.findIndex((o) => o.value === "firefoxrelay"),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async history() {
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
organizationName="{{ c.organizationId | orgNameFromId: organizations }}"
|
||||
profileName="{{ profileName }}"
|
||||
(onOrganizationClicked)="onOrganizationClicked(c.organizationId)"
|
||||
></app-org-badge>
|
||||
>
|
||||
</app-org-badge>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<button
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</button>
|
||||
<a
|
||||
href="#"
|
||||
routerLink="/settings/create-organization"
|
||||
routerLink="/create-organization"
|
||||
class="btn btn-primary"
|
||||
*ngIf="!organizations || !organizations.length"
|
||||
>
|
||||
|
||||
@@ -916,7 +916,7 @@
|
||||
},
|
||||
"specialCharacters": {
|
||||
"message": "Special Characters (!@#$%^&*)"
|
||||
},
|
||||
},
|
||||
"numWords": {
|
||||
"message": "Number of Words"
|
||||
},
|
||||
@@ -3034,10 +3034,10 @@
|
||||
"message": "Adjustments to your subscription will result in prorated changes to your billing totals. You cannot invite more than $COUNT$ users without increasing your subscription seats.",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "50"
|
||||
}
|
||||
}
|
||||
"content": "$1",
|
||||
"example": "50"
|
||||
}
|
||||
}
|
||||
},
|
||||
"seatsToAdd": {
|
||||
"message": "Seats To Add"
|
||||
@@ -4395,10 +4395,10 @@
|
||||
"message": "Your Master Password does not meet the policy requirements of this organization. In order to join the organization, you must update your Master Password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
|
||||
},
|
||||
"maximumVaultTimeout": {
|
||||
"message": "Vault Timeout"
|
||||
"message": "Vault Timeout"
|
||||
},
|
||||
"maximumVaultTimeoutDesc": {
|
||||
"message": "Configure a maximum vault timeout for all users."
|
||||
"message": "Configure a maximum vault timeout for all users."
|
||||
},
|
||||
"maximumVaultTimeoutLabel": {
|
||||
"message": "Maximum Vault Timeout"
|
||||
@@ -4594,7 +4594,7 @@
|
||||
"message": "Enter your personal email to redeem Bitwarden Families"
|
||||
},
|
||||
"sponsoredFamiliesLeaveCopy": {
|
||||
"message": "If you leave or are removed from the sponsoring organization, your Families plan will expire at the end of the billing period."
|
||||
"message": "If you remove an offer or are removed from the sponsoring organization, your Families sponsorship will expire at the next renewal date."
|
||||
},
|
||||
"acceptBitwardenFamiliesHelp": {
|
||||
"message": "Accept offer for an existing organization or create a new Families organization."
|
||||
@@ -4669,13 +4669,13 @@
|
||||
"message": "Email Sent"
|
||||
},
|
||||
"revokeSponsorshipConfirmation": {
|
||||
"message": "After removing this account, the Families organization owner will be responsible for this subscription and related invoices. Are you sure you want to continue?"
|
||||
"message": "After removing this account, the Families plan sponsorship will expire at the end of the billing period. You will not be able to redeem a new sponsorship offer until the existing one expires. Are you sure you want to continue?"
|
||||
},
|
||||
"removeSponsorshipSuccess": {
|
||||
"message": "Sponsorship Removed"
|
||||
},
|
||||
"ssoKeyConnectorUnavailable": {
|
||||
"message": "Unable to reach the Key Connector, try again later."
|
||||
"ssoKeyConnectorError": {
|
||||
"message": "Key Connector error: make sure Key Connector is available and working correctly."
|
||||
},
|
||||
"keyConnectorUrl": {
|
||||
"message": "Key Connector URL"
|
||||
@@ -4803,6 +4803,75 @@
|
||||
"freeWithSponsorship": {
|
||||
"message": "FREE with sponsorship"
|
||||
},
|
||||
"viewBillingSyncToken": {
|
||||
"message": "View Billing Sync Token"
|
||||
},
|
||||
"generateBillingSyncToken": {
|
||||
"message": "Generate Billing Sync Token"
|
||||
},
|
||||
"copyPasteBillingSync": {
|
||||
"message": "Copy and paste this token into the Billing Sync settings of your self-hosted organization."
|
||||
},
|
||||
"billingSyncCanAccess": {
|
||||
"message": "Your Billing Sync token can access and edit this organization's subscription settings."
|
||||
},
|
||||
"manageBillingSync": {
|
||||
"message": "Manage Billing Sync"
|
||||
},
|
||||
"setUpBillingSync": {
|
||||
"message": "Set Up Billing Sync"
|
||||
},
|
||||
"generateToken": {
|
||||
"message": "Generate Token"
|
||||
},
|
||||
"rotateToken": {
|
||||
"message": "Rotate Token"
|
||||
},
|
||||
"rotateBillingSyncTokenWarning": {
|
||||
"message": "If you proceed, you will need to re-setup billing sync on your self-hosted server."
|
||||
},
|
||||
"rotateBillingSyncTokenTitle": {
|
||||
"message": "Rotating the Billing Sync Token will invalidate the previous token."
|
||||
},
|
||||
"selfHostingTitle": {
|
||||
"message": "Self-Hosting"
|
||||
},
|
||||
"selfHostingEnterpriseOrganizationSectionCopy": {
|
||||
"message": "To set-up your organization on your own server, you will need to upload your license file. To support Free Families plans and advanced billing capabilities for your self-hosted organization, you will need to set up billing sync."
|
||||
},
|
||||
"billingSyncApiKeyRotated": {
|
||||
"message": "Token rotated."
|
||||
},
|
||||
"billingSync": {
|
||||
"message": "Billing Sync"
|
||||
},
|
||||
"billingSyncDesc": {
|
||||
"message": "Billing Sync provides Free Families plans for members and advanced billing capabilities by linking your self-hosted Bitwarden to the Bitwarden cloud server."
|
||||
},
|
||||
"billingSyncKeyDesc": {
|
||||
"message": "A Billing Sync Token from your cloud organization's subscription settings is required to complete this form."
|
||||
},
|
||||
"billingSyncKey": {
|
||||
"message": "Billing Sync Token"
|
||||
},
|
||||
"active": {
|
||||
"message": "Active"
|
||||
},
|
||||
"inactive": {
|
||||
"message": "Inactive"
|
||||
},
|
||||
"sentAwaitingSync": {
|
||||
"message": "Sent (Awaiting Sync)"
|
||||
},
|
||||
"sent": {
|
||||
"message": "Sent"
|
||||
},
|
||||
"requestRemoved": {
|
||||
"message": "Removed (Awaiting Sync)"
|
||||
},
|
||||
"requested": {
|
||||
"message": "Requested"
|
||||
},
|
||||
"formErrorSummaryPlural": {
|
||||
"message": "$COUNT$ fields above need your attention.",
|
||||
"placeholders": {
|
||||
@@ -4935,6 +5004,46 @@
|
||||
"service": {
|
||||
"message": "Service"
|
||||
},
|
||||
"unknownCipher": {
|
||||
"message": "Unknown Item, you may need to request permission to access this item."
|
||||
},
|
||||
"cannotSponsorSelf": {
|
||||
"message": "You cannot redeem for the active account. Enter a different email."
|
||||
},
|
||||
"revokeWhenExpired": {
|
||||
"message": "Expires $DATE$",
|
||||
"placeholders": {
|
||||
"date": {
|
||||
"content": "$1",
|
||||
"example": "12/31/2020"
|
||||
}
|
||||
}
|
||||
},
|
||||
"awaitingSyncSingular": {
|
||||
"message": "Token rotated $DAYS$ day ago. Update the billing sync token in your self-hosted organization settings.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
"example": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"awaitingSyncPlural": {
|
||||
"message": "Token rotated $DAYS$ days ago. Update the billing sync token in your self-hosted organization settings.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
"content": "$1",
|
||||
"example": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lastSync": {
|
||||
"message": "Last Sync",
|
||||
"Description": "Used as a prefix to indicate the last time a sync occured. Example \"Last sync 1968-11-16 00:00:00\""
|
||||
},
|
||||
"sponsorshipsSynced": {
|
||||
"message": "Self-hosted sponsorships synced."
|
||||
},
|
||||
"billingManagedByProvider": {
|
||||
"message": "Managed by $PROVIDER$",
|
||||
"placeholders": {
|
||||
@@ -4960,8 +5069,5 @@
|
||||
},
|
||||
"apiAccessToken": {
|
||||
"message": "API Access Token"
|
||||
},
|
||||
"unknownCipher": {
|
||||
"message": "Unknown Item, you may need to login with another account to access this item."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-radius: 0.3rem 0.3rem 0 0;
|
||||
justify-content: flex-start;
|
||||
@include themify($themes) {
|
||||
background-color: themed("footerBackgroundColor");
|
||||
|
||||
@@ -14,22 +14,12 @@
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
a.create-organization-link {
|
||||
&:hover {
|
||||
@include themify($themes) {
|
||||
color: themed("iconHover") !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@extend .no-btn;
|
||||
}
|
||||
|
||||
h3,
|
||||
button.filter-button {
|
||||
text-transform: uppercase;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
@include themify($themes) {
|
||||
color: themed("textMuted");
|
||||
@@ -118,6 +108,7 @@
|
||||
}
|
||||
text-decoration: none;
|
||||
}
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
|
||||
Reference in New Issue
Block a user