Skip to content

Commit 7148bf2

Browse files
committed
Address review comments
1 parent 1dcb25c commit 7148bf2

File tree

7 files changed

+143
-16
lines changed

7 files changed

+143
-16
lines changed

src/Api/Billing/Controllers/AccountsController.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public class AccountsController(
2424
IUserService userService,
2525
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
2626
IUserAccountKeysQuery userAccountKeysQuery,
27-
IFeatureService featureService) : Controller
27+
IFeatureService featureService,
28+
ILicensingService licensingService) : Controller
2829
{
2930
// TODO: Remove when pm-24996-implement-upgrade-from-free-dialog is removed
3031
[HttpPost("premium")]
@@ -86,6 +87,11 @@ public async Task<SubscriptionResponseModel> GetSubscriptionAsync(
8687
throw new UnauthorizedAccessException();
8788
}
8889

90+
// Check if the new premium subscription page feature flag is enabled
91+
// When enabled, clients should use the separate /license endpoint
92+
// When disabled, include license in subscription response for backward compatibility
93+
var useNewPremiumPage = featureService.IsEnabled(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog);
94+
8995
// Only cloud-hosted users with payment gateways have subscription and discount information
9096
if (!globalSettings.SelfHosted)
9197
{
@@ -96,11 +102,34 @@ public async Task<SubscriptionResponseModel> GetSubscriptionAsync(
96102
// The feature flag controls the broader Milestone 2 feature set, not just this specific task.
97103
var includeMilestone2Discount = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
98104
var subscriptionInfo = await paymentService.GetSubscriptionAsync(user);
99-
return new SubscriptionResponseModel(user, subscriptionInfo, includeMilestone2Discount);
105+
106+
if (useNewPremiumPage)
107+
{
108+
// New flow: Don't include license, clients should call /license endpoint
109+
return new SubscriptionResponseModel(user, subscriptionInfo, includeMilestone2Discount);
110+
}
111+
else
112+
{
113+
// Old flow: Include license for backward compatibility
114+
var license = await userService.GenerateLicenseAsync(user, subscriptionInfo);
115+
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
116+
return new SubscriptionResponseModel(user, subscriptionInfo, license, claimsPrincipal, includeMilestone2Discount);
117+
}
100118
}
101119
else
102120
{
103-
return new SubscriptionResponseModel(user, null);
121+
if (useNewPremiumPage)
122+
{
123+
// New flow: Don't include license
124+
return new SubscriptionResponseModel(user, null);
125+
}
126+
else
127+
{
128+
// Old flow: Include license for backward compatibility
129+
var license = await userService.GenerateLicenseAsync(user);
130+
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
131+
return new SubscriptionResponseModel(user, null, license, claimsPrincipal);
132+
}
104133
}
105134
}
106135
else

src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
using Bit.Api.Billing.Attributes;
22
using Bit.Api.Billing.Models.Requests.Payment;
33
using Bit.Api.Billing.Models.Requests.Premium;
4-
using Bit.Api.Models.Response;
54
using Bit.Core;
5+
using Bit.Core.Billing.Licenses.Queries;
66
using Bit.Core.Billing.Payment.Commands;
77
using Bit.Core.Billing.Payment.Queries;
88
using Bit.Core.Billing.Premium.Commands;
9-
using Bit.Core.Billing.Services;
109
using Bit.Core.Entities;
11-
using Bit.Core.Services;
1210
using Bit.Core.Utilities;
1311
using Microsoft.AspNetCore.Authorization;
1412
using Microsoft.AspNetCore.Mvc;
@@ -24,9 +22,8 @@ public class AccountBillingVNextController(
2422
ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,
2523
IGetCreditQuery getCreditQuery,
2624
IGetPaymentMethodQuery getPaymentMethodQuery,
27-
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
28-
IUserService userService,
29-
ILicensingService licensingService) : BaseBillingController
25+
IGetUserLicenseQuery getUserLicenseQuery,
26+
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
3027
{
3128
[HttpGet("credit")]
3229
[InjectUser]
@@ -93,10 +90,7 @@ public async Task<IResult> GetLicenseAsync(
9390
throw new UnauthorizedAccessException();
9491
}
9592

96-
var license = await userService.GenerateLicenseAsync(user);
97-
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
98-
var response = new LicenseResponseModel(license, claimsPrincipal);
99-
93+
var response = await getUserLicenseQuery.Run(user);
10094
return TypedResults.Ok(response);
10195
}
10296
}

src/Api/Models/Response/SubscriptionResponseModel.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
using Bit.Core.Billing.Constants;
1+
using System.Security.Claims;
2+
using Bit.Core.Billing.Constants;
3+
using Bit.Core.Billing.Licenses;
4+
using Bit.Core.Billing.Licenses.Extensions;
5+
using Bit.Core.Billing.Models.Business;
26
using Bit.Core.Entities;
37
using Bit.Core.Models.Api;
48
using Bit.Core.Models.Business;
@@ -41,6 +45,51 @@ public SubscriptionResponseModel(User user)
4145
MaxStorageGb = user.MaxStorageGb;
4246
}
4347

