清理代码
Some checks failed
ci / release-gate (push) Has been cancelled

This commit is contained in:
2026-08-03 12:31:39 +08:00
parent caea0062b0
commit c497a3ca8d
537 changed files with 14171 additions and 12503 deletions

View File

@@ -5,10 +5,10 @@ using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
using Tiku.Application.Security;
using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.Auth;
@@ -20,6 +20,10 @@ public sealed class SmsVerificationService(
IFeatureAccessService featureAccessService,
IOptions<SmsSecurityOptions> securityOptions) : ISmsVerificationService
{
private static readonly TimeSpan CodeLifetime = TimeSpan.FromMinutes(10);
private static readonly SemaphoreSlim InMemoryRateLimitLock = new(1, 1);
private readonly SmsSecurityOptions options = securityOptions.Value;
public SmsVerificationService(
TikuDbContext dbContext,
ISmsProvider smsProvider,
@@ -29,10 +33,6 @@ public sealed class SmsVerificationService(
{
}
private static readonly TimeSpan CodeLifetime = TimeSpan.FromMinutes(10);
private static readonly SemaphoreSlim InMemoryRateLimitLock = new(1, 1);
private readonly SmsSecurityOptions options = securityOptions.Value;
public async Task<SmsSendResult> CreateCodeAsync(
SendSmsCodeRequest request,
CancellationToken cancellationToken = default)
@@ -48,11 +48,9 @@ public sealed class SmsVerificationService(
1,
cancellationToken);
if (!quotaReserved)
{
throw new FeatureAccessException(
"Tenant SMS quota is exhausted.",
"feature_quota_exhausted");
}
var code = RandomNumberGenerator
.GetInt32(100000, 1000000)
@@ -116,13 +114,11 @@ public sealed class SmsVerificationService(
finally
{
if (!providerAccepted)
{
await featureAccessService.ReleaseQuotaAsync(
request.TenantId,
SaasQuotaMetricCatalog.SmsCount,
1,
CancellationToken.None);
}
}
await ExpirePreviousCodesAsync(
@@ -197,10 +193,7 @@ public sealed class SmsVerificationService(
.OrderByDescending(entity => entity.CreatedAt)
.FirstOrDefaultAsync(cancellationToken);
if (verification is null)
{
throw new InvalidCredentialsException("invalid_sms_code");
}
if (verification is null) throw new InvalidCredentialsException("invalid_sms_code");
if (verification.ExpiresAt <= now)
{
@@ -211,10 +204,7 @@ public sealed class SmsVerificationService(
if (HashesMatch(verification.CodeHash, codeHash))
{
var consumed = await TryConsumeAsync(verification.Id, now, cancellationToken);
if (consumed)
{
return;
}
if (consumed) return;
throw new InvalidCredentialsException("invalid_sms_code");
}
@@ -229,10 +219,7 @@ public sealed class SmsVerificationService(
SmsPurpose purpose,
CancellationToken cancellationToken)
{
if (!redisSecurityStore.IsConfigured)
{
return;
}
if (!redisSecurityStore.IsConfigured) return;
var phoneHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(phone)))
.ToLowerInvariant();
@@ -245,10 +232,7 @@ public sealed class SmsVerificationService(
options.MaxVerificationAttempts,
CodeLifetime)
], cancellationToken);
if (!result.Allowed)
{
throw new SmsRateLimitedException();
}
if (!result.Allowed) throw new SmsRateLimitedException();
}
catch (RedisSecurityUnavailableException)
{
@@ -266,7 +250,6 @@ public sealed class SmsVerificationService(
var bucketStart = TruncateToHour(now);
if (redisSecurityStore.IsConfigured)
{
try
{
var distributed = await redisSecurityStore.ConsumeAsync(
@@ -275,16 +258,12 @@ public sealed class SmsVerificationService(
limit.Maximum,
TimeSpan.FromHours(1))).ToArray(),
cancellationToken);
if (!distributed.Allowed)
{
throw new SmsRateLimitedException();
}
if (!distributed.Allowed) throw new SmsRateLimitedException();
}
catch (RedisSecurityUnavailableException)
{
throw new AuthSecurityUnavailableException();
}
}
if (!dbContext.Database.IsRelational())
{
@@ -297,16 +276,16 @@ public sealed class SmsVerificationService(
{
var dimension = ToSnakeCase(limit.Dimension);
var affected = await dbContext.Database.ExecuteSqlInterpolatedAsync($$"""
INSERT INTO sms_send_rate_limits
(tenant_id, dimension, scope_hash, bucket_start, request_count, updated_at)
VALUES
({{request.TenantId}}, {{dimension}}, {{limit.ScopeHash}}, {{bucketStart}}, 1, {{now}})
ON CONFLICT (tenant_id, dimension, scope_hash, bucket_start)
DO UPDATE SET
request_count = sms_send_rate_limits.request_count + 1,
updated_at = EXCLUDED.updated_at
WHERE sms_send_rate_limits.request_count < {{limit.Maximum}}
""", cancellationToken);
INSERT INTO sms_send_rate_limits
(tenant_id, dimension, scope_hash, bucket_start, request_count, updated_at)
VALUES
({{request.TenantId}}, {{dimension}}, {{limit.ScopeHash}}, {{bucketStart}}, 1, {{now}})
ON CONFLICT (tenant_id, dimension, scope_hash, bucket_start)
DO UPDATE SET
request_count = sms_send_rate_limits.request_count + 1,
updated_at = EXCLUDED.updated_at
WHERE sms_send_rate_limits.request_count < {{limit.Maximum}}
""", cancellationToken);
if (affected == 0)
{
@@ -334,10 +313,7 @@ public sealed class SmsVerificationService(
var counter = await dbContext.SmsSendRateLimits.FindAsync(
[tenantId, limit.Dimension, limit.ScopeHash, bucketStart],
cancellationToken);
if (counter?.RequestCount >= limit.Maximum)
{
throw new SmsRateLimitedException();
}
if (counter?.RequestCount >= limit.Maximum) throw new SmsRateLimitedException();
counters.Add((limit, counter));
}
@@ -374,27 +350,24 @@ public sealed class SmsVerificationService(
var limits = new List<RateLimitSpec>
{
CreateLimit(SmsRateLimitDimension.Tenant, $"tenant:{request.TenantId:N}", options.TenantRequestsPerHour),
CreateLimit(SmsRateLimitDimension.Phone, $"phone:{request.TenantId:N}:{phone}", options.PhoneRequestsPerHour)
CreateLimit(SmsRateLimitDimension.Phone, $"phone:{request.TenantId:N}:{phone}",
options.PhoneRequestsPerHour)
};
if (!string.IsNullOrWhiteSpace(request.IpAddress))
{
limits.Add(CreateLimit(
SmsRateLimitDimension.Ip,
$"ip:{request.IpAddress.Trim()}",
options.IpRequestsPerHour));
}
var deviceKey = string.IsNullOrWhiteSpace(request.DeviceId)
? request.UserAgent
: request.DeviceId;
if (!string.IsNullOrWhiteSpace(deviceKey))
{
limits.Add(CreateLimit(
SmsRateLimitDimension.Device,
$"device:{deviceKey.Trim()}",
options.DeviceRequestsPerHour));
}
return limits;
}
@@ -491,9 +464,7 @@ public sealed class SmsVerificationService(
verification.ConsumedAt is not null ||
verification.ExpiresAt <= now ||
verification.Attempts >= options.MaxVerificationAttempts)
{
return false;
}
verification.Status = SmsVerificationStatus.Verified;
verification.ConsumedAt = now;
@@ -530,15 +501,11 @@ public sealed class SmsVerificationService(
verification.ConsumedAt is not null ||
verification.ExpiresAt <= now ||
verification.Attempts >= options.MaxVerificationAttempts)
{
return;
}
verification.Attempts++;
if (verification.Attempts >= options.MaxVerificationAttempts)
{
verification.Status = SmsVerificationStatus.Blocked;
}
await dbContext.SaveChangesAsync(cancellationToken);
}
@@ -546,11 +513,9 @@ public sealed class SmsVerificationService(
private void EnsureValidOptions()
{
if (!SmsSecurityOptions.BeValid(options))
{
throw new InvalidOperationException(
$"{SmsSecurityOptions.SectionName} must contain a pepper of at least 32 characters, " +
"exactly five verification attempts, and positive rate limits.");
}
}
private static bool HashesMatch(string expected, string actual)
@@ -588,4 +553,4 @@ public sealed class SmsVerificationService(
SmsRateLimitDimension Dimension,
string ScopeHash,
int Maximum);
}
}