Files
tiku-backend.net/Tiku.Infrastructure/Tenancy/TenantExternalProviderConfigService.cs

211 lines
7.4 KiB
C#

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}");
}
}
}
}