chore: initial project import
This commit is contained in:
commit
3fc4e5ca53
347 files changed
+56172
No files matched your search
@@ -0,0 +1,130 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Monitor.Backend.Application;
|
||||
|
||||
namespace Monitor.Backend.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Api 层的入口:服务注册、中间件、端点映射。新增资源时在 <see cref="MapMonitorApi"/> 里加一行。
|
||||
/// Api 只调用 Application;错误码到 HTTP 状态的映射只在这一层做一次(<see cref="ErrorCodeHttpMap"/>)。
|
||||
/// </summary>
|
||||
public static class ApiModule
|
||||
{
|
||||
public static IServiceCollection AddMonitorApi(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<SseLimiter>();
|
||||
services.Configure<JsonOptions>(options =>
|
||||
{
|
||||
JsonSerializerOptions json = options.SerializerOptions;
|
||||
json.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
json.DictionaryKeyPolicy = null;
|
||||
json.DefaultIgnoreCondition = JsonIgnoreCondition.Never;
|
||||
json.Converters.Add(new Rfc3339Converter());
|
||||
json.TypeInfoResolverChain.Insert(0, ApiJsonContext.Default);
|
||||
});
|
||||
|
||||
// 框架自己产生的错误(请求体格式不对、路由不存在、未处理的异常)也统一成 problem+json,并带上 code 和 trace_id
|
||||
services.AddProblemDetails(options => options.CustomizeProblemDetails = context =>
|
||||
{
|
||||
int status = context.ProblemDetails.Status ?? context.HttpContext.Response.StatusCode;
|
||||
context.ProblemDetails.Extensions["code"] = DefaultCode(status);
|
||||
context.ProblemDetails.Extensions["trace_id"] = System.Diagnostics.Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;
|
||||
if (status >= 500)
|
||||
{
|
||||
context.ProblemDetails.Detail = null;
|
||||
}
|
||||
});
|
||||
|
||||
services.AddOpenApi("v1");
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 跨域(可选):只对 <c>/api/public</c> 和 <c>/api/admin</c> 生效,只放行名单里的来源,且不带凭据(Cookie 不会跨域),
|
||||
/// 所以跨域的前端只能用 Bearer 令牌。需要放在认证和路由之前。
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseMonitorCors(this IApplicationBuilder app, string? origins)
|
||||
{
|
||||
string[] allowed = (origins ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (allowed.Length == 0)
|
||||
{
|
||||
return app;
|
||||
}
|
||||
|
||||
bool any = allowed.Contains("*");
|
||||
return app.Use(async (http, next) =>
|
||||
{
|
||||
string? origin = http.Request.Headers.Origin;
|
||||
if (origin is null || !http.Request.Path.StartsWithSegments("/api") || !(any || allowed.Contains(origin.TrimEnd('/'), StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
await next(http);
|
||||
return;
|
||||
}
|
||||
|
||||
http.Response.Headers.AccessControlAllowOrigin = any ? "*" : origin;
|
||||
http.Response.Headers.Vary = "Origin";
|
||||
http.Response.Headers.AccessControlExposeHeaders = "Retry-After";
|
||||
if (HttpMethods.IsOptions(http.Request.Method) && http.Request.Headers.ContainsKey("Access-Control-Request-Method"))
|
||||
{
|
||||
http.Response.Headers.AccessControlAllowMethods = "GET, POST, PATCH, PUT, DELETE, OPTIONS";
|
||||
http.Response.Headers.AccessControlAllowHeaders = "Authorization, Content-Type, X-CSRF-Token";
|
||||
http.Response.Headers.AccessControlMaxAge = "600";
|
||||
http.Response.StatusCode = StatusCodes.Status204NoContent;
|
||||
return;
|
||||
}
|
||||
|
||||
await next(http);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>认证中间件:解析 Bearer 令牌或会话 Cookie。需要放在路由之后、端点之前。</summary>
|
||||
public static IApplicationBuilder UseMonitorApi(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseExceptionHandler();
|
||||
app.UseStatusCodePages();
|
||||
app.Use(PrincipalResolver.ResolveAsync);
|
||||
return app;
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapMonitorApi(this IEndpointRouteBuilder app)
|
||||
{
|
||||
RouteGroupBuilder publicGroup = app.MapGroup("/api/public").AddEndpointFilter(PublicGuard.InvokeAsync);
|
||||
QueryEndpoints.Map(publicGroup);
|
||||
LiveEndpoints.Map(publicGroup);
|
||||
HostEndpoints.Map(publicGroup);
|
||||
CatalogEndpoints.Map(publicGroup);
|
||||
|
||||
RouteGroupBuilder admin = app.MapGroup("/api/admin");
|
||||
AuthEndpoints.MapLogin(admin);
|
||||
|
||||
RouteGroupBuilder guarded = admin.MapGroup(string.Empty).AddEndpointFilter(AdminGuard.InvokeAsync);
|
||||
AuthEndpoints.MapSession(guarded);
|
||||
AgentEndpoints.Map(guarded);
|
||||
TaskEndpoints.Map(guarded);
|
||||
DataEndpoints.Map(guarded);
|
||||
SystemEndpoints.Map(guarded);
|
||||
|
||||
OpsEndpoints.Map(app);
|
||||
app.MapOpenApi("/openapi/v1.json");
|
||||
return app;
|
||||
}
|
||||
|
||||
private static string DefaultCode(int status) => status switch
|
||||
{
|
||||
400 => "validation_failed",
|
||||
401 => "unauthorized",
|
||||
403 => "forbidden",
|
||||
404 => "not_found",
|
||||
405 => "method_not_allowed",
|
||||
413 => "payload_too_large",
|
||||
415 => "unsupported_media_type",
|
||||
429 => "too_many_requests",
|
||||
503 => "storage_unavailable",
|
||||
_ => "internal_error",
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user