feat: establish dotnet engineering foundation

This commit is contained in:
2026-07-27 15:00:14 +08:00
parent 60a376ea0b
commit 28e9a9fa41
36 changed files with 17301 additions and 89 deletions

View File

@@ -1,7 +1,42 @@
using Microsoft.Extensions.Configuration;
using Tiku.Application.Security;
namespace Tiku.Api.Options;
public static class OptionsValidation
{
public const string DevelopmentSigningKey = "development-only-tiku-signing-key-change-before-production";
public static string ResolveDatabaseConnectionString(
IConfiguration configuration,
bool isDevelopment)
{
var connectionString =
configuration.GetConnectionString("Database") ??
configuration["DATABASE_URL"];
if (!string.IsNullOrWhiteSpace(connectionString))
{
return connectionString;
}
if (isDevelopment)
{
return $"Host=localhost;Database=tiku;Username={Environment.UserName}";
}
throw new InvalidOperationException(
"Database connection is required outside Development. Configure ConnectionStrings:Database or DATABASE_URL.");
}
public static bool BeValidJwtOptions(JwtOptions options, bool isProduction)
{
return !isProduction ||
!string.Equals(
options.SigningKey,
DevelopmentSigningKey,
StringComparison.Ordinal);
}
public static bool BeValidCorsOptions(CorsOptions options)
{
if (options.AllowCredentials && options.AllowedOrigins.Length == 0)

View File

@@ -17,6 +17,7 @@ using Tiku.Api.Security;
using Tiku.Application;
using Tiku.Application.Security;
using Tiku.Infrastructure;
using Tiku.Infrastructure.Commerce;
using Tiku.Infrastructure.Persistence;
using Tiku.Infrastructure.Storage;
@@ -133,10 +134,9 @@ try
});
}
var connectionString =
builder.Configuration.GetConnectionString("Database") ??
builder.Configuration["DATABASE_URL"] ??
"Host=localhost;Database=tiku;Username=postgres";
var connectionString = OptionsValidation.ResolveDatabaseConnectionString(
builder.Configuration,
builder.Environment.IsDevelopment());
builder.Services.AddInfrastructure(connectionString);
builder.Services.Configure<ObjectStorageOptions>(
@@ -172,10 +172,28 @@ try
? useInternalEndpoint
: options.UseInternalEndpoint;
});
builder.Services.AddOptions<TenantSecretEncryptionOptions>()
.Bind(builder.Configuration.GetSection(TenantSecretEncryptionOptions.SectionName))
.PostConfigure(options =>
{
options.KeyId = builder.Configuration["TIKU_TENANT_SECRET_KEY_ID"] ?? options.KeyId;
options.MasterKey = builder.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 => !builder.Environment.IsProduction() ||
!TenantSecretEncryptionOptions.IsDevelopmentDefault(options),
"Production tenant secret encryption cannot use the development master key.")
.ValidateOnStart();
builder.Services.AddOptions<JwtOptions>()
.Bind(builder.Configuration.GetSection("Security:Jwt"))
.ValidateDataAnnotations()
.Validate(
options => OptionsValidation.BeValidJwtOptions(options, builder.Environment.IsProduction()),
"Production JWT signing key must be explicitly configured and cannot use the development key.")
.ValidateOnStart();
var jwtOptions = builder.Configuration
.GetSection("Security:Jwt")

View File

@@ -24,6 +24,15 @@
"WindowSeconds": 60,
"QueueLimit": 0
},
"Security": {
"Jwt": {
"SigningKey": "development-only-tiku-signing-key-change-before-production"
},
"TenantSecrets": {
"KeyId": "development-v1",
"MasterKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
},
"Storage": {
"AliyunOss": {
"Region": "cn-hangzhou",

View File

@@ -84,10 +84,14 @@
"Jwt": {
"Issuer": "tiku-backend",
"Audience": "tiku-api",
"SigningKey": "development-only-tiku-signing-key-change-before-production",
"SigningKey": "",
"AccessTokenMinutes": 30,
"RefreshTokenDays": 30,
"ValidateSessions": true
},
"TenantSecrets": {
"KeyId": "",
"MasterKey": ""
}
},
"AllowedHosts": "*"