using Microsoft.Extensions.Options; using Tiku.Application.Auth; using Tiku.Application.Storage; using Tiku.Infrastructure.Assets; using Tiku.Infrastructure.Commerce; using Tiku.Infrastructure.Storage; namespace Tiku.Api.Configuration; internal static class ExternalServiceOptionsExtensions { internal static IServiceCollection AddExternalServiceOptions( this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) { services.Configure( configuration.GetSection(ObjectStorageOptions.SectionName)); services.Configure( configuration.GetSection(AliyunOssOptions.SectionName)); services.PostConfigure(options => { options.DefaultProvider = configuration["STORAGE_DEFAULT_PROVIDER"] ?? options.DefaultProvider; options.DefaultBucket = configuration["STORAGE_DEFAULT_BUCKET"] ?? options.DefaultBucket; options.PublicBaseUrl = configuration["STORAGE_PUBLIC_BASE_URL"] ?? options.PublicBaseUrl; options.AllowedMimePrefixes = SplitLegacyList( configuration["STORAGE_ALLOWED_MIME_PREFIXES"], options.AllowedMimePrefixes); options.AllowedMimeTypes = SplitLegacyList( configuration["STORAGE_ALLOWED_MIME_TYPES"], options.AllowedMimeTypes); options.RequireTenantPrefix = bool.TryParse( configuration["STORAGE_REQUIRE_TENANT_PREFIX"], out var requireTenantPrefix) ? requireTenantPrefix : options.RequireTenantPrefix; options.MaxUploadBytes = long.TryParse( configuration["STORAGE_MAX_UPLOAD_BYTES"], out var maxUploadBytes) ? maxUploadBytes : options.MaxUploadBytes; }); services.PostConfigure(options => { options.Region = configuration["ALIYUN_OSS_REGION"] ?? options.Region; options.Endpoint = configuration["ALIYUN_OSS_ENDPOINT"] ?? options.Endpoint; options.AccessKeyId = configuration["ALIYUN_OSS_ACCESS_KEY_ID"] ?? options.AccessKeyId; options.AccessKeySecret = configuration["ALIYUN_OSS_ACCESS_KEY_SECRET"] ?? options.AccessKeySecret; options.SecurityToken = configuration["ALIYUN_OSS_STS_TOKEN"] ?? options.SecurityToken; options.UseInternalEndpoint = bool.TryParse( configuration["ALIYUN_OSS_INTERNAL"], out var useInternalEndpoint) ? useInternalEndpoint : options.UseInternalEndpoint; }); services.AddOptions() .Validate( options => !environment.IsProduction() || options.DefaultProvider == ObjectStorageProviders.AliyunOss, "Production managed storage must use the configured Aliyun OSS provider.") .ValidateOnStart(); services.AddOptions() .Validate>( (aliyun, storage) => !environment.IsProduction() || storage.Value.DefaultProvider != ObjectStorageProviders.AliyunOss || aliyun.IsConfigured, "Aliyun OSS credentials and region or endpoint are required when it is the default provider.") .ValidateOnStart(); services.AddOptions() .Bind(configuration.GetSection(ClamAvOptions.SectionName)) .Validate(ClamAvOptions.BeValid, "ClamAV settings are invalid.") .Validate>( (clamAv, storage) => clamAv.StreamMaxLength >= storage.Value.MaxUploadBytes, "ClamAV StreamMaxLength must be greater than or equal to the storage max upload size.") .ValidateOnStart(); services.AddOptions() .Bind(configuration.GetSection(TenantSecretEncryptionOptions.SectionName)) .PostConfigure(options => { options.KeyId = configuration["TIKU_TENANT_SECRET_KEY_ID"] ?? options.KeyId; options.MasterKey = configuration["TIKU_TENANT_SECRET_MASTER_KEY"] ?? options.MasterKey; }) .Validate( TenantSecretEncryptionOptions.BeValid, "Tenant secret encryption requires a key ID and a base64-encoded 32-byte master key.") .Validate( options => !environment.IsProduction() || !TenantSecretEncryptionOptions.IsDevelopmentDefault(options), "Production tenant secret encryption cannot use the development master key.") .ValidateOnStart(); services.AddOptions() .Bind(configuration.GetSection(SmsSecurityOptions.SectionName)) .PostConfigure(options => { options.CodePepper = configuration["TIKU_SMS_CODE_PEPPER"] ?? options.CodePepper; }) .Validate( SmsSecurityOptions.BeValid, "SMS security requires a pepper of at least 32 characters, exactly five verification attempts, " + "and positive tenant, phone, IP, and device rate limits.") .ValidateOnStart(); return services; } private static string[] SplitLegacyList(string? value, string[] fallback) { return string.IsNullOrWhiteSpace(value) ? fallback : value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); } }