48+
/// <summary>
49+
/// BACKWARD COMPATIBILITY CONSTRUCTOR
50+
/// This constructor is used when the PM24996ImplementUpgradeFromFreeDialog feature flag is NOT enabled.
51+
/// When the feature flag is enabled, clients should use the new separate /license endpoint.
52+
/// </summary>
53+
/// <param name="user">The user entity containing storage and premium subscription information</param>
54+
/// <param name="subscription">Subscription information retrieved from the payment provider (Stripe/Braintree)</param>
55+
/// <param name="license">The user's license containing expiration and feature entitlements</param>
56+
/// <param name="claimsPrincipal">The claims principal containing cryptographically secure token claims</param>
57+
/// <param name="includeMilestone2Discount">
58+
/// Whether to include discount information in the response.
59+
/// Set to true when the PM23341_Milestone_2 feature flag is enabled AND
60+
/// you want to expose Milestone 2 discount information to the client.
61+
/// The discount will only be included if it matches the specific Milestone 2 coupon ID.
62+
/// </param>
63+
public SubscriptionResponseModel(User user, SubscriptionInfo? subscription, UserLicense license, ClaimsPrincipal? claimsPrincipal, bool includeMilestone2Discount = false)
64+
: base("subscription")
65+
{
66+
Subscription = subscription?.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
67+
UpcomingInvoice = subscription?.UpcomingInvoice != null ?
68+
new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
69+
StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
70+
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
71+
MaxStorageGb = user.MaxStorageGb;
72+
License = license;
73+
74+
// CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim
75+
// The token's expiration is cryptographically secured and cannot be tampered with
76+
// The file's Expires property can be manually edited and should NOT be trusted for display
77+
if (claimsPrincipal != null)
78+
{
79+
Expiration = claimsPrincipal.GetValue<DateTime?>(UserLicenseConstants.Expires);
80+
}
81+
else
82+
{
83+
// No token - use the license file expiration (for older licenses without tokens)
84+
Expiration = license.Expires;
85+
}
86+
87+
// Only display the Milestone 2 subscription discount on the subscription page.
88+
CustomerDiscount = ShouldIncludeMilestone2Discount(includeMilestone2Discount, subscription?.CustomerDiscount)
89+
? new BillingCustomerDiscount(subscription!.CustomerDiscount!)
90+
: null;
91+
}
92+
4493
public string? StorageName { get; set; }
4594
public double? StorageGb { get; set; }
4695
public short? MaxStorageGb { get; set; }
@@ -61,6 +110,24 @@ public SubscriptionResponseModel(User user)
61110
/// </summary>
62111
public BillingCustomerDiscount? CustomerDiscount { get; set; }
63112

113+
/// <summary>
114+
/// BACKWARD COMPATIBILITY PROPERTY
115+
/// The user's license containing feature entitlements and metadata.
116+
/// Only populated when the PM24996ImplementUpgradeFromFreeDialog feature flag is NOT enabled.
117+
/// When the feature flag is enabled, clients should use the separate /license endpoint.
118+
/// </summary>
119+
public UserLicense? License { get; set; }
120+
121+
/// <summary>
122+
/// BACKWARD COMPATIBILITY PROPERTY
123+
/// The license expiration date.
124+
/// Extracted from the cryptographically secured JWT token when available,
125+
/// otherwise falls back to the license file's expiration date.
126+
/// Only populated when the PM24996ImplementUpgradeFromFreeDialog feature flag is NOT enabled.
127+
/// When the feature flag is enabled, clients should use the separate /license endpoint.
128+
/// </summary>
129+
public DateTime? Expiration { get; set; }
130+
64131
/// <summary>
65132
/// Determines whether the Milestone 2 discount should be included in the response.
66133
/// </summary>

src/Core/Billing/Extensions/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Bit.Core.Billing.Caches;
22
using Bit.Core.Billing.Caches.Implementations;
3+
using Bit.Core.Billing.Licenses;
34
using Bit.Core.Billing.Licenses.Extensions;
45
using Bit.Core.Billing.Organizations.Commands;
56
using Bit.Core.Billing.Organizations.Queries;
@@ -28,6 +29,7 @@ public static void AddBillingOperations(this IServiceCollection services)
2829
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
2930
services.AddTransient<ISubscriberService, SubscriberService>();
3031
services.AddLicenseServices();
32+
services.AddLicenseOperations();
3133
services.AddPricingClient();
3234
services.AddPaymentOperations();
3335
services.AddOrganizationLicenseCommandsQueries();

src/Api/Models/Response/LicenseResponseModel.cs renamed to src/Core/Billing/Licenses/Models/Api/Response/LicenseResponseModel.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
using System.Security.Claims;
2-
using Bit.Core.Billing.Licenses;
32
using Bit.Core.Billing.Licenses.Extensions;
43
using Bit.Core.Billing.Models.Business;
54
using Bit.Core.Models.Api;
65

7-
namespace Bit.Api.Models.Response;
6+
namespace Bit.Core.Billing.Licenses.Models.Api.Response;
87

98
/// <summary>
109
/// Response model containing user license information.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Bit.Core.Billing.Licenses.Models.Api.Response;
2+
using Bit.Core.Billing.Services;
3+
using Bit.Core.Entities;
4+
using Bit.Core.Services;
5+
6+
namespace Bit.Core.Billing.Licenses.Queries;
7+
8+
public interface IGetUserLicenseQuery
9+
{
10+
Task<LicenseResponseModel> Run(User user);
11+
}
12+
13+
public class GetUserLicenseQuery(
14+
IUserService userService,
15+
ILicensingService licensingService) : IGetUserLicenseQuery
16+
{
17+
public async Task<LicenseResponseModel> Run(User user)
18+
{
19+
var license = await userService.GenerateLicenseAsync(user);
20+
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
21+
return new LicenseResponseModel(license, claimsPrincipal);
22+
}
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Bit.Core.Billing.Licenses.Queries;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace Bit.Core.Billing.Licenses;
5+
6+
public static class Registrations
7+
{
8+
public static void AddLicenseOperations(this IServiceCollection services)
9+
{
10+
// Queries
11+
services.AddTransient<IGetUserLicenseQuery, GetUserLicenseQuery>();
12+
}
13+
}

0 commit comments

Comments
 (0)