using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Security.Claims; using Microsoft.EntityFrameworkCore; namespace IM.InitCommon.Management; public record InfrastructureEnvelope(long Version, JsonObject Value, string Secret); public record ManagementDbProbe(DbContext Context); public sealed class RuntimePolicy(IServiceProvider services, IConfiguration config, ILogger logger) : BackgroundService { private Policy? current; public InfrastructureEnvelope? Storage { get; private set; } public bool Enabled => config.GetValue("Management:Enabled"); public Policy Current => current ?? (!Enabled ? new Policy() : throw new InvalidOperationException("管理配置尚未加载")); public long AppliedVersion => current?.Version ?? 0; public DateTime? LastLoadedAt { get; private set; } protected override async Task ExecuteAsync(CancellationToken ct) { if (!Enabled) return; using var timer = new PeriodicTimer(TimeSpan.FromSeconds(20)); do { try { var client = services.GetRequiredService(); current = await client.Send("admin", "/internal/management/policy", ct: ct); if (config["Management:ServiceName"] == "file") { var storage = await client.Send("admin", "/internal/management/infrastructure/storage", ct: ct); if (Storage?.Version != storage.Version) { Storage = storage; services.GetService>()?.Clear(); } } LastLoadedAt = DateTime.UtcNow; } catch (Exception e) when (!ct.IsCancellationRequested) { logger.LogWarning("Policy refresh failed; keeping last committed version: {Type}", e.GetType().Name); } } while (await timer.WaitForNextTickAsync(ct)); } public void CheckFile(string fileName, long size) { var p = Current; if (p.UploadMaxBytes > 0 && size > p.UploadMaxBytes) throw new IM.DomainCommons.DomainException("文件超过平台上传大小限制"); if (p.AllowedFileTypes.Length > 0 && !p.AllowedFileTypes.Contains(Path.GetExtension(fileName).ToLowerInvariant())) throw new IM.DomainCommons.DomainException("平台不允许上传此文件类型"); } } public sealed class DynamicStorageOptions(RuntimePolicy runtime) : IPostConfigureOptions { public void PostConfigure(string? name, StorageOptions options) { var envelope = runtime.Storage; if (envelope is null || !envelope.Value.ContainsKey("providers")) return; var configured = envelope.Value.Deserialize(new JsonSerializerOptions(JsonSerializerDefaults.Web))!; var secrets = JsonNode.Parse(string.IsNullOrEmpty(envelope.Secret) ? "{}" : envelope.Secret)!.AsObject(); foreach (var (code, provider) in configured.Providers) { if (secrets[code] is JsonObject credential) { provider.AccessKeyId = credential["accessKeyId"]?.GetValue(); provider.AccessKeySecret = credential["accessKeySecret"]?.GetValue(); } } options.DefaultProviderCode = configured.DefaultProviderCode; options.Providers = configured.Providers; } } public static class RuntimeManagementExtensions { public static IServiceCollection AddManagementRuntime(this IServiceCollection services) { services.AddHttpClient(c => c.Timeout = TimeSpan.FromSeconds(4)); services.AddSingleton(); services.AddHostedService(sp => sp.GetRequiredService()); services.AddSingleton, DynamicStorageOptions>(); return services; } public static IApplicationBuilder UseManagementRuntime(this IApplicationBuilder app) => app.Use(async (c, next) => { if (c.Request.Path.StartsWithSegments("/internal/management")) { if (!InternalClient.Authorized(c)) { c.Response.StatusCode = 403; return; } await next(); return; } var policy = c.RequestServices.GetRequiredService(); if (policy.Enabled && c.User.Identity?.IsAuthenticated == true && Guid.TryParse(c.User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId)) { try { var user = await c.RequestServices.GetRequiredService().Send("user", $"/internal/management/access/{userId}", ct: c.RequestAborted); if (!user.Enabled || user.Stamp != c.User.FindFirstValue("session_stamp")) { c.Response.StatusCode = 401; await c.Response.WriteAsJsonAsync(ManagementResult.Fail("账号已被封禁或会话已撤销,请重新登录")); return; } } catch { c.Response.StatusCode = 503; await c.Response.WriteAsJsonAsync(ManagementResult.Fail("账号状态验证暂不可用,请稍后重试")); return; } } await next(); }); public static void MapManagementHealth(this WebApplication app) => app.MapGet("/internal/management/health", async (RuntimePolicy runtime, IConfiguration config, IEnumerable probes, CancellationToken ct) => { var ready = !runtime.Enabled || runtime.LastLoadedAt is not null; foreach (var probe in probes) ready &= await probe.Context.Database.CanConnectAsync(ct); return new { service = config["Management:ServiceName"], status = ready ? "healthy" : "unavailable", configVersion = runtime.AppliedVersion, storageVersion = runtime.Storage?.Version, runtime.LastLoadedAt }; }); }