Skip to content

Commit f953e5e

Browse files
Merge branch 'main' into billing/aspire
2 parents e9e95bf + 07049b3 commit f953e5e

File tree

136 files changed

+14556
-2233
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

136 files changed

+14556
-2233
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ jobs:
263263
264264
- name: Scan Docker image
265265
id: container-scan
266-
uses: anchore/scan-action@0d444ed77d83ee2ba7f5ced0d90d640a1281d762 # v7.3.0
266+
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
267267
with:
268268
image: ${{ steps.image-tags.outputs.primary_tag }}
269269
fail-build: false

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
3636

3737
- name: Install rust
38-
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable
38+
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable
3939
with:
4040
toolchain: stable
4141

src/Api/Auth/Controllers/TwoFactorController.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Bit.Core.Auth.Identity.TokenProviders;
1212
using Bit.Core.Auth.Models.Business.Tokenables;
1313
using Bit.Core.Auth.Services;
14+
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
1415
using Bit.Core.Context;
1516
using Bit.Core.Entities;
1617
using Bit.Core.Exceptions;
@@ -39,6 +40,9 @@ public class TwoFactorController : Controller
3940
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
4041
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
4142
private readonly ITwoFactorEmailService _twoFactorEmailService;
43+
private readonly IStartTwoFactorWebAuthnRegistrationCommand _startTwoFactorWebAuthnRegistrationCommand;
44+
private readonly ICompleteTwoFactorWebAuthnRegistrationCommand _completeTwoFactorWebAuthnRegistrationCommand;
45+
private readonly IDeleteTwoFactorWebAuthnCredentialCommand _deleteTwoFactorWebAuthnCredentialCommand;
4246

4347
public TwoFactorController(
4448
IUserService userService,
@@ -50,7 +54,10 @@ public TwoFactorController(
5054
IDuoUniversalTokenService duoUniversalConfigService,
5155
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
5256
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector,
53-
ITwoFactorEmailService twoFactorEmailService)
57+
ITwoFactorEmailService twoFactorEmailService,
58+
IStartTwoFactorWebAuthnRegistrationCommand startTwoFactorWebAuthnRegistrationCommand,
59+
ICompleteTwoFactorWebAuthnRegistrationCommand completeTwoFactorWebAuthnRegistrationCommand,
60+
IDeleteTwoFactorWebAuthnCredentialCommand deleteTwoFactorWebAuthnCredentialCommand)
5461
{
5562
_userService = userService;
5663
_organizationRepository = organizationRepository;
@@ -62,6 +69,9 @@ public TwoFactorController(
6269
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
6370
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
6471
_twoFactorEmailService = twoFactorEmailService;
72+
_startTwoFactorWebAuthnRegistrationCommand = startTwoFactorWebAuthnRegistrationCommand;
73+
_completeTwoFactorWebAuthnRegistrationCommand = completeTwoFactorWebAuthnRegistrationCommand;
74+
_deleteTwoFactorWebAuthnCredentialCommand = deleteTwoFactorWebAuthnCredentialCommand;
6575
}
6676

6777
[HttpGet("")]
@@ -282,7 +292,7 @@ public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody] SecretV
282292
public async Task<CredentialCreateOptions> GetWebAuthnChallenge([FromBody] SecretVerificationRequestModel model)
283293
{
284294
var user = await CheckAsync(model, false, true);
285-
var reg = await _userService.StartWebAuthnRegistrationAsync(user);
295+
var reg = await _startTwoFactorWebAuthnRegistrationCommand.StartTwoFactorWebAuthnRegistrationAsync(user);
286296
return reg;
287297
}
288298

