196 lines
7.2 KiB
C#
196 lines
7.2 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(
|
|
ITenancyPersistence 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)
|
|
{
|
|
return new TenantExternalProviderItem(
|
|
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)
|
|
{
|
|
return 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}");
|
|
}
|
|
}
|
|
} |