feat: enforce tenant isolation and shared question bank

This commit is contained in:
2026-07-27 16:59:12 +08:00
parent 28e9a9fa41
commit db4c7b4496
137 changed files with 6402 additions and 112274 deletions

View File

@@ -1,6 +1,17 @@
using Tiku.Application;
using Tiku.Application.Tenancy;
using Tiku.Infrastructure;
using Tiku.Worker;
var builder = Host.CreateApplicationBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("Database")
?? builder.Configuration["DATABASE_URL"]
?? throw new InvalidOperationException(
"Database connection is required. Configure ConnectionStrings:Database or DATABASE_URL.");
builder.Services.AddApplication();
builder.Services.AddInfrastructure(connectionString);
builder.Services.AddOptions<DomainLifecycleOptions>()
.Bind(builder.Configuration.GetSection("TenantDomains"));
builder.Services.AddHostedService<Worker>();
var host = builder.Build();

View File

@@ -1,16 +1,43 @@
using Microsoft.Extensions.Options;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
namespace Tiku.Worker;
public class Worker(ILogger<Worker> logger) : BackgroundService
public class Worker(
IServiceScopeFactory scopeFactory,
IOptions<DomainLifecycleOptions> options,
ILogger<Worker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (logger.IsEnabled(LogLevel.Information))
try
{
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await using var scope = scopeFactory.CreateAsyncScope();
scope.ServiceProvider.GetRequiredService<ITenantContextInitializer>()
.InitializeSystem(null, "Tenant domain DNS and TLS lifecycle worker");
var processed = await scope.ServiceProvider
.GetRequiredService<ITenantDomainLifecycleService>()
.ProcessPendingAsync(stoppingToken);
if (processed > 0)
{
logger.LogInformation("Processed {Count} pending tenant domains.", processed);
}
}
await Task.Delay(1000, stoppingToken);
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception exception)
{
logger.LogError(exception, "Tenant domain lifecycle processing failed.");
}
await Task.Delay(
TimeSpan.FromSeconds(Math.Clamp(options.Value.PollSeconds, 10, 3600)),
stoppingToken);
}
}
}

View File

@@ -1,4 +1,14 @@
{
"TenantDomains": {
"Enabled": true,
"PollSeconds": 60,
"BatchSize": 50,
"DnsJsonEndpoint": "https://cloudflare-dns.com/dns-query",
"VerificationRecordPrefix": "_tiku-verification",
"AllowedCnameTargets": [],
"GatewayBaseUrl": null,
"GatewayApiKey": null
},
"Logging": {
"LogLevel": {
"Default": "Information",