feat(saas): implement marketplace and tenant onboarding

This commit is contained in:
2026-07-29 13:58:59 +08:00
parent 76606029e2
commit 6db200a2fc
145 changed files with 16862 additions and 94529 deletions

View File

@@ -17,13 +17,15 @@ public sealed class SmsVerificationService(
TikuDbContext dbContext,
ISmsProvider smsProvider,
IRedisSecurityStore redisSecurityStore,
IFeatureAccessService featureAccessService,
IOptions<SmsSecurityOptions> securityOptions) : ISmsVerificationService
{
public SmsVerificationService(
TikuDbContext dbContext,
ISmsProvider smsProvider,
IFeatureAccessService featureAccessService,
IOptions<SmsSecurityOptions> securityOptions)
: this(dbContext, smsProvider, new NullRedisSecurityStore(), securityOptions)
: this(dbContext, smsProvider, new NullRedisSecurityStore(), featureAccessService, securityOptions)
{
}
@@ -40,6 +42,17 @@ public sealed class SmsVerificationService(
var phone = SmsCodeHashing.NormalizePhone(request.Phone);
var now = DateTimeOffset.UtcNow;
await ConsumeRateLimitsAsync(request, phone, now, cancellationToken);
var quotaReserved = await featureAccessService.TryConsumeQuotaAsync(
request.TenantId,
SaasQuotaMetricCatalog.SmsCount,
1,
cancellationToken);
if (!quotaReserved)
{
throw new FeatureAccessException(
"Tenant SMS quota is exhausted.",
"feature_quota_exhausted");
}
var code = RandomNumberGenerator
.GetInt32(100000, 1000000)
@@ -52,6 +65,7 @@ public sealed class SmsVerificationService(
options.CodePepper);
SmsProviderSendResult sendResult;
var providerAccepted = false;
try
{
sendResult = await smsProvider.SendAsync(
@@ -63,6 +77,7 @@ public sealed class SmsVerificationService(
request.IpAddress,
request.UserAgent),
cancellationToken);
providerAccepted = true;
}
catch (Exception exception) when (exception is not OperationCanceledException)
{
@@ -98,6 +113,17 @@ public sealed class SmsVerificationService(
"sms_provider_send_failed",
exception);
}
finally
{
if (!providerAccepted)
{
await featureAccessService.ReleaseQuotaAsync(
request.TenantId,
SaasQuotaMetricCatalog.SmsCount,
1,
CancellationToken.None);
}
}
await ExpirePreviousCodesAsync(
request.TenantId,