using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using MoShiro.Backend.Application; using MoShiro.Protocol; namespace MoShiro.Backend.Api; /// 审计日志:管理接口里每个写操作、登录都会记一行,这里只读。 public static class AuditEndpoints { private const string Tag = "admin-audit"; /// handler 往这里放一句「这次具体做了什么」(例如新建令牌的名字),守卫记审计时一起写进去。 public const string DetailItem = "moshiro.audit.detail"; public static void Map(RouteGroupBuilder guarded) => guarded.MapGet("/audit", ListAsync) .Api("audit_list", "Audit log of admin actions, newest first", Tag).Returns>().ProducesProblems(400, 401, 403) .Requires(Capability.AuditRead); public static void SetDetail(this HttpContext http, string detail) => http.Items[DetailItem] = detail; private static async Task ListAsync(HttpContext http, AuditService audit, string? actor, string? action, int? limit, string? cursor) { long? before = null; if (cursor is not null) { if (!long.TryParse(cursor, out long parsed)) { return Problems.From(http, ValidationError.Single("cursor", "is not a valid cursor")); } before = parsed; } int size = Math.Clamp(limit ?? 50, 1, 500); IReadOnlyList rows = await audit.ListAsync(new AuditQuery(before, size + 1, string.IsNullOrEmpty(actor) ? null : actor, string.IsNullOrEmpty(action) ? null : action), http.RequestAborted); bool more = rows.Count > size; IReadOnlyList page = [.. rows.Take(size)]; string? next = more ? page[^1].Id.ToString(System.Globalization.CultureInfo.InvariantCulture) : null; return TypedResults.Json(new PageDto([.. page.Select(Mappers.ToDto)], next), ApiJsonContext.Default.PageDtoAuditDto); } }