feat: modularize external providers

This commit is contained in:
2026-07-27 17:47:21 +08:00
parent db4c7b4496
commit 70b99e6063
47 changed files with 1219 additions and 667 deletions

View File

@@ -1,6 +1,7 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Auth;
using Tiku.Application.Tenancy;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
@@ -12,12 +13,13 @@ public sealed class AuthService(
IPasswordHasher passwordHasher,
ISmsVerificationService smsVerificationService,
ISessionService sessionService,
IWechatOAuthClient wechatOAuthClient) : IAuthService
IWechatOAuthClient wechatOAuthClient,
ITenantExternalProviderConfigService providerConfigService) : IAuthService
{
private const string PasswordProvider = "password";
private const string SmsProvider = "sms";
private const string WechatWebProvider = "wechat_web";
private const string WechatMiniAppProvider = "wechat-miniapp";
private const string WechatMiniAppProvider = "wechat_miniapp";
private static readonly string[] WechatWebProviderAliases = ["wechat_web", "wechat-web", "wechat"];
private static readonly string[] WechatMiniAppProviderAliases = ["wechat-miniapp", "wechat_miniapp", "wechat-mini", "wechatMiniapp"];
private static readonly string[] WechatIdentityProviders = ["wechat_web", "wechat-web", "wechat", "wechat-miniapp", "wechat_miniapp", "wechat-mini", "wechatMiniapp"];
@@ -356,24 +358,30 @@ public sealed class AuthService(
IReadOnlyList<string> aliases,
CancellationToken cancellationToken)
{
var rows = await dbContext.TenantAuthProviders
.Where(entity =>
entity.TenantId == tenantId &&
aliases.Contains(entity.Provider) &&
(entity.Status == TenantAuthProviderStatus.Active ||
entity.Status == TenantAuthProviderStatus.Testing))
.ToListAsync(cancellationToken);
var row = aliases
.Select(alias => rows.FirstOrDefault(entity => entity.Provider == alias))
.FirstOrDefault(entity => entity is not null);
TenantExternalProviderAccount? account = null;
foreach (var alias in aliases)
{
try
{
account = await providerConfigService.GetActiveProviderAsync(
tenantId,
TenantExternalProviderCapability.Identity,
alias,
cancellationToken);
break;
}
catch (TenantExternalProviderException)
{
}
}
if (row is null)
if (account is null)
{
throw new AuthProviderNotConfiguredException(provider);
}
var appId = GetJsonString(row.ConfigPublic, "appId", "clientId");
var appSecret = GetJsonString(row.ConfigPublic, "appSecret", "clientSecret", "secret");
var appId = GetJsonString(account.ConfigPublic, "appId", "clientId");
var appSecret = GetJsonString(account.SecretPayload, "appSecret", "clientSecret", "secret");
if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(appSecret))
{
throw new AuthProviderNotConfiguredException(provider);

View File

@@ -0,0 +1,14 @@
using Tiku.Application.Auth;
namespace Tiku.Infrastructure.Auth;
internal sealed class NoopSmsProvider : ISmsProvider
{
public Task<SmsProviderSendResult> SendAsync(
SmsProviderSendRequest request,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new SmsProviderSendResult("noop", "accepted"));
}
}

View File

@@ -0,0 +1,54 @@
using Tiku.Application.Auth;
namespace Tiku.Infrastructure.Auth;
internal sealed class SelfHostedIdentityProvider(IAuthService authService) : IIdentityProvider
{
public async Task<IdentityProviderResult> AuthenticateAsync(
IdentityProviderRequest request,
CancellationToken cancellationToken = default)
{
var provider = request.Provider.Trim().ToLowerInvariant().Replace("-", "_", StringComparison.Ordinal);
var authenticated = provider switch
{
"password" => await authService.LoginWithPasswordAsync(
new PasswordLoginRequest(
request.TenantId,
request.Identifier,
request.Secret,
request.IpAddress,
request.UserAgent),
cancellationToken),
"sms" => await authService.LoginWithSmsAsync(
new SmsLoginRequest(
request.TenantId,
request.Identifier,
request.Secret,
request.IpAddress,
request.UserAgent),
cancellationToken),
"wechat_web" => await authService.LoginWithWechatWebAsync(
new WechatLoginRequest(
request.TenantId,
request.Secret,
request.IpAddress,
request.UserAgent),
cancellationToken),
"wechat_miniapp" => await authService.LoginWithWechatMiniAppAsync(
new WechatLoginRequest(
request.TenantId,
request.Secret,
request.IpAddress,
request.UserAgent),
cancellationToken),
_ => throw new AuthProviderNotConfiguredException(provider)
};
return new IdentityProviderResult(
provider,
authenticated.UserId.ToString("N"),
authenticated.Phone,
authenticated.Email,
authenticated.Name);
}
}

View File

@@ -5,7 +5,9 @@ using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Auth;
public sealed class SmsVerificationService(TikuDbContext dbContext) : ISmsVerificationService
public sealed class SmsVerificationService(
TikuDbContext dbContext,
ISmsProvider smsProvider) : ISmsVerificationService
{
private const int MaxPhoneRequestsPerHour = 5;
private static readonly TimeSpan CodeLifetime = TimeSpan.FromMinutes(10);
@@ -42,13 +44,22 @@ public sealed class SmsVerificationService(TikuDbContext dbContext) : ISmsVerifi
rateLimit.UpdatedAt = DateTimeOffset.UtcNow;
var code = Random.Shared.Next(100000, 999999).ToString(System.Globalization.CultureInfo.InvariantCulture);
var sendResult = await smsProvider.SendAsync(
new SmsProviderSendRequest(
request.TenantId,
phone,
request.Purpose,
code,
request.IpAddress,
request.UserAgent),
cancellationToken);
var verification = new SmsVerificationCode
{
TenantId = request.TenantId,
Phone = phone,
Purpose = request.Purpose,
CodeHash = SmsCodeHashing.Hash(request.TenantId, phone, request.Purpose, code),
Provider = "mock",
Provider = sendResult.Provider,
Status = SmsVerificationStatus.Sent,
ExpiresAt = DateTimeOffset.UtcNow.Add(CodeLifetime),
IpAddress = request.IpAddress,