1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-06-20 09:35:22 +02:00

[AC-1512] Feature: Secrets Manager billing - round 2 (#5854)

* [AC-1423] Update organization subscription cloud page (#5614)

* [AC-1423] Add ProgressModule to shared.module.ts

* [AC-1423] Update cloud subscription page styles

- Remove bootstrap styles
- Use CL components where applicable
- Use CL typography directives
- Update heading levels to prepare for new SM sections

* [AC-1423] Add usePasswordManager boolean to organization domain

* [AC-1423] Introduce BitwardenProductType enum

* [AC-1423] Update Organization subscription line items

- Add product type prefix
- Indent addon services like additional storage and service accounts
- Show line items for free plans

* [AC-1423] Simply sort function

* [AC-1423] Remove header border

* [AC-1423] Make "Password Manager" the default fallback for product name

* [AC-1420] Add Secrets Manager subscribe component (#5617)

* [AC-1418] Add secrets manager manage subscription component (#5661)

* [AC-1423] Add minWidth input to bit-progress component

* [AC-1423] Add ProgressModule to shared.module.ts

* [AC-1423] Update cloud subscription page styles

- Remove bootstrap styles
- Use CL components where applicable
- Use CL typography directives
- Update heading levels to prepare for new SM sections

* [AC-1423] Add usePasswordManager boolean to organization domain

* [AC-1423] Introduce BitwardenProductType enum

* [AC-1423] Update Organization subscription line items

- Add product type prefix
- Indent addon services like additional storage and service accounts
- Show line items for free plans

* [AC-1423] Simply sort function

* [AC-1423] Remove header border

* [AC-1423] Remove redundant condition

* [AC-1423] Remove ineffective div

* [AC-1423] Make "Password Manager" the default fallback for product name

* Revert "[AC-1423] Add minWidth input to bit-progress component"

This reverts commit 95b2223a30.

* [AC-1423] Remove minWidth attribute

* [AC-1423] Switch to AddonProductType enum instead of boolean

* Revert "[AC-1423] Switch to AddonProductType enum instead of boolean"

This reverts commit 204f64b4e7.

* [AC-1423] Tweak sorting comment

* [AC-1418] Add initial SecretsManagerAdjustSubscription component

* [AC-1418] Add initial SM adjustment form

* [AC-1418] Adjust organization-subscription-update.request.ts to support both PM and SM

* [AC-1418] Rename service account fields in the options interface

* [AC-1418] Add api service call to update SM subscription

* [AC-1418] Cleanup form html

* [AC-1418] Add missing SM plan properties

* [AC-1418] Add SM subscription adjust form and logic to hide it

* [AC-1418] Add better docs to options interface

* [AC-1418] Fix conflicting required/optional labels for auto-scaling limits

* [AC-1418] Adjust labels and appearance to better match design

* [AC-1418] Use the SM plan for billing interval

* [AC-1418] Hide SM billing adjustment component behind feature flag

* [AC-1418] Update request model to match server

* [AC-1418] Cleanup BitwardenProductType after merge

Add to barrel file and update applicable imports.

* [AC-1418] Revert change to update PM subscription request model

* [AC-1418] Add new update SM subscription request model

* [AC-1418] Add new service method to update SM subscription

* [AC-1418] Use new model and service method

* [AC-1418] Cleanup SM subscription UI flags

* [AC-1418] Move SM adjust subscription component into SM billing module

* [AC-1418] Update SM seat count minimum to 1

* [AC-1418] Add missing currency codes

* [AC-1418] Simplify monthly price calculation

* [AC-1418] Increase PM adjust subscription form input width

* [AC-1418] Add check for null subscription

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* add the additional properties (#5743)

* Allow autoscale limits to be removed, update naming (#5781)

* [AC-1488] Store Organization.SmServiceAccounts as total not additional (#5784)

* Allow autoscale limits to be removed, update naming

* Display additional service accounts only

---------

Co-authored-by: Shane Melton <smelton@bitwarden.com>

* [AC-1473] SM beta ending callout (#5719)

* [AC-1423] Add minWidth input to bit-progress component

* [AC-1423] Add ProgressModule to shared.module.ts

* [AC-1423] Update cloud subscription page styles

- Remove bootstrap styles
- Use CL components where applicable
- Use CL typography directives
- Update heading levels to prepare for new SM sections

* [AC-1423] Add usePasswordManager boolean to organization domain

* [AC-1423] Introduce BitwardenProductType enum

* [AC-1423] Update Organization subscription line items

- Add product type prefix
- Indent addon services like additional storage and service accounts
- Show line items for free plans

* [AC-1423] Simply sort function

* [AC-1423] Remove header border

* [AC-1423] Remove redundant condition

* [AC-1423] Remove ineffective div

* [AC-1423] Make "Password Manager" the default fallback for product name

* Revert "[AC-1423] Add minWidth input to bit-progress component"

This reverts commit 95b2223a30.

* [AC-1423] Remove minWidth attribute

* [AC-1423] Switch to AddonProductType enum instead of boolean

* Revert "[AC-1423] Switch to AddonProductType enum instead of boolean"

This reverts commit 204f64b4e7.

* [AC-1423] Tweak sorting comment

* [AC-1418] Add initial SecretsManagerAdjustSubscription component

* [AC-1418] Add initial SM adjustment form

* [AC-1418] Adjust organization-subscription-update.request.ts to support both PM and SM

* [AC-1418] Rename service account fields in the options interface

* [AC-1418] Add api service call to update SM subscription

* [AC-1418] Cleanup form html

* [AC-1418] Add missing SM plan properties

* [AC-1418] Add SM subscription adjust form and logic to hide it

* [AC-1418] Add better docs to options interface

* [AC-1418] Fix conflicting required/optional labels for auto-scaling limits

* [AC-1418] Adjust labels and appearance to better match design

* [AC-1418] Use the SM plan for billing interval

* [AC-1418] Hide SM billing adjustment component behind feature flag

* [AC-1418] Update request model to match server

* [AC-1418] Cleanup BitwardenProductType after merge

Add to barrel file and update applicable imports.

* [AC-1418] Revert change to update PM subscription request model

* [AC-1418] Add new update SM subscription request model

* [AC-1418] Add new service method to update SM subscription

* [AC-1418] Use new model and service method

* [AC-1418] Cleanup SM subscription UI flags

* [AC-1418] Move SM adjust subscription component into SM billing module

* [AC-1418] Update SM seat count minimum to 1

* [AC-1418] Add missing currency codes

* [AC-1418] Simplify monthly price calculation

* add daysRemaining util function and unit tests

* [AC-1474] update organization models to include SM beta flag

* add SM beta callout to org subscription page

* update messages.json

* remove beta field from profile org response

* improve daysRemaining code clarity

* set SM beta in org model constructor

* tweak free SM row visibility

* refactor callout description

* Revert "remove beta field from profile org response"

This reverts commit 6c6249e1ec.

* fix dates

* [AC-1468]: hide adjust SM component if beta user

* add sm beta field to org sub response; remove everywhere else

* fix copy

---------

Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>

* [AC-1531] Fix SM subscribe component not showing in free org billing tab (#5848)

Also:

* Fix spacing in layout

* Send zero values for free plans

* Fix: properly delete enroll component

* remove the beta end message for free org (#5877)

* [AC-1458] Update local organization data after subscribing to Secrets Manager (#5888)

* [AC-1567] Fix max additional service account cost estimate (#5923)

* Fix max additional service account cost estimate

* Update i18n string ref

* Make i18n string keys consistent

* [AC-1461] Secrets Manager seat autoscaling cleanup (#5924)

* Remove unused return value from putOrganizationUserBulkEnableSecretsManager

* Fix service account limit validator (#5926)

* Updated Utils.daysRemaining method to calculate result using Math.floor and updated unit tests.

---------

Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Rui Tome <rtome@bitwarden.com>
Co-authored-by: Will Martin <contact@willmartian.com>
Co-authored-by: Álison Fernandes <vvolkgang@users.noreply.github.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
This commit is contained in:
Thomas Rittson 2023-08-05 07:52:55 +10:00 committed by GitHub
parent dad6fedebd
commit b89f31101f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 167 additions and 46 deletions

View File

@ -9,7 +9,7 @@ import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service";

View File

@ -87,13 +87,32 @@
<td bitCell>{{ "passwordManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
</tr>
<tr bitRow *ngIf="userOrg.useSecretsManager">
<tr bitRow *ngIf="userOrg.useSecretsManager && !sub.secretsManagerBeta">
<td bitCell>{{ "secretsManager" | i18n }} - {{ "freeOrganization" | i18n }}</td>
<td bitCell class="tw-text-right">{{ "free" | i18n }}</td>
</tr>
</ng-container>
<tr bitRow *ngIf="sub.secretsManagerBeta">
<td bitCell>
{{ "secretsManager" | i18n }} -
{{ "beta" | i18n }}
({{ "annually" | i18n }}) @
{{ 0 | currency : "$" }}
<span bitBadge badgeType="warning" class="tw-ml-2">{{
"betaEnding" | i18n | uppercase
}}</span>
</td>
<td bitCell class="tw-text-right">{{ 0 | currency : "$" }} /{{ "year" | i18n }}</td>
</tr>
</ng-template>
</bit-table>
<bit-callout
*ngIf="sub.secretsManagerBeta && !userOrg.isFreeOrg"
type="warning"
class="tw-mt-4 tw-block"
>
{{ smBetaEndedDesc }}
</bit-callout>
</div>
</ng-container>

View File

@ -1,3 +1,4 @@
import { DatePipe } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";
@ -17,6 +18,7 @@ import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstraction
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
BillingSyncApiKeyComponent,
@ -45,6 +47,9 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
firstLoaded = false;
loading: boolean;
private readonly _smBetaEndingDate = new Date(2023, 7, 25);
private readonly _smGracePeriodEndingDate = new Date(2023, 9, 24);
private destroy$ = new Subject<void>();
constructor(
@ -57,7 +62,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
private organizationApiService: OrganizationApiServiceAbstraction,
private route: ActivatedRoute,
private dialogService: DialogServiceAbstraction,
private configService: ConfigServiceAbstraction
private configService: ConfigServiceAbstraction,
private datePipe: DatePipe
) {}
async ngOnInit() {
@ -122,6 +128,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.userOrg.useSecretsManager &&
this.subscription != null &&
this.sub.secretsManagerPlan?.hasAdditionalSeatsOption &&
!this.sub.secretsManagerBeta &&
!this.subscription.cancelled &&
!this.subscriptionMarkedForCancel;
@ -256,6 +263,14 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
);
}
get smBetaEndedDesc() {
return this.i18nService.translate(
"smBetaEndedDesc",
this.datePipe.transform(this._smBetaEndingDate),
Utils.daysRemaining(this._smGracePeriodEndingDate).toString()
);
}
cancel = async () => {
if (this.loading) {
return;

View File

@ -5,7 +5,7 @@
<bit-hint>
<strong>{{ "total" | i18n }}:</strong>
{{ formGroup.value.seatCount || 0 }} &times; {{ options.seatPrice | currency : "$" }} =
{{ seatTotal | currency : "$" }} / {{ options.interval | i18n }}
{{ seatTotalCost | currency : "$" }} / {{ options.interval | i18n }}
</bit-hint>
</bit-form-field>
<bit-form-control>
@ -28,7 +28,7 @@
<bit-hint>
<strong>{{ "maxSeatCost" | i18n }}:</strong>
{{ formGroup.value.maxAutoscaleSeats || 0 }} &times;
{{ options.seatPrice | currency : "$" }} = {{ maxSeatTotal | currency : "$" }} /
{{ options.seatPrice | currency : "$" }} = {{ maxSeatTotalCost | currency : "$" }} /
{{ options.interval | i18n }}
</bit-hint>
</bit-form-field>
@ -44,16 +44,14 @@
/>
<bit-hint>
<div>
{{
"additionalServiceAccountsDesc"
| i18n : options.baseServiceAccountCount : (monthlyServiceAccountPrice | currency : "$")
}}
{{ "includedServiceAccounts" | i18n : options.baseServiceAccountCount }}
{{ "addAdditionalServiceAccounts" | i18n : (monthlyServiceAccountPrice | currency : "$") }}
</div>
<div>
<strong>{{ "total" | i18n }}:</strong>
{{ formGroup.value.additionalServiceAccounts || 0 }} &times;
{{ options.additionalServiceAccountPrice | currency : "$" }} =
{{ serviceAccountTotal | currency : "$" }} / {{ options.interval | i18n }}
{{ serviceAccountTotalCost | currency : "$" }} / {{ options.interval | i18n }}
</div>
</bit-hint>
</bit-form-field>
@ -80,10 +78,13 @@
[min]="formGroup.value.additionalServiceAccounts"
/>
<bit-hint>
<div>
{{ "includedServiceAccounts" | i18n : options.baseServiceAccountCount }}
</div>
<strong>{{ "maxServiceAccountCost" | i18n }}:</strong>
{{ formGroup.value.maxAutoscaleServiceAccounts || 0 }} &times;
{{ maxAdditionalServiceAccounts }} &times;
{{ options.additionalServiceAccountPrice | currency : "$" }} =
{{ maxServiceAccountTotal | currency : "$" }} / {{ options.interval | i18n }}
{{ maxServiceAccountTotalCost | currency : "$" }} / {{ options.interval | i18n }}
</bit-hint>
</bit-form-field>
<button type="submit" bitButton buttonType="primary" bitFormButton>

View File

@ -72,24 +72,26 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest
: this.options.additionalServiceAccountPrice / 12;
}
get serviceAccountTotal(): number {
get serviceAccountTotalCost(): number {
return Math.abs(
this.formGroup.value.additionalServiceAccounts * this.options.additionalServiceAccountPrice
);
}
get seatTotal(): number {
get seatTotalCost(): number {
return Math.abs(this.formGroup.value.seatCount * this.options.seatPrice);
}
get maxServiceAccountTotal(): number {
return Math.abs(
(this.formGroup.value.maxAutoscaleServiceAccounts ?? 0) *
this.options.additionalServiceAccountPrice
);
get maxAdditionalServiceAccounts(): number {
const maxTotalServiceAccounts = this.formGroup.value.maxAutoscaleServiceAccounts ?? 0;
return Math.max(0, maxTotalServiceAccounts - this.options.baseServiceAccountCount);
}
get maxSeatTotal(): number {
get maxServiceAccountTotalCost(): number {
return this.maxAdditionalServiceAccounts * this.options.additionalServiceAccountPrice;
}
get maxSeatTotalCost(): number {
return Math.abs((this.formGroup.value.maxAutoscaleSeats ?? 0) * this.options.seatPrice);
}
@ -115,7 +117,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest
if (value.limitServiceAccounts) {
maxAutoscaleServiceAccountsControl.setValidators([
Validators.min(value.additionalServiceAccounts),
Validators.min(value.additionalServiceAccounts + this.options.baseServiceAccountCount),
]);
maxAutoscaleServiceAccountsControl.enable({ emitEvent: false });
} else {

View File

@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, Output } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { SecretsManagerSubscribeRequest } from "@bitwarden/common/billing/models/request/sm-subscribe.request";
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
@ -25,7 +27,8 @@ export class SecretsManagerSubscribeStandaloneComponent {
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationApiService: OrganizationApiServiceAbstraction
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationService: InternalOrganizationServiceAbstraction
) {}
submit = async () => {
@ -37,7 +40,15 @@ export class SecretsManagerSubscribeStandaloneComponent {
? this.formGroup.value.additionalServiceAccounts
: 0;
await this.organizationApiService.subscribeToSecretsManager(this.organization.id, request);
const profileOrganization = await this.organizationApiService.subscribeToSecretsManager(
this.organization.id,
request
);
const organizationData = new OrganizationData(profileOrganization, {
isMember: this.organization.isMember,
isProviderUser: this.organization.isProviderUser,
});
await this.organizationService.upsert(organizationData);
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));

View File

@ -56,10 +56,13 @@
<bit-form-field>
<bit-label>{{ "additionalServiceAccounts" | i18n }}</bit-label>
<input bitInput formControlName="additionalServiceAccounts" type="number" />
<bit-hint>{{
"additionalServiceAccountsDesc"
| i18n : serviceAccountsIncluded : (monthlyCostPerServiceAccount | currency : "$")
}}</bit-hint>
<bit-hint>
{{ "includedServiceAccounts" | i18n : serviceAccountsIncluded }}
{{
"addAdditionalServiceAccounts"
| i18n : (monthlyCostPerServiceAccount | currency : "$")
}}
</bit-hint>
</bit-form-field>
</div>

View File

@ -7027,15 +7027,20 @@
"additionalServiceAccounts": {
"message": "Additional service accounts"
},
"additionalServiceAccountsDesc": {
"message": "Your plan comes with $COUNT$ service accounts. You can add additional service accounts for $COST$ per month.",
"includedServiceAccounts": {
"message": "Your plan comes with $COUNT$ service accounts.",
"placeholders": {
"count": {
"content": "$1",
"example": "50"
},
}
}
},
"addAdditionalServiceAccounts": {
"message": "You can add additional service accounts for $COST$ per month.",
"placeholders": {
"cost": {
"content": "$2",
"content": "$1",
"example": "$0.50"
}
}
@ -7063,5 +7068,24 @@
},
"maxServiceAccountCost": {
"message": "Max potential service account cost"
},
"smBetaEndedDesc": {
"message": "The Secrets Manager Beta ended $BETA_ENDING_DATE$. You have $DAYS$ days left to add Secrets Manager to your paid subscription and maintain access to Secrets Manager data. Contact Customer Success to add Secrets Manager to your subscription.",
"placeholders": {
"beta_ending_date": {
"content": "$1",
"example": "August 1, 2023"
},
"days": {
"content": "$2",
"example": "11"
}
}
},
"betaEnding": {
"message": "Beta Ending"
},
"beta": {
"message": "Beta"
}
}

View File

@ -22,7 +22,7 @@ import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "@bitwarde
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
InternalOrganizationService,
InternalOrganizationServiceAbstraction,
OrganizationService as OrganizationServiceAbstraction,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
@ -583,7 +583,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
deps: [StateServiceAbstraction],
},
{
provide: InternalOrganizationService,
provide: InternalOrganizationServiceAbstraction,
useExisting: OrganizationServiceAbstraction,
},
{

View File

@ -210,7 +210,7 @@ export abstract class OrganizationUserService {
abstract putOrganizationUserBulkEnableSecretsManager(
organizationId: string,
ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>>;
): Promise<void>;
/**
* Delete an organization user

View File

@ -26,6 +26,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
import { OrganizationResponse } from "../../models/response/organization.response";
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
export class OrganizationApiServiceAbstraction {
get: (id: string) => Promise<OrganizationResponse>;
@ -68,5 +69,8 @@ export class OrganizationApiServiceAbstraction {
getSso: (id: string) => Promise<OrganizationSsoResponse>;
updateSso: (id: string, request: OrganizationSsoRequest) => Promise<OrganizationSsoResponse>;
selfHostedSyncLicense: (id: string) => Promise<void>;
subscribeToSecretsManager: (id: string, request: SecretsManagerSubscribeRequest) => Promise<void>;
subscribeToSecretsManager: (
id: string,
request: SecretsManagerSubscribeRequest
) => Promise<ProfileOrganizationResponse>;
}

View File

@ -86,6 +86,7 @@ export abstract class OrganizationService {
hasOrganizations: () => boolean;
}
export abstract class InternalOrganizationService extends OrganizationService {
export abstract class InternalOrganizationServiceAbstraction extends OrganizationService {
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
upsert: (OrganizationData: OrganizationData | OrganizationData[]) => Promise<void>;
}

View File

@ -29,6 +29,7 @@ import { OrganizationApiKeyInformationResponse } from "../../models/response/org
import { OrganizationAutoEnrollStatusResponse } from "../../models/response/organization-auto-enroll-status.response";
import { OrganizationKeysResponse } from "../../models/response/organization-keys.response";
import { OrganizationResponse } from "../../models/response/organization.response";
import { ProfileOrganizationResponse } from "../../models/response/profile-organization.response";
export class OrganizationApiService implements OrganizationApiServiceAbstraction {
constructor(private apiService: ApiService, private syncService: SyncService) {}
@ -311,13 +312,14 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
async subscribeToSecretsManager(
id: string,
request: SecretsManagerSubscribeRequest
): Promise<void> {
return await this.apiService.send(
): Promise<ProfileOrganizationResponse> {
const r = await this.apiService.send(
"POST",
"/organizations/" + id + "/subscribe-secrets-manager",
request,
true,
false
true
);
return new ProfileOrganizationResponse(r);
}
}

View File

@ -2,7 +2,7 @@ import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
import { StateService } from "../../../platform/abstractions/state.service";
import {
InternalOrganizationService as InternalOrganizationServiceAbstraction,
InternalOrganizationServiceAbstraction,
isMember,
} from "../../abstractions/organization/organization.service.abstraction";
import { OrganizationData } from "../../models/data/organization.data";

View File

@ -12,6 +12,7 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
upcomingInvoice: BillingSubscriptionUpcomingInvoiceResponse;
expiration: string;
expirationWithoutGracePeriod: string;
secretsManagerBeta: boolean;
constructor(response: any) {
super(response);
@ -26,5 +27,6 @@ export class OrganizationSubscriptionResponse extends OrganizationResponse {
: new BillingSubscriptionUpcomingInvoiceResponse(upcomingInvoice);
this.expiration = this.getResponseProperty("Expiration");
this.expirationWithoutGracePeriod = this.getResponseProperty("ExpirationWithoutGracePeriod");
this.secretsManagerBeta = this.getResponseProperty("SecretsManagerBeta");
}
}

View File

@ -358,4 +358,32 @@ describe("Utils Service", () => {
expect(actual.protocol).toBe("http:");
});
});
describe("daysRemaining", () => {
beforeAll(() => {
const now = new Date(2023, 9, 2, 10);
jest.spyOn(Date, "now").mockReturnValue(now.getTime());
});
afterAll(() => {
jest.restoreAllMocks();
});
it("should return 0 for equal dates", () => {
expect(Utils.daysRemaining(new Date(2023, 9, 2))).toBe(0);
expect(Utils.daysRemaining(new Date(2023, 9, 2, 12))).toBe(0);
});
it("should return 0 for dates in the past", () => {
expect(Utils.daysRemaining(new Date(2020, 5, 11))).toBe(0);
expect(Utils.daysRemaining(new Date(2023, 9, 1))).toBe(0);
});
it("should handle future dates", () => {
expect(Utils.daysRemaining(new Date(2023, 9, 3, 10))).toBe(1);
expect(Utils.daysRemaining(new Date(2023, 10, 12, 10))).toBe(41);
// leap year
expect(Utils.daysRemaining(new Date(2024, 9, 2, 10))).toBe(366);
});
});
});

View File

@ -538,6 +538,16 @@ export class Utils {
return of(undefined).pipe(switchMap(() => generator()));
}
/**
* Return the number of days remaining before a target date arrives.
* Returns 0 if the day has already passed.
*/
static daysRemaining(targetDate: Date): number {
const diffTime = targetDate.getTime() - Date.now();
const msPerDay = 86400000;
return Math.max(0, Math.floor(diffTime / msPerDay));
}
private static isAppleMobile(win: Window) {
return (
win.navigator.userAgent.match(/iPhone/i) != null ||

View File

@ -209,15 +209,14 @@ export class OrganizationUserServiceImplementation implements OrganizationUserSe
async putOrganizationUserBulkEnableSecretsManager(
organizationId: string,
ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>> {
const r = await this.apiService.send(
): Promise<void> {
await this.apiService.send(
"PUT",
"/organizations/" + organizationId + "/users/enable-secrets-manager",
new OrganizationUserBulkRequest(ids),
true,
true
false
);
return new ListResponse(r, OrganizationUserBulkResponse);
}
putOrganizationUser(

View File

@ -1,6 +1,6 @@
import { ApiService } from "../../../abstractions/api.service";
import { SettingsService } from "../../../abstractions/settings.service";
import { InternalOrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../../admin-console/abstractions/provider.service";
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
@ -55,7 +55,7 @@ export class SyncService implements SyncServiceAbstraction {
private stateService: StateService,
private providerService: ProviderService,
private folderApiService: FolderApiServiceAbstraction,
private organizationService: InternalOrganizationService,
private organizationService: InternalOrganizationServiceAbstraction,
private sendApiService: SendApiService,
private logoutCallback: (expired: boolean) => Promise<void>
) {}