@@ -291,7 +301,7 @@ public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody] TwoFact
291301
{
292302
var user = await CheckAsync(model, false);
293303

294-
var success = await _userService.CompleteWebAuthRegistrationAsync(
304+
var success = await _completeTwoFactorWebAuthnRegistrationCommand.CompleteTwoFactorWebAuthnRegistrationAsync(
295305
user, model.Id.Value, model.Name, model.DeviceResponse);
296306
if (!success)
297307
{
@@ -314,7 +324,18 @@ public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn(
314324
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
315325
{
316326
var user = await CheckAsync(model, false);
317-
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
327+
328+
if (!model.Id.HasValue)
329+
{
330+
throw new BadRequestException("Unable to delete WebAuthn credential.");
331+
}
332+
333+
var success = await _deleteTwoFactorWebAuthnCredentialCommand.DeleteTwoFactorWebAuthnCredentialAsync(user, model.Id.Value);
334+
if (!success)
335+
{
336+
throw new BadRequestException("Unable to delete WebAuthn credential.");
337+
}
338+
318339
var response = new TwoFactorWebAuthnResponseModel(user);
319340
return response;
320341
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public async Task<IResult> GetSubscriptionAsync(
102102
[BindNever] User user)
103103
{
104104
var subscription = await getBitwardenSubscriptionQuery.Run(user);
105-
return TypedResults.Ok(subscription);
105+
return subscription == null ? TypedResults.NotFound() : TypedResults.Ok(subscription);
106106
}
107107

108108
[HttpPost("subscription/reinstate")]

src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,35 +104,56 @@ await _stripeFacade.UpdateSubscription(invoicedProviderSubscription.Id,
104104
var unpaidSubscriptions = subscriptions?.Data.Where(subscription =>
105105
subscription.Status == StripeConstants.SubscriptionStatus.Unpaid).ToList();
106106

107-
if (unpaidSubscriptions == null || unpaidSubscriptions.Count == 0)
107+
var incompleteSubscriptions = subscriptions?.Data.Where(subscription =>
108+
subscription.Status == StripeConstants.SubscriptionStatus.Incomplete).ToList();
109+
110+
// Process unpaid subscriptions
111+
if (unpaidSubscriptions != null && unpaidSubscriptions.Count > 0)
112+
{
113+
foreach (var subscription in unpaidSubscriptions)
114+
{
115+
await AttemptToPayOpenSubscriptionAsync(subscription);
116+
}
117+
}
118+
119+
// Process incomplete subscriptions - only if there's exactly one to avoid overcharging
120+
if (incompleteSubscriptions == null || incompleteSubscriptions.Count == 0)
108121
{
109122
return;
110123
}
111124

112-
foreach (var unpaidSubscription in unpaidSubscriptions)
125+
if (incompleteSubscriptions.Count > 1)
113126
{
114-
await AttemptToPayOpenSubscriptionAsync(unpaidSubscription);
127+
_logger.LogWarning(
128+
"Customer {CustomerId} has {Count} incomplete subscriptions. Skipping automatic payment retry to avoid overcharging. Subscription IDs: {SubscriptionIds}",
129+
customer.Id,
130+
incompleteSubscriptions.Count,
131+
string.Join(", ", incompleteSubscriptions.Select(s => s.Id)));
132+
return;
115133
}
134+
135+
// Exactly one incomplete subscription - safe to retry
136+
await AttemptToPayOpenSubscriptionAsync(incompleteSubscriptions.First());
116137
}
117138

118-
private async Task AttemptToPayOpenSubscriptionAsync(Subscription unpaidSubscription)
139+
private async Task AttemptToPayOpenSubscriptionAsync(Subscription subscription)
119140
{
120-
var latestInvoice = unpaidSubscription.LatestInvoice;
141+
var latestInvoice = subscription.LatestInvoice;
121142

122-
if (unpaidSubscription.LatestInvoice is null)
143+
if (subscription.LatestInvoice is null)
123144
{
124145
_logger.LogWarning(
125-
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice didn't exist",
126-
unpaidSubscription.Id);
146+
"Attempted to pay subscription {SubscriptionId} with status {Status} but latest invoice didn't exist",
147+
subscription.Id, subscription.Status);
127148

128149
return;
129150
}
130151

131152
if (latestInvoice.Status != StripeInvoiceStatus.Open)
132153
{
133154
_logger.LogWarning(
134-
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice wasn't \"open\"",
135-
unpaidSubscription.Id);
155+
"Attempted to pay subscription {SubscriptionId} with status {Status} but latest invoice wasn't \"open\"",
156+
subscription.Id, subscription.Status);
136157

137158
return;
138159
}
@@ -144,8 +165,8 @@ private async Task AttemptToPayOpenSubscriptionAsync(Subscription unpaidSubscrip
144165
catch (Exception e)
145166
{
146167
_logger.LogError(e,
147-
"Attempted to pay open invoice {InvoiceId} on unpaid subscription {SubscriptionId} but encountered an error",
148-
latestInvoice.Id, unpaidSubscription.Id);
168+
"Attempted to pay open invoice {InvoiceId} on subscription {SubscriptionId} with status {Status} but encountered an error",
169+
latestInvoice.Id, subscription.Id, subscription.Status);
149170
throw;
150171
}
151172
}

src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ public async Task HandleAsync(Event parsedEvent)
6969

7070
var currentPeriodEnd = subscription.GetCurrentPeriodEnd();
7171

72-
if (SubscriptionWentUnpaid(parsedEvent, subscription))
72+
if (SubscriptionWentUnpaid(parsedEvent, subscription) ||
73+
SubscriptionWentIncompleteExpired(parsedEvent, subscription))
7374
{
7475
await DisableSubscriberAsync(subscriberId, currentPeriodEnd);
7576
await SetSubscriptionToCancelAsync(subscription);
@@ -111,6 +112,18 @@ SubscriptionStatus.Active or
111112
LatestInvoice.BillingReason: BillingReasons.SubscriptionCreate or BillingReasons.SubscriptionCycle
112113
};
113114

115+
private static bool SubscriptionWentIncompleteExpired(
116+
Event parsedEvent,
117+
Subscription currentSubscription) =>
118+
parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() is Subscription
119+
{
120+
Status: SubscriptionStatus.Incomplete
121+
} && currentSubscription is
122+
{
123+
Status: SubscriptionStatus.IncompleteExpired,
124+
LatestInvoice.BillingReason: BillingReasons.SubscriptionCreate or BillingReasons.SubscriptionCycle
125+
};
126+
114127
private static bool SubscriptionBecameActive(
115128
Event parsedEvent,
116129
Subscription currentSubscription) =>

0 commit comments

Comments
 (0)