93 lines
5.8 KiB
C#
93 lines
5.8 KiB
C#
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<RuntimePolicy> logger) : BackgroundService
|
|
{
|
|
private Policy? current;
|
|
public InfrastructureEnvelope? Storage { get; private set; }
|
|
public bool Enabled => config.GetValue<bool>("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<InternalClient>();
|
|
current = await client.Send<Policy>("admin", "/internal/management/policy", ct: ct);
|
|
if (config["Management:ServiceName"] == "file") {
|
|
var storage = await client.Send<InfrastructureEnvelope>("admin", "/internal/management/infrastructure/storage", ct: ct);
|
|
if (Storage?.Version != storage.Version) { Storage = storage; services.GetService<IOptionsMonitorCache<StorageOptions>>()?.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<StorageOptions>
|
|
{
|
|
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<StorageOptions>(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<string>();
|
|
provider.AccessKeySecret = credential["accessKeySecret"]?.GetValue<string>();
|
|
}
|
|
}
|
|
options.DefaultProviderCode = configured.DefaultProviderCode; options.Providers = configured.Providers;
|
|
}
|
|
}
|
|
public static class RuntimeManagementExtensions
|
|
{
|
|
public static IServiceCollection AddManagementRuntime(this IServiceCollection services)
|
|
{
|
|
services.AddHttpClient<InternalClient>(c => c.Timeout = TimeSpan.FromSeconds(4));
|
|
services.AddSingleton<RuntimePolicy>(); services.AddHostedService(sp => sp.GetRequiredService<RuntimePolicy>());
|
|
services.AddSingleton<IPostConfigureOptions<StorageOptions>, 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<RuntimePolicy>();
|
|
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<InternalClient>().Send<UserAccess>("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<ManagementDbProbe> 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 };
|
|
});
|
|
}
|