forked from xiongyuxing/tiku-backend.net
feat: modularize external providers
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Tenancy;
|
||||
|
||||
internal sealed class TenantExternalProviderConfigService(
|
||||
TikuDbContext dbContext,
|
||||
ITenantSecretService tenantSecretService) : ITenantExternalProviderConfigService
|
||||
{
|
||||
public async Task<TenantExternalProviderAccount> GetActiveProviderAsync(
|
||||
Guid tenantId,
|
||||
TenantExternalProviderCapability capability,
|
||||
string? provider = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedProvider = NormalizeProvider(provider);
|
||||
var query = dbContext.TenantExternalProviders
|
||||
.AsNoTracking()
|
||||
.Where(item =>
|
||||
item.TenantId == tenantId &&
|
||||
item.Capability == capability &&
|
||||
item.Status == TenantExternalProviderStatus.Active);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(normalizedProvider))
|
||||
{
|
||||
query = query.Where(item => item.Provider == normalizedProvider);
|
||||
}
|
||||
|
||||
var item = await query
|
||||
.OrderBy(item => item.Priority)
|
||||
.ThenBy(item => item.Provider)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
throw new TenantExternalProviderException(
|
||||
"Tenant external provider is not configured.",
|
||||
"tenant_external_provider_not_configured");
|
||||
}
|
||||
|
||||
var secretPayload = string.IsNullOrWhiteSpace(item.SecretRef)
|
||||
? JsonDocument.Parse("{}").RootElement.Clone()
|
||||
: await tenantSecretService.GetActiveSecretPayloadAsync(
|
||||
tenantId,
|
||||
item.SecretRef,
|
||||
cancellationToken);
|
||||
|
||||
return new TenantExternalProviderAccount(
|
||||
item.TenantId,
|
||||
item.Capability,
|
||||
item.Provider,
|
||||
item.Status,
|
||||
item.DisplayName,
|
||||
item.ConfigPublic,
|
||||
secretPayload,
|
||||
item.SecretRef,
|
||||
item.Priority,
|
||||
item.Metadata);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<TenantExternalProviderItem>> GetProvidersAsync(
|
||||
Guid tenantId,
|
||||
TenantExternalProviderCapability? capability = null,
|
||||
string? provider = null,
|
||||
int? limit = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedProvider = NormalizeProvider(provider);
|
||||
var query = dbContext.TenantExternalProviders
|
||||
.AsNoTracking()
|
||||
.Where(item => item.TenantId == tenantId);
|
||||
|
||||
if (capability.HasValue)
|
||||
{
|
||||
query = query.Where(item => item.Capability == capability.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(normalizedProvider))
|
||||
{
|
||||
query = query.Where(item => item.Provider == normalizedProvider);
|
||||
}
|
||||
|
||||
return await query
|
||||
.OrderBy(item => item.Capability)
|
||||
.ThenBy(item => item.Priority)
|
||||
.ThenBy(item => item.Provider)
|
||||
.Take(Math.Clamp(limit ?? 100, 1, 200))
|
||||
.Select(item => ToItem(item))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<TenantExternalProviderItem> UpsertProviderAsync(
|
||||
Guid tenantId,
|
||||
UpsertTenantExternalProviderCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var provider = NormalizeRequiredProvider(command.Provider);
|
||||
AssertPublicConfig(command.ConfigPublic, "config_public");
|
||||
AssertPublicConfig(command.Metadata, "metadata");
|
||||
|
||||
var item = await dbContext.TenantExternalProviders
|
||||
.SingleOrDefaultAsync(
|
||||
providerConfig =>
|
||||
providerConfig.TenantId == tenantId &&
|
||||
providerConfig.Capability == command.Capability &&
|
||||
providerConfig.Provider == provider,
|
||||
cancellationToken);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
item = new TenantExternalProvider
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Capability = command.Capability,
|
||||
Provider = provider
|
||||
};
|
||||
dbContext.TenantExternalProviders.Add(item);
|
||||
}
|
||||
|
||||
item.Status = command.Status;
|
||||
item.DisplayName = command.DisplayName?.Trim();
|
||||
item.SecretRef = string.IsNullOrWhiteSpace(command.SecretRef) ? null : command.SecretRef.Trim();
|
||||
item.Priority = Math.Clamp(command.Priority ?? item.Priority, 0, 10_000);
|
||||
item.ConfigPublic = JsonObjectOrDefault(command.ConfigPublic);
|
||||
item.Metadata = JsonObjectOrDefault(command.Metadata);
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return ToItem(item);
|
||||
}
|
||||
|
||||
public static string NormalizeRequiredProvider(string provider)
|
||||
{
|
||||
var normalized = NormalizeProvider(provider);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
throw new TenantExternalProviderException("Provider is required.", "provider_required");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static string? NormalizeProvider(string? provider)
|
||||
{
|
||||
var normalized = (provider ?? string.Empty).Trim().ToLowerInvariant().Replace("-", "_", StringComparison.Ordinal);
|
||||
return normalized switch
|
||||
{
|
||||
"wechat" or "wechatpay" or "wxpay" or "wx_pay" => "wechat_pay",
|
||||
"ali_pay" => "alipay",
|
||||
"" => null,
|
||||
_ => normalized
|
||||
};
|
||||
}
|
||||
|
||||
private static TenantExternalProviderItem ToItem(TenantExternalProvider item) =>
|
||||
new(
|
||||
item.Id,
|
||||
item.Capability,
|
||||
item.Provider,
|
||||
item.Status,
|
||||
item.DisplayName,
|
||||
item.SecretRef,
|
||||
item.Priority,
|
||||
item.ConfigPublic,
|
||||
item.Metadata,
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt);
|
||||
|
||||
private static JsonElement JsonObjectOrDefault(JsonElement element) =>
|
||||
element.ValueKind == JsonValueKind.Object
|
||||
? element.Clone()
|
||||
: JsonSerializer.SerializeToElement(new { });
|
||||
|
||||
private static void AssertPublicConfig(JsonElement element, string path)
|
||||
{
|
||||
if (element.ValueKind is JsonValueKind.Undefined or JsonValueKind.Null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
throw new TenantExternalProviderException(
|
||||
$"{path} must be a JSON object.",
|
||||
"provider_config_must_be_object");
|
||||
}
|
||||
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
var key = property.Name.Replace("_", string.Empty, StringComparison.Ordinal).ToLowerInvariant();
|
||||
if (key.Contains("secret", StringComparison.Ordinal) ||
|
||||
key.Contains("token", StringComparison.Ordinal) ||
|
||||
key.Contains("privatekey", StringComparison.Ordinal) ||
|
||||
key is "key" or "apikey" or "accesskey" or "accesskeyid" or "accesskeysecret" or "appsecret" or "apiv3key")
|
||||
{
|
||||
throw new TenantExternalProviderException(
|
||||
$"{path} cannot contain secrets.",
|
||||
"public_config_contains_secret");
|
||||
}
|
||||
|
||||
if (property.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
AssertPublicConfig(property.Value, $"{path}.{property.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user