forked from xiongyuxing/tiku-backend.net
feat(saas): implement marketplace and tenant onboarding
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Tenancy;
|
||||
@@ -19,7 +20,8 @@ public sealed class AuthService(
|
||||
ISmsVerificationService smsVerificationService,
|
||||
IAuthSessionStore sessionStore,
|
||||
IWechatOAuthClient wechatOAuthClient,
|
||||
ITenantExternalProviderConfigService providerConfigService) : IAuthService
|
||||
ITenantExternalProviderConfigService providerConfigService,
|
||||
IFeatureAccessService featureAccessService) : IAuthService
|
||||
{
|
||||
private const string PasswordProvider = "password";
|
||||
private const string SmsProvider = "sms";
|
||||
@@ -393,6 +395,9 @@ public sealed class AuthService(
|
||||
throw;
|
||||
}
|
||||
|
||||
await using var transaction = dbContext.Database.CurrentTransaction is null
|
||||
? await dbContext.Database.BeginTransactionAsync(cancellationToken)
|
||||
: null;
|
||||
var providerSubject = $"{config.AppId}:{identity.OpenId}";
|
||||
var user = await UpsertWechatUserAsync(
|
||||
provider,
|
||||
@@ -408,6 +413,10 @@ public sealed class AuthService(
|
||||
// tenant policy and existing membership state have accepted the login.
|
||||
// A denied first login must not leave a user or provider identity behind.
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
if (transaction is not null)
|
||||
{
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return await CompleteSuccessfulLoginAsync(
|
||||
request.Realm,
|
||||
@@ -568,6 +577,11 @@ public sealed class AuthService(
|
||||
throw new TenantAccessDeniedException();
|
||||
}
|
||||
|
||||
await featureAccessService.ConsumeQuotaIfConfiguredAsync(
|
||||
tenantId,
|
||||
SaasQuotaMetricCatalog.StudentCount,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
dbContext.TenantMemberships.Add(new TenantMembership
|
||||
{
|
||||
TenantId = tenantId